У меня есть периодические данные, а распределение для них лучше всего визуализируется вокруг круга. Теперь вопрос в том, как я могу сделать эту визуализацию с помощью matplotlib? Если нет, можно ли это сделать легко в Python?
Ответ 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]()