web-dev-qa-db-fra.com

Histogramme circulaire pour Python

J'ai des données périodiques et leur distribution est mieux visualisée autour d'un cercle. Maintenant, la question est de savoir comment puis-je faire cette visualisation en utilisant matplotlib? Sinon, cela peut-il être fait facilement en Python?

Mon code ici démontrera une approximation grossière de la distribution autour d'un cercle:

from matplotlib import pyplot as plt
import numpy as np

#generatin random data
a=np.random.uniform(low=0,high=2*np.pi,size=50)

#real circle
b=np.linspace(0,2*np.pi,1000)
a=sorted(a)
plt.plot(np.sin(a)*0.5,np.cos(a)*0.5)
plt.plot(np.sin(b),np.cos(b))
plt.show()

enter image description here

Il y a quelques exemples dans une question sur SX pour Mathematicaenter image description hereenter image description here

32
Cupitor

Construire à partir de cet exemple de la galerie, vous pouvez le faire

enter image description here

import numpy as np
import matplotlib.pyplot as plt

N = 80
bottom = 8
max_height = 4

theta = np.linspace(0.0, 2 * np.pi, N, endpoint=False)
radii = max_height*np.random.Rand(N)
width = (2*np.pi) / N

ax = plt.subplot(111, polar=True)
bars = ax.bar(theta, radii, width=width, bottom=bottom)

# Use custom colors and opacity
for r, bar in Zip(radii, bars):
    bar.set_facecolor(plt.cm.jet(r / 10.))
    bar.set_alpha(0.8)

plt.show()

Bien sûr, il existe de nombreuses variantes et tweeks, mais cela devrait vous aider à démarrer.

En général, parcourir la galerie matplotlib est généralement un bon point de départ.

Ici, j'ai utilisé le mot clé bottom pour laisser le centre vide, car je pense avoir vu une question précédente de votre part avec un graphique plus semblable à ce que j'ai, donc je suppose que c'est ce que vous voulez. Pour obtenir les coins complets que vous montrez ci-dessus, utilisez simplement bottom=0 (ou laissez-le de côté depuis 0 est la valeur par défaut).

36
tom10

J'ai 5 ans de retard sur cette question, mais de toute façon ...

Je recommanderais toujours la prudence lors de l'utilisation d'histogrammes circulaires car ils peuvent facilement induire les lecteurs en erreur.

En particulier, je vous conseille de rester à l'écart des histogrammes circulaires où fréquence et rayon sont tracés proportionnellement. Je le recommande car l'esprit est grandement affecté par la zone des bacs, pas seulement par leur étendue radiale. Ceci est similaire à la façon dont nous sommes habitués à interpréter les graphiques circulaires: par zone.

Ainsi, au lieu d'utiliser l'étendue radial d'un bac pour visualiser le nombre de points de données qu'il contient, je recommande de visualiser le nombre de points par zone.

Le problème

Considérez les conséquences d'un doublement du nombre de points de données dans une case d'histogramme donnée. Dans un histogramme circulaire où la fréquence et le rayon sont proportionnels, le rayon de ce bac augmentera d'un facteur 2 (car le nombre de points a doublé). Cependant, la surface de ce bac aura été multipliée par 4! En effet, l'aire du bac est proportionnelle au rayon au carré.

Si cela ne vous semble pas encore trop problématique, voyons cela graphiquement:

frequency histograms

Les deux graphiques ci-dessus visualisent les mêmes points de données.

Dans le graphique de gauche, il est facile de voir qu'il y a deux fois plus de points de données dans le bac (0, pi/4) que dans le bac (-pi/4, 0).

Cependant, jetez un œil au graphique de droite (fréquence proportionnelle au rayon). À première vue, votre esprit est grandement affecté par la surface des bacs. Vous seriez pardonné de penser qu'il y a plus que deux fois plus de points dans le bac (0, pi/4) que dans le bac (-pi/4, 0). Cependant, vous auriez été induit en erreur. Ce n'est qu'en examinant de plus près le graphique (et l'axe radial) que vous réalisez qu'il y a exactement deux fois plus de points de données dans le bac (0, pi/4) que dans le (- pi/4, 0) bin. Pas plus de deux fois plus, comme le graphique l'a peut-être suggéré à l'origine.

Les graphiques ci-dessus peuvent être recréés avec le code suivant:

import numpy as np
import matplotlib.pyplot as plt
plt.style.use('seaborn')

# Generate data with twice as many points in (0, np.pi/4) than (-np.pi/4, 0)
angles = np.hstack([np.random.uniform(0, np.pi/4, size=100),
                    np.random.uniform(-np.pi/4, 0, size=50)])

bins = 2

fig = plt.figure()
ax = fig.add_subplot(1, 2, 1)
polar_ax = fig.add_subplot(1, 2, 2, projection="polar")

# Plot "standard" histogram
ax.hist(angles, bins=bins)
# Fiddle with labels and limits
ax.set_xlim([-np.pi/4, np.pi/4])
ax.set_xticks([-np.pi/4, 0, np.pi/4])
ax.set_xticklabels([r'$-\pi/4$', r'$0$', r'$\pi/4$'])

# bin data for our polar histogram
count, bin = np.histogram(angles, bins=bins)
# Plot polar histogram
polar_ax.bar(bin[:-1], count, align='Edge', color='C0')

# Fiddle with labels and limits
polar_ax.set_xticks([0, np.pi/4, 2*np.pi - np.pi/4])
polar_ax.set_xticklabels([r'$0$', r'$\pi/4$', r'$-\pi/4$'])
polar_ax.set_rlabel_position(90)

fig.tight_layout()

Une solution

Étant donné que nous sommes tellement affectés par la zone des cases dans les histogrammes circulaires, je trouve plus efficace de veiller à ce que la surface de chaque case soit proportionnelle au nombre d'observations qu'elle contient, au lieu de le rayon. Ceci est similaire à la façon dont nous sommes habitués à interpréter les graphiques circulaires, où la zone est la quantité d'intérêt.

Utilisons le jeu de données que nous avons utilisé dans l'exemple précédent pour reproduire les graphiques en fonction de l'aire, au lieu du rayon:

density histograms

Je fais l'hypothèse que les lecteurs ont moins de chances d'être induits en erreur à première vue de ce graphique.

Cependant, lorsque vous tracez un histogramme circulaire avec une aire proportionnelle au rayon, nous avons l'inconvénient que vous n'auriez jamais su qu'il y avait exactement deux fois plus de points dans le bac (0, pi/4) que dans le bac (-pi/4, 0) juste en regardant les zones. Cependant, vous pouvez contrer cela en annotant chaque bac avec sa densité correspondante. Je pense que cet inconvénient est préférable à induire en erreur un lecteur.

Bien sûr, je veillerais à ce qu'une légende informative soit placée à côté de cette figure pour expliquer que nous visualisons ici la fréquence avec une zone, pas un rayon.

Les tracés ci-dessus ont été créés comme:

fig = plt.figure()
ax = fig.add_subplot(1, 2, 1)
polar_ax = fig.add_subplot(1, 2, 2, projection="polar")

# Plot "standard" histogram
ax.hist(angles, bins=bins, density=True)
# Fiddle with labels and limits
ax.set_xlim([-np.pi/4, np.pi/4])
ax.set_xticks([-np.pi/4, 0, np.pi/4])
ax.set_xticklabels([r'$-\pi/4$', r'$0$', r'$\pi/4$'])

# bin data for our polar histogram
counts, bin = np.histogram(angles, bins=bins)
# Normalise counts to compute areas
area = counts / angles.size
# Compute corresponding radii from areas
radius = (area / np.pi)**.5

polar_ax.bar(bin[:-1], radius, align='Edge', color='C0')

# Label angles according to convention
polar_ax.set_xticks([0, np.pi/4, 2*np.pi - np.pi/4])
polar_ax.set_xticklabels([r'$0$', r'$\pi/4$', r'$-\pi/4$'])

fig.tight_layout()

Mettre tous ensemble

Si vous créez beaucoup d'histogrammes circulaires, vous feriez mieux de créer une fonction de traçage que vous pouvez réutiliser facilement. Ci-dessous, j'inclus une fonction que j'ai écrite et que j'utilise dans mon travail.

Par défaut, la fonction visualise par zone, comme je l'ai recommandé. Cependant, si vous préférez toujours visualiser les cases avec un rayon proportionnel à la fréquence, vous pouvez le faire en passant density=False. De plus, vous pouvez utiliser l'argument offset pour définir la direction de l'angle zéro et lab_unit pour définir si les étiquettes doivent être en degrés ou en radians.

def rose_plot(ax, angles, bins=16, density=None, offset=0, lab_unit="degrees",
              start_zero=False, **param_dict):
    """
    Plot polar histogram of angles on ax. ax must have been created using
    subplot_kw=dict(projection='polar'). Angles are expected in radians.
    """
    # Wrap angles to [-pi, pi)
    angles = (angles + np.pi) % (2*np.pi) - np.pi

    # Set bins symetrically around zero
    if start_zero:
        # To have a bin Edge at zero use an even number of bins
        if bins % 2:
            bins += 1
        bins = np.linspace(-np.pi, np.pi, num=bins+1)

    # Bin data and record counts
    count, bin = np.histogram(angles, bins=bins)

    # Compute width of each bin
    widths = np.diff(bin)

    # By default plot density (frequency potentially misleading)
    if density is None or density is True:
        # Area to assign each bin
        area = count / angles.size
        # Calculate corresponding bin radius
        radius = (area / np.pi)**.5
    else:
        radius = count

    # Plot data on ax
    ax.bar(bin[:-1], radius, zorder=1, align='Edge', width=widths,
           edgecolor='C0', fill=False, linewidth=1)

    # Set the direction of the zero angle
    ax.set_theta_offset(offset)

    # Remove ylabels, they are mostly obstructive and not informative
    ax.set_yticks([])

    if lab_unit == "radians":
        label = ['$0$', r'$\pi/4$', r'$\pi/2$', r'$3\pi/4$',
                  r'$\pi$', r'$5\pi/4$', r'$3\pi/2$', r'$7\pi/4$']
        ax.set_xticklabels(label)

Il est super facile d'utiliser cette fonction. Ici, je démontre qu'il est utilisé pour certaines directions générées de manière aléatoire:

angles0 = np.random.normal(loc=0, scale=1, size=10000)
angles1 = np.random.uniform(0, 2*np.pi, size=1000)

# Visualise with polar histogram
fig, ax = plt.subplots(1, 2, subplot_kw=dict(projection='polar'))
rose_plot(ax[0], angles0)
rose_plot(ax[1], angles1, lab_unit="radians")
fig.tight_layout()

example images

9
Ralph