Составление гистограммы Python: NumPy, Matplotlib, Pandas & Seaborn

Составление гистограммы Python: NumPy, Matplotlib, Pandas & Seaborn

В этом учебном пособии вы будете подготовлены к созданию качественных, готовых к презентации графиков гистограмм Python с широким выбором и функциями.

Если у вас есть начальные знания по промежуточным знаниям в Python и статистике, вы можете использовать эту статью как универсальное средство для построения и построения гистограмм в Python с использованием библиотек из своего научного стека, включая NumPy, Matplotlib, Pandas и Seaborn.

Гистограмма - отличный инструмент для быстрой оценкиprobability distribution, который интуитивно понятен практически любой аудитории. Python предлагает несколько различных вариантов построения и построения гистограмм. Большинство людей знают гистограмму по ее графическому представлению, которое похоже на гистограмму:

Histogram of commute times for 1000 commuters

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

  • Построение гистограмм на чистом Python без использования сторонних библиотек

  • Построение гистограмм с помощью NumPy для обобщения основных данных

  • Построение полученной гистограммы с помощью Matplotlib, Pandas и Seaborn

Free Bonus: Мало времени? Click here to get access to a free two-page Python histograms cheat sheet, который обобщает методы, описанные в этом руководстве.

Гистограммы в чистом Python

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

>>>

>>> # Need not be sorted, necessarily
>>> a = (0, 1, 1, 1, 2, 3, 7, 7, 23)

>>> def count_elements(seq) -> dict:
...     """Tally elements from `seq`."""
...     hist = {}
...     for i in seq:
...         hist[i] = hist.get(i, 0) + 1
...     return hist

>>> counted = count_elements(a)
>>> counted
{0: 1, 1: 3, 2: 1, 3: 1, 7: 2, 23: 1}

count_elements() возвращает словарь с уникальными элементами из последовательности в качестве ключей и их частотами (счетчиками) в качестве значений. В цикле надseqhist[i] = hist.get(i, 0) + 1 говорит: «Для каждого элемента последовательности увеличивайте его соответствующее значение вhist на 1».

Фактически, это именно то, что делает классcollections.Counter из стандартной библиотеки Python, которыйhttps://github.com/python/cpython/blob/7f1bcda9bc3c04100cb047373732db0eba00e581/Lib/collections/init.py # L466 [подклассы] словаря Python и переопределяет его.update() метод:

>>>

>>> from collections import Counter

>>> recounted = Counter(a)
>>> recounted
Counter({0: 1, 1: 3, 3: 1, 2: 1, 7: 2, 23: 1})

Вы можете подтвердить, что ваша ручная функция выполняет практически то же самое, что иcollections.Counter, проверив их равенство:

>>>

>>> recounted.items() == counted.items()
True

Technical Detail: отображениеcount_elements() по умолчанию на более оптимизированныйC function, если он доступен. В функции Pythoncount_elements() одна микрооптимизация, которую вы можете сделать, - это объявитьget = hist.get перед циклом for. Это связывает метод с переменной для более быстрых вызовов в цикле.

Это может быть полезно для создания упрощенных функций с нуля в качестве первого шага к пониманию более сложных. Давайте еще немного изобретем велосипед с гистограммой ASCII, которая использует Pythonoutput formatting:

def ascii_histogram(seq) -> None:
    """A horizontal frequency-table/histogram plot."""
    counted = count_elements(seq)
    for k in sorted(counted):
        print('{0:5d} {1}'.format(k, '+' * counted[k]))

Эта функция создает отсортированный частотный график, на котором счетчики представлены в виде чисел плюсов (+). Вызовsorted() в словаре возвращает отсортированный список его ключей, а затем вы получаете доступ к соответствующему значению для каждого с помощьюcounted[k]. Чтобы увидеть это в действии, вы можете создать немного больший набор данных с модулем Pythonrandom:

>>>

>>> # No NumPy ... yet
>>> import random
>>> random.seed(1)

>>> vals = [1, 3, 4, 6, 8, 9, 10]
>>> # Each number in `vals` will occur between 5 and 15 times.
>>> freq = (random.randint(5, 15) for _ in vals)

>>> data = []
>>> for f, v in zip(freq, vals):
...     data.extend([v] * f)

>>> ascii_histogram(data)
    1 +++++++
    3 ++++++++++++++
    4 ++++++
    6 +++++++++
    8 ++++++
    9 ++++++++++++
   10 ++++++++++++

Здесь вы моделируете выщипывание изvals с частотами, заданными какfreq (agenerator expression). В результирующих выборочных данных каждое значение изvals повторяется определенное количество раз от 5 до 15.

Note:random.seed() используется для заполнения или инициализации базового генератора псевдослучайных чисел (PRNG), используемогоrandom. Это может звучать как оксюморон, но это способ сделать случайные данные воспроизводимыми и детерминированными. То есть, если вы скопируете код здесь как есть, вы должны получить точно такую ​​же гистограмму, потому что первый вызовrandom.randint() после заполнения генератора выдаст идентичные «случайные» данные с использованиемMersenne Twister.

Создание на основе: вычисления гистограммы в NumPy

До сих пор вы работали с тем, что лучше всего назвать «частотными таблицами». Но математически гистограмма - это отображение бинов (интервалов) на частотах. Более технически его можно использовать для аппроксимации функции плотности вероятности (PDF) базовой переменной.

Переходя от приведенной выше «таблицы частот», истинная гистограмма сначала «связывает» диапазон значений, а затем подсчитывает количество значений, попадающих в каждую ячейку. Это то, что делает функцияNumPy’shistogram(), и это основа для других функций, которые вы увидите здесь позже в библиотеках Python, таких как Matplotlib и Pandas.

Рассмотрим пример поплавков, взятых изLaplace distribution. Это распределение имеет более толстые хвосты, чем нормальное распределение, и имеет два описательных параметра (местоположение и масштаб):

>>>

>>> import numpy as np
>>> # `numpy.random` uses its own PRNG.
>>> np.random.seed(444)
>>> np.set_printoptions(precision=3)

>>> d = np.random.laplace(loc=15, scale=3, size=500)
>>> d[:5]
array([18.406, 18.087, 16.004, 16.221,  7.358])

В этом случае вы работаете с непрерывным распределением, и было бы не очень полезно подсчитывать каждое число с плавающей запятой независимо до десятого знака после запятой. Вместо этого вы можете скопировать или «собрать» данные и сосчитать наблюдения, попадающие в каждую ячейку. Гистограмма - это итоговое количество значений в каждом бине:

>>>

>>> hist, bin_edges = np.histogram(d)

>>> hist
array([ 1,  0,  3,  4,  4, 10, 13,  9,  2,  4])

>>> bin_edges
array([ 3.217,  5.199,  7.181,  9.163, 11.145, 13.127, 15.109, 17.091,
       19.073, 21.055, 23.037])

Этот результат может быть не сразу интуитивным. np.histogram() по умолчанию использует 10 интервалов одинакового размера и возвращает кортеж из значений частоты и соответствующих границ интервалов. Они являются ребрами в том смысле, что их будет больше, чем элементов гистограммы:

>>>

>>> hist.size, bin_edges.size
(10, 11)

Technical Detail: все, кроме последнего (крайнего правого) контейнера, наполовину открыты. То есть все ячейки, кроме последней, [включительно, эксклюзивно), а окончательная ячейка [включительно, включительно].

Очень сжатая разбивка того, как построены биныby NumPy, выглядит так:

>>>

>>> # The leftmost and rightmost bin edges
>>> first_edge, last_edge = a.min(), a.max()

>>> n_equal_bins = 10  # NumPy's default
>>> bin_edges = np.linspace(start=first_edge, stop=last_edge,
...                         num=n_equal_bins + 1, endpoint=True)
...
>>> bin_edges
array([ 0. ,  2.3,  4.6,  6.9,  9.2, 11.5, 13.8, 16.1, 18.4, 20.7, 23. ])

Вышеприведенный случай имеет большой смысл: 10 одинаково разнесенных корзин в диапазоне 23 между пиками означают интервалы ширины 2.3.

Оттуда функция делегируетnp.bincount() илиnp.searchsorted(). Самbincount() может быть использован для эффективного построения «таблицы частот», с которой вы начали здесь, с той разницей, что включены значения с нулевыми вхождениями:

>>>

>>> bcounts = np.bincount(a)
>>> hist, _ = np.histogram(a, range=(0, a.max()), bins=a.max() + 1)

>>> np.array_equal(hist, bcounts)
True

>>> # Reproducing `collections.Counter`
>>> dict(zip(np.unique(a), bcounts[bcounts.nonzero()]))
{0: 1, 1: 3, 2: 1, 3: 1, 7: 2, 23: 1}

Note:hist здесь действительно использует ячейки шириной 1,0, а не «дискретные» счетчики. Следовательно, это работает только для подсчета целых чисел, а не для чисел с плавающей запятой, например[3.9, 4.1, 4.15].

Визуализация гистограмм с помощью Matplotlib и Pandas

Теперь, когда вы увидели, как построить гистограмму в Python с нуля, давайте посмотрим, как другие пакеты Python могут выполнить эту работу за вас. Matplotlib предоставляет функциональные возможности для визуализации гистограмм Python из коробки с универсальной оболочкой дляhistogram() NumPy:

import matplotlib.pyplot as plt

# An "interface" to matplotlib.axes.Axes.hist() method
n, bins, patches = plt.hist(x=d, bins='auto', color='#0504aa',
                            alpha=0.7, rwidth=0.85)
plt.grid(axis='y', alpha=0.75)
plt.xlabel('Value')
plt.ylabel('Frequency')
plt.title('My Very Own Histogram')
plt.text(23, 45, r'$\mu=15, b=3$')
maxfreq = n.max()
# Set a clean upper y-axis limit.
plt.ylim(ymax=np.ceil(maxfreq / 10) * 10 if maxfreq % 10 else maxfreq + 10)

Histogram

Как было определено ранее, график гистограммы использует края бина на оси х и соответствующие частоты на оси у. В приведенной выше таблице при передачеbins='auto' выбирается один из двух алгоритмов для оценки «идеального» количества интервалов. На высоком уровне цель алгоритма состоит в том, чтобы выбрать ширину ячейки, которая генерирует наиболее точное представление данных. Для получения дополнительной информации по этому вопросу, который может быть довольно техническим, ознакомьтесь сChoosing Histogram Bins из документации Astropy.

Оставаясь в научном стеке Python, Pandas ’Series.histogram()uses matplotlib.pyplot.hist(), чтобы нарисовать гистограмму Matplotlib входной серии:

import pandas as pd

# Generate data on commute times.
size, scale = 1000, 10
commutes = pd.Series(np.random.gamma(scale, size=size) ** 1.5)

commutes.plot.hist(grid=True, bins=20, rwidth=0.9,
                   color='#607c8e')
plt.title('Commute Times for 1,000 Commuters')
plt.xlabel('Counts')
plt.ylabel('Commute Time')
plt.grid(axis='y', alpha=0.75)

Histogram of commute times for 1000 commuters

pandas.DataFrame.histogram() аналогичен, но создает гистограмму для каждого столбца данных в DataFrame.

Построение оценки плотности ядра (KDE)

В этом уроке вы работали с образцами, говоря статистически. Независимо от того, являются ли данные дискретными или непрерывными, предполагается, что они получены из совокупности, которая имеет истинное, точное распределение, описываемое всего несколькими параметрами.

Оценка плотности ядра (KDE) - это способ оценки функции плотности вероятности (PDF) случайной величины, которая «лежит в основе» нашей выборки. KDE - это средство сглаживания данных.

Придерживаясь библиотеки Pandas, вы можете создавать и накладывать графики плотности с помощьюplot.kde(), который доступен как для объектовSeries, так и дляDataFrame. Но сначала давайте сгенерируем две отдельные выборки данных для сравнения:

>>>

>>> # Sample from two different normal distributions
>>> means = 10, 20
>>> stdevs = 4, 2
>>> dist = pd.DataFrame(
...     np.random.normal(loc=means, scale=stdevs, size=(1000, 2)),
...     columns=['a', 'b'])
>>> dist.agg(['min', 'max', 'mean', 'std']).round(decimals=2)
          a      b
min   -1.57  12.46
max   25.32  26.44
mean  10.12  19.94
std    3.94   1.94

Теперь, чтобы построить каждую гистограмму на одних и тех же осях Matplotlib:

fig, ax = plt.subplots()
dist.plot.kde(ax=ax, legend=False, title='Histogram: A vs. B')
dist.plot.hist(density=True, ax=ax)
ax.set_ylabel('Probability')
ax.grid(axis='y')
ax.set_facecolor('#d8dcd6')

Histogram

Эти методы используютgaussian_kde() SciPy, что приводит к более плавному оформлению PDF.

Если вы внимательно посмотрите на эту функцию, вы увидите, насколько хорошо она аппроксимирует «истинный» PDF для сравнительно небольшой выборки из 1000 точек данных. Ниже вы можете сначала построить «аналитическое» распределение с помощьюscipy.stats.norm(). Это экземпляр класса, который инкапсулирует статистическое стандартное нормальное распределение, его моменты и описательные функции. Его PDF является «точным» в том смысле, что он точно определяется какnorm.pdf(x) = exp(-x**2/2) / sqrt(2*pi).

Исходя из этого, вы можете взять случайную выборку из 1000 точек данных из этого распределения, а затем попытаться вернуться к оценке PDF с помощьюscipy.stats.gaussian_kde():

from scipy import stats

# An object representing the "frozen" analytical distribution
# Defaults to the standard normal distribution, N~(0, 1)
dist = stats.norm()

# Draw random samples from the population you built above.
# This is just a sample, so the mean and std. deviation should
# be close to (1, 0).
samp = dist.rvs(size=1000)

# `ppf()`: percent point function (inverse of cdf — percentiles).
x = np.linspace(start=stats.norm.ppf(0.01),
                stop=stats.norm.ppf(0.99), num=250)
gkde = stats.gaussian_kde(dataset=samp)

# `gkde.evaluate()` estimates the PDF itself.
fig, ax = plt.subplots()
ax.plot(x, dist.pdf(x), linestyle='solid', c='red', lw=3,
        alpha=0.8, label='Analytical (True) PDF')
ax.plot(x, gkde.evaluate(x), linestyle='dashed', c='black', lw=2,
        label='PDF Estimated via KDE')
ax.legend(loc='best', frameon=False)
ax.set_title('Analytical vs. Estimated PDF')
ax.set_ylabel('Probability')
ax.text(-2., 0.35, r'$f(x) = \frac{\exp(-x^2/2)}{\sqrt{2*\pi}}$',
        fontsize=12)

Chart

Это большая часть кода, поэтому давайте уделим секунду, чтобы коснуться нескольких ключевых строк:

  • SciPy'sstats subpackage позволяет создавать объекты Python, которые представляют аналитические распределения, из которых вы можете выбирать образцы для создания фактических данных. Итак,dist = stats.norm() представляет собой нормальную непрерывную случайную величину, и вы генерируете из нее случайные числа с помощьюdist.rvs().

  • Чтобы оценить как аналитическую PDF, так и гауссовский KDE, вам понадобится массивx квантилей (стандартные отклонения выше / ниже среднего для нормального распределения). stats.gaussian_kde() представляет собой примерный PDF-файл, который вам нужно оценить на массиве, чтобы получить что-то визуально значимое в этом случае.

  • Последняя строка содержит несколькоLaTex, которые прекрасно интегрируются с Matplotlib.

Необычная альтернатива с Seaborn

Давайте добавим еще один пакет Python. У Seaborn есть функцияdisplot(), которая строит гистограмму и KDE для одномерного распределения за один шаг. Используя массив NumPyd от ealier:

import seaborn as sns

sns.set_style('darkgrid')
sns.distplot(d)

Seaborn’s distplot

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

sns.distplot(d, fit=stats.laplace, kde=False)

Histogram with fitted laplace distribution

Опять же, обратите внимание на небольшую разницу. В первом случае вы оцениваете какой-то неизвестный PDF; во втором вы берете известное распределение и находите, какие параметры лучше всего описывают его, исходя из эмпирических данных.

Другие инструменты в Пандах

В дополнение к инструментам построения графиков Pandas также предлагает удобный метод.value_counts(), который вычисляет гистограмму ненулевых значений для PandasSeries:

>>>

>>> import pandas as pd

>>> data = np.random.choice(np.arange(10), size=10000,
...                         p=np.linspace(1, 11, 10) / 60)
>>> s = pd.Series(data)

>>> s.value_counts()
9    1831
8    1624
7    1423
6    1323
5    1089
4     888
3     770
2     535
1     347
0     170
dtype: int64

>>> s.value_counts(normalize=True).head()
9    0.1831
8    0.1624
7    0.1423
6    0.1323
5    0.1089
dtype: float64

В другом местеpandas.cut() - удобный способ разбить значения на произвольные интервалы. Допустим, у вас есть некоторые данные о возрасте людей и вы хотите их разумно представить:

>>>

>>> ages = pd.Series(
...     [1, 1, 3, 5, 8, 10, 12, 15, 18, 18, 19, 20, 25, 30, 40, 51, 52])
>>> bins = (0, 10, 13, 18, 21, np.inf)  # The edges
>>> labels = ('child', 'preteen', 'teen', 'military_age', 'adult')
>>> groups = pd.cut(ages, bins=bins, labels=labels)

>>> groups.value_counts()
child           6
adult           5
teen            3
military_age    2
preteen         1
dtype: int64

>>> pd.concat((ages, groups), axis=1).rename(columns={0: 'age', 1: 'group'})
    age         group
0     1         child
1     1         child
2     3         child
3     5         child
4     8         child
5    10         child
6    12       preteen
7    15          teen
8    18          teen
9    18          teen
10   19  military_age
11   20  military_age
12   25         adult
13   30         adult
14   40         adult
15   51         adult
16   52         adult

Приятно то, что обе эти операции в конечном итогеutilize Cython code, что делает их конкурентоспособными по скорости при сохранении их гибкости.

Хорошо, так что я должен использовать?

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

У вас есть / вы хотите Рассмотрите возможность использования Примечания)

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

collections.Counter() из стандартной библиотеки Python предлагает быстрый и простой способ получить счетчики частоты из контейнера данных.

Это частотная таблица, поэтому в ней не используется концепция биннинга, как в «истинной» гистограмме.

Большой массив данных, и вы хотите вычислить «математическую» гистограмму, которая представляет интервалы и соответствующие частоты.

NumPy'snp.histogram() иnp.bincount() полезны для численного вычисления значений гистограммы и соответствующих границ интервала.

Чтобы узнать больше, посетитеnp.digitize().

Табличные данные в объектеSeries илиDataFrame Pandas.

Методы Pandas, такие какSeries.plot.hist(),DataFrame.plot.hist(),Series.value_counts() иcut(), а такжеSeries.plot.kde() иDataFrame.plot.kde().

Посмотрите на Pandasvisualization docs для вдохновения.

Создавайте настраиваемый и точно настраиваемый график из любой структуры данных.

pyplot.hist() - широко используемая функция построения гистограмм, которая используетnp.histogram() и является основой для функций построения графиков Pandas.

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

Готовый дизайн и интеграция.

distplot() Сиборна для объединения гистограммы и графика KDE или построения графика подгонки распределения.

По сути, это «оболочка вокруг оболочки», которая использует внутреннюю гистограмму Matplotlib, которая, в свою очередь, использует NumPy.

Free Bonus: Мало времени? Click here to get access to a free two-page Python histograms cheat sheet, который обобщает методы, описанные в этом руководстве.

Вы также можете найти фрагменты кода из этой статьи вместе в одномscript на странице материалов Real Python.

С этим, удачи в создании гистограмм в дикой природе. Надеемся, что один из инструментов выше подойдет для ваших нужд. Что бы вы ни делали, простоdon’t use a pie chart.

Related