Визуализация статистических распределений

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

import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
from scipy import stats

Визуализация одномерных распределений

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

penguins = sns.load_dataset("penguins")
penguins.head()
species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g sex
0 Adelie Torgersen 39.1 18.7 181.0 3750.0 Male
1 Adelie Torgersen 39.5 17.4 186.0 3800.0 Female
2 Adelie Torgersen 40.3 18.0 195.0 3250.0 Female
3 Adelie Torgersen NaN NaN NaN NaN NaN
4 Adelie Torgersen 36.7 19.3 193.0 3450.0 Female

Эти данные о трех видах пингвинов и содержат следующую информацию:

  • species - информация о виде пингвина;
  • island - название острова в архипелаге Палмера;
  • bill_length_mm - длина клюва в мм;
  • bill_depth_mm - толщина клюва в мм;
  • flipper_length_mm - длина крыла в мм;
  • body_mass_g - масса тела в граммах;
  • sex - пол особи.

Теперь мы можем построить гистограмму какой-нибудь величины из этой таблицы, например, гистограмму длины крыла, сделать это можно с помощью функции displot():

sns.displot(x='flipper_length_mm',
            data=penguins);

График гистограммы, построенный в Seaborn с помощью displot

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

sns.displot(x='flipper_length_mm',
            hue='species',
            data=penguins);

График гистограммы, построенный в Seaborn с помощью displot с параметром hue

Однако, такой график не позволяет легко воспринимать информацию, т.к. гистограммы перекрывают друг друга. Попробовать исправить ситуацию можно несколькоми способами. Сначала можно попробовать отображать только ступеньки гистограмм:

График гистограммы, построенный в Seaborn с помощью displot с параметрами hue и element

Еще один способ, это воспользоваться параметром multiple. Если данному параметру передать значение stack, то перекрывающиеся участки пудут надставлены друг над другом:

sns.displot(x='flipper_length_mm',
            hue='species',
            multiple='stack',
            data=penguins);

График гистограммы, построенный в Seaborn с помощью displot с параметрами hue и multiple в значении stack

Но, как мне кажется, так тоже "не очень", получается гораздо лучше, если параметру multiple передать значение dodge:

sns.displot(x='flipper_length_mm',
            hue='species',
            multiple='dodge',
            data=penguins);

График гистограммы, построенный в Seaborn с помощью displot с параметрами hue и multiple в значении dodge

Еще один способ изображения перекрывающихся гистограмм - это визуализация их процентного соотношения в каждой ячейке:

sns.displot(y='flipper_length_mm',
            hue='species',
            multiple='fill',
            data=penguins);

График гистограммы, построенный в Seaborn с помощью displot с параметрами hue и multiple в значении fill

Обратите внимание, что для горизонтального расположения графика мы просто сменили параметр x на y.

Самым простым выходом для визуализации гистограмм нескольких подмножеств, является использование многосегментных графиков, которые на самом деле и призвана строить функция displot(). Для этого нужно передать столбец с категориальными данными не параметру hue, а параметру col (или row):

sns.displot(x='flipper_length_mm',
            col='species',
            data=penguins);

График гистограммы, построенный в Seaborn с помощью displot в нескольких колонках

Так же очень часто возникает потребность в изменении ширины ячеек или увеличения их числа:

sns.displot(x='flipper_length_mm',
            binwidth=1,    # установка ширины ячейки
            col='species',
            data=penguins);

График гистограммы, построенный в Seaborn с помощью displot с установленной шириной ячеек

sns.displot(x='flipper_length_mm',
            bins=30,     # установка количества ячеек
            col='species',
            data=penguins);

График гистограммы, построенный в Seaborn с помощью displot с установленным количеством ячеек


Оценка плотности ядра

Еще один способ оценки распределения заключается в оценке плотности ядра (kernel density estimation или сокращенно kde). Визуально, kde рисуется в виде линии и часто повторяет форму гистограмм, но дает больше информации о характере распределения:

sns.displot(x='flipper_length_mm',
            kde=True,
            data=penguins);

График гистограммы с оценкой плотности ядра, построенный в Seaborn.

Чтобы продемонстрировать суть данного метода, давайте загрузим данные о размере чаевых:

tips = sns.load_dataset("tips")
tips.head()
subject timepoint event region signal
0 s13 18 stim parietal -0.017552
1 s5 14 stim parietal -0.080883
2 s12 18 stim parietal -0.081033
3 s11 18 stim parietal -0.046134
4 s10 18 stim parietal -0.037970

Теперь взглянем на плотность ядра распределения общей стоимости заказа:

# Задаем размер рисунка:
f, ax = plt.subplots(figsize=(14, 7))

# рисуем гистограмму:
sns.histplot(x='total_bill',
             stat="density",
             bins=20,
             fill=False,
             data=tips);

# отмечаем каждую точку наблюдений:
sns.rugplot(x='total_bill',
            data=tips);


# и, наконец, плотность ядра:
sns.kdeplot(x='total_bill',
            data=tips);

График гистограммы с оценкой плотности ядра и точками наблюдений, построенный в Seaborn.

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

f, ax = plt.subplots(figsize=(20, 7))

x = tips['total_bill']

# в этом списке будем сохранять
# каждую кривую:
kernels = []

# теперь рисуем для каждого наблюдения
# свое нормальное распределение
# с центром в значении этого наблюдения:
for x_i in x:
    x_axis = np.linspace(0, 55, 400)
    kernel = stats.norm(x_i, 1).pdf(x_axis)
    kernels.append(kernel)
    plt.plot(x_axis, kernel, color='r', alpha=0.4)

sns.rugplot(x, color='.2', linewidth=1);

График, объясняющий как выполняется первый этап построения графика ядра плотности распределения.

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

# импортируем функцию для вычисления
# площади под кривой методом трапеций:
from scipy.integrate import trapz

# суммируем соответствующие значения
# y для каждой кривой:
S = np.sum(kernels, axis=0)

# нормализуем площадь:
S /= trapz(S, x_axis)

f, ax = plt.subplots(figsize=(20, 7))

# и наконец-то получаем kde:
plt.plot(x_axis, S, c='r');

sns.rugplot(x, color='.2', linewidth=1);

График, объясняющий как выполняется второй этап построения графика ядра плотности распределения.

Вы могли заметить, что полученный график несколько отличается своей "волнистостью" от графика, который был получен с помощью функции kdeplot(). Все дело в используемой пропускной способности, которая влияет на точность оценки наблюдений и аналогична ширине ячеек в гистограммах. Увидеть, как пропускная способность влияет на вид графика можно с помощью параметра bw_adjust:

f, ax = plt.subplots(figsize=(14, 7))

sns.kdeplot(x='total_bill',
            bw_adjust=0.2,
            label='bw: 0.2',
            data=tips);

sns.kdeplot(x='total_bill',
            bw_adjust=0.4,
            label='bw: 0.4',
            data=tips);

sns.kdeplot(x='total_bill',
            bw_adjust=0.8,
            label='bw: 0.8',
            data=tips);

plt.legend(fontsize = 15);

График с оценкой плотности ядра и установленной пропускной способностью, построенный в Seaborn.

Функция displot() по умолчанию рисует гистограммы, но она так же может рисовать и kde-графики, для этого нужно передать параметру kind значение 'kde'. Например, вот так можно взглянуть на оценку плотности длины крыла пингвинов разного вида:

sns.displot(x="flipper_length_mm",
            hue="species",
            kind="kde",
            data=penguins);

График с оценкой плотности ядра и параметром hue, построенный в Seaborn.

Но displot() позволяет строить многосегментные графики, что очень улучшает их информативность:

sns.displot(x="flipper_length_mm",
            col="species",
            hue='sex',
            kind="kde",
            bw_adjust=0.4,
            data=penguins);

График с оценкой плотности ядра и параметром hue в нескольких колонках, построенный в Seaborn.

При этом kde-графики, точно так же как и гистограммы, поддерживают некоторые настройки внешнего вида. Например, они могут быть закрашены:

sns.displot(x="flipper_length_mm",
            hue="species",
            kind="kde",
            fill=True,
            data=penguins);

График с оценкой плотности ядра и параметром fill, построенный в Seaborn.

А если нужно избежать перекрытия, то линии могут быть надставлены:

sns.displot(x="flipper_length_mm",
            hue="species",
            kind="kde",
            multiple="stack",
            data=penguins);

График с оценкой плотности ядра и параметром multiple в значении stack, построенный в Seaborn.

И если необходимо, то оценка плотности может изображаться в процентном соотношении наблюдений каждой категории:

sns.displot(x="flipper_length_mm",
            hue="species",
            kind="kde",
            multiple="fill",
            data=penguins);

График с оценкой плотности ядра и параметром multiple в значении fill, построенный в Seaborn.

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

f, ax = plt.subplots(figsize=(14, 4))

sns.rugplot(x='tip',
            data=tips,
            height=0.1);

sns.kdeplot(x='tip',
            data=tips);

График с оценкой плотности ядра без параметра cut, построенный в Seaborn.

Глядя на этот график мы видим как кривая плотности ядра выходит за пределы максимального и минимального значений размера чаевых. Что бы избежать этого, можно воспользоваться параметром cut:

f, ax = plt.subplots(figsize=(14, 4))

sns.rugplot(x='tip',
            data=tips,
            height=0.1);

sns.kdeplot(x='tip',
            cut=0,
            data=tips);

График с оценкой плотности ядра с параметром cut, построенный в Seaborn.

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

diamonds = sns.load_dataset("diamonds")
diamonds.head()
carat cut color clarity depth table price x y z
0 0.23 Ideal E SI2 61.5 55.0 326 3.95 3.98 2.43
1 0.21 Premium E SI1 59.8 61.0 326 3.89 3.84 2.31
2 0.23 Good E VS1 56.9 65.0 327 4.05 4.07 2.31
3 0.29 Premium I VS2 62.4 58.0 334 4.20 4.23 2.63
4 0.31 Good J SI2 63.3 58.0 335 4.34 4.35 2.75

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

f, ax = plt.subplots(figsize=(14, 4))

sns.kdeplot(x='carat',
            cut=0,
            data=diamonds);

График с оценкой плотности ядра для очень неравномерных данных, построенный в Seaborn.

Сравните этот график с гистограммой, которая свидетельствует о том, что очень много камней имеют одинаковуюмассу:

sns.displot(x='carat',
            aspect=2.5,
            data=diamonds);

Гистограмма для, построенный в Seaborn.

Ориентируясь только на график плотности ядра такие нюансы могут быть упущены. Именно поэтому лучше выполнять совместное построение гистограмм и kde-графиков, это можно быстро сделать если в функции displot() установить параметр kde в значение True:

sns.set_style('darkgrid')

sns.displot(x='carat',
            kde=True,
            aspect=2.5,
            data=diamonds);

Гистограмма и kde график, построенный в Seaborn.

Обратите внимание на добавленную уоманду set_style('darkgrid'), которая позволяет задать визуальный стиль графика и улучшить его восприятие. До этого, мы пользовались только "дефолтными" настройками Seaborn, которых, как правило достаточно, но добавление стилей, так же добавляет и эстетики к графикам которые вы строите.


Визуализация функции распределения

Функция распределения в теории вероятности показывает вероятность того, что некотороя величина окажется меньше заданного значения. Для стандартного нормального распределения (\(\mu = 0, \sigma = 1\)) данная функция будет выглядеть следующим образом:

sns.displot(x=np.random.randn(500),
            kind="ecdf");

График ecdf для нормального распределения, показывающий функцию распределения и построенный в Seaborn.

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

sns.displot(x='tip',
            kind="ecdf",
            data=tips);

График ecdf для эмпирических данных, показывающий функцию распределения и построенный в Seaborn.

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

sns.displot(x='tip',
            hue='day',
            kind="ecdf",
            data=tips);

График ecdf для эмпирических данных, показывающий функцию распределения для нескольких категорий с помощью параметра hue и построенный в Seaborn.

На основе данного графика можно сказать, что вероятность получить наибольшие чаевые выше всего в субботу. Или, например, вероятность того, что мужчины и женщины датуд не более 5 долларов чаевых, одинакова для тех и других, и, равна 0.9:

sns.displot(x='tip',
            hue='sex',
            kind="ecdf",
            data=tips);

График ecdf для эмпирических данных, показывающий функцию распределения для двух категорий с помощью параметра hue и построенный в Seaborn.

Как видите, на основе данного графика очень трудно что-то сказать о форме и характере распределения, но зато он позволяет легко оценивать вероятность того или инного события. А если более пристально присмотреться к участкам с разным наклоном, то можно сделать и другие выводы о характере распределения. Ранее мы видели бимодальность в распределении длины крыльев пингвинов:

fig = plt.figure()

ax_1 = fig.add_subplot(1, 2, 1)
ax_2 = fig.add_subplot(1, 2, 2)

sns.kdeplot(x="flipper_length_mm",
            ax=ax_1,
            data=penguins);
sns.ecdfplot(x="flipper_length_mm",
             ax=ax_2,
            data=penguins);

ax_1.set_title('kdeplot',
               fontsize=15)
ax_2.set_title('ecdfplot',
               fontsize=15)

fig.set_figwidth(14)
fig.set_figheight(7)

plt.show()

Графики kde и ecdf для бимодального распределения, построенные в Seaborn.

Возможно, при первом взгляде все может показаться не столь очевидным, но обратите внимание, что ecdf-график так же отражает бимодальность распределения, так как наибольший рост функции распределения соответствует пикам на kde-графике.


Визуализация двумерных распределений

Очень часто необходимо взглянуть на характер совместного распределения нескольких величин, это проще всего сделать, построив двумерную гистограмму. Для этого достаточно передать параметру y имя необходимого столбца:

sns.displot(x="bill_length_mm",
            y="bill_depth_mm",
            data=penguins);

Двумерная гистограмма, построенная в Seaborn.

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

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

sns.displot(x="bill_length_mm",
            y="bill_depth_mm",
            bins=30,
            data=penguins);

Двумерная гистограмма с увеличенным количеством ячеек с помощью параметра bins, построенная в Seaborn.

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

sns.displot(x="bill_length_mm",
            y="bill_depth_mm",
            kind='kde',
            data=penguins);

Двумерная оценка плотности вероятности, построенная в Seaborn.

Благодаря параметру hue можно еще сильнее выделить цветом различные подмножества в данных:

fig = plt.figure()

ax_1 = fig.add_subplot(1, 2, 1)
ax_2 = fig.add_subplot(1, 2, 2)

sns.kdeplot(x="bill_length_mm",
            y="bill_depth_mm",
            hue='species',
            ax=ax_1,
            data=penguins);

sns.histplot(x="bill_length_mm",
             y="bill_depth_mm",
             hue='species',
             bins=30,
             ax=ax_2,
             data=penguins);

ax_1.set_title('kdeplot',
               fontsize=15)
ax_2.set_title('histplot',
               fontsize=15)

fig.set_figwidth(14)
fig.set_figheight(7)

plt.show()

Двумерная гистограмма и kde с параметром hue, построенная в Seaborn.


Одновременная визуализация 1-d и 2-d распределений

Еще больше информации об одельном и совместном распределении двух величин можно получить если воспользоваться функцией jointplot():

sns.jointplot(x="bill_length_mm",
              y="bill_depth_mm",
              data=penguins);

Двумерное распределение двух величин, построенное с помощью функции jointplot в Seaborn.

По умолчанию, двумерное распределение отображается точками (графиком разброса), а одномерное распределение каждой величины в отдельности с помощью гистограммы. С помощью параметра kind можно указать тип графика для каждого из распределений, а параметр hue позволяет выделить цветом разные категории:

sns.jointplot(x="bill_length_mm",
              y="bill_depth_mm",
              hue='species',
              kind='kde',
              data=penguins);

Двумерная оценка плотности ядра (kde) двух величин, построенная с помощью функции jointplot в Seaborn.

Поскольку данный вид графиков построен на основе класса JointGrid, то мы можем воспользоваться более тонкими настройками, например строить разные типы графиков:

# создаем экземпляр класса:
g = sns.JointGrid(x="bill_length_mm",
                  y="bill_depth_mm",
                  data=penguins)

# указываем как рисовать 2-d распределение:
g.plot_joint(sns.kdeplot)

# указываем как рисовать 1-d распределения:
g.plot_marginals(sns.violinplot);

Двумерная оценка плотности ядра (kde) двух величин и графики violinplot, построенные с помощью функции jointplot и класса JointGrid в Seaborn.


Визуализация многомерных распределений

Визуализация парных отношений является самой наглядной и что бы нарисовать совместное распределение всех переменных в наборе данных достаточно воспользоваться функцией pairplot():

sns.pairplot(penguins);

Многомерное распределение всех величин в наборе данных с помощью функции pairplot() в Seaborn.

По умолчанию, парные распределения (те что под и над главной диагональю сетки) изображаются графиками разброса, а одномерные распределения (те что на главной диагонали) изображаются гистограммами. Функция pairplot() использует класс PairGrid, кторый строит сетку для решетчатого графика, а это значит, что создав экземпляр данного класса, мы можем выполнять более тонкие настройки:

# Создаем экземпляр класса:
g = sns.PairGrid(penguins)

# задаем тип графиков над
# главной диагональю:
g.map_upper(sns.histplot, bins=30)

# задаем тип графиков под
# главной диагональю:
g.map_lower(sns.kdeplot, bw_adjust=0.7)

# задаем тип графиков на
# главной диагонали:
g.map_diag(sns.histplot, kde=True);

Многомерное распределение всех величин в наборе данных с помощью функции pairplot() с настройками PairGrid в Seaborn.