Подтвердить что ты не робот

Круговая гистограмма для Python

У меня есть периодические данные, а распределение для них лучше всего визуализируется вокруг круга. Теперь вопрос в том, как я могу сделать эту визуализацию с помощью matplotlib? Если нет, можно ли это сделать легко в Python?

Мой код здесь продемонстрирует грубое приближение распределения по кругу:

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

В вопросе SX есть несколько примеров для Mathematica: enter image description hereenter image description here

4b9b3361

Ответ 1

Создание этого примера из галереи, вы можете сделать

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()

Конечно, есть много вариантов и твиков, но это должно заставить вас начать.

В общем, просмотр галереи matplotlib обычно является хорошим местом для начала.

Здесь я использовал ключевое слово bottom, чтобы оставить центр пустым, потому что, как я думаю, я видел более ранний вопрос, с графиком, похожим на то, что у меня есть, поэтому я предполагаю, что вы хотите. Чтобы получить полный клин, который вы показываете выше, просто используйте bottom=0 (или оставьте его, так как 0 по умолчанию).

Ответ 2

Я на 5 лет опоздал на этот вопрос, но все равно...

Я всегда рекомендую осторожность при использовании круговых гистограмм, поскольку они могут легко ввести читателей в заблуждение.

В частности, я бы посоветовал держаться подальше от круговых гистограмм, где частота и радиус изображены пропорционально. Я рекомендую это, потому что на ум сильно влияет площадь бункеров, а не только их радиальная протяженность. Это похоже на то, как мы привыкли интерпретировать круговые диаграммы: по областям.

Поэтому вместо того, чтобы использовать радиальный экстент ячейки для визуализации количества содержащихся в ней точек данных, я бы рекомендовал визуализировать количество точек по областям.

Эта проблема

Рассмотрим последствия удвоения количества точек данных в данном бункере гистограммы. На круговой гистограмме, где частота и радиус пропорциональны, радиус этого бина будет увеличиваться в 2 раза (так как количество точек удвоилось). Тем не менее, площадь этой корзины будет увеличена в 4 раза! Это потому, что площадь бункера пропорциональна квадрату радиуса.

Если это не кажется слишком большой проблемой, давайте посмотрим на это графически:

frequency histograms

Оба приведенных графика отображают одинаковые точки данных.

На левом графике легко увидеть, что в бине (0, pi/4) точек данных в два раза больше, чем в (-pi/4, 0) бина.

Однако взгляните на график справа (частота, пропорциональная радиусу). На первый взгляд ваш ум сильно зависит от площади мусорных ведер. Вам будет прощено думать, что в корзине (0, pi/4) больше чем в два раза больше точек, чем в корзине (-pi/4, 0). Тем не менее, вы бы ввели в заблуждение. Только при более внимательном рассмотрении графики (и радиальной оси) вы понимаете, что в бункере (0, pi/4) ровно в два раза больше точек данных, чем в (-pi/4, 0). Не более чем вдвое больше, чем могло показаться на графике.

Приведенную выше графику можно воссоздать с помощью следующего кода:

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()

Решение

Так как площадь бинов в круговых гистограммах так сильно зависит от нас, я считаю более эффективным обеспечить, чтобы площадь каждого бина была пропорциональна количеству наблюдений в нем, а не радиусу. Это похоже на то, как мы привыкли интерпретировать круговые диаграммы, где площадь - это количество интереса.

Давайте использовать набор данных, который мы использовали в предыдущем примере, чтобы воспроизвести графику на основе площади вместо радиуса:

density histograms

Я предполагаю, что у читателей меньше шансов быть введенными в заблуждение с первого взгляда на эту графику.

Однако при построении круговой гистограммы с площадью, пропорциональной радиусу, у нас есть недостаток, заключающийся в том, что вы никогда не знали бы, что в бине (0, pi/4) ровно в два раза больше точек, чем в (-pi/4, 0) мусорное ведро, просто наблюдая за областями. Хотя вы можете противостоять этому, аннотируя каждый бин соответствующей плотностью. Я думаю, что этот недостаток предпочтительнее, чем вводить читателя в заблуждение.

Конечно, я бы позаботился о том, чтобы рядом с этим рисунком была размещена информационная подпись, чтобы объяснить, что здесь мы визуализируем частоту с площадью, а не с радиусом.

Вышеуказанные участки были созданы как:

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()

Собираем все вместе

Если вы создаете много круговых гистограмм, вам лучше всего создать некоторую функцию построения графиков, которую вы можете легко использовать повторно. Ниже я включаю функцию, которую я написал и использую в своей работе.

По умолчанию функция визуализируется по области, как я рекомендовал. Тем не менее, если вы все еще предпочитаете визуализировать ячейки с радиусом, пропорциональным частоте, вы можете сделать это, передав density=False. Кроме того, вы можете использовать аргумент offset чтобы установить направление нулевого угла, и lab_unit чтобы указать, должны ли метки быть в градусах или радианах.

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)

Это очень легко использовать эту функцию. Здесь я демонстрирую его использование для некоторых случайно сгенерированных направлений:

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