Визуализация категориальных данных

Прежде всего сделаем все необходимые импорты:

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

Разброс внутри категорий

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

tips = sns.load_dataset("tips")
tips.head()
total_bill tip sex smoker day time size
0 16.99 1.01 Female No Sun Dinner 2
1 10.34 1.66 Male No Sun Dinner 3
2 21.01 3.50 Male No Sun Dinner 3
3 23.68 3.31 Male No Sun Dinner 2
4 24.59 3.61 Female No Sun Dinner 4

Это данные о размере чаевых в ресторане, подробное описание которых можно посмотреть здесь.

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

sns.relplot(x='total_bill', y='tip', data=tips);

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

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

sns.relplot(x='total_bill',
            y='tip',
            col='sex',
            data=tips);

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

Но что если хочется посмотреть только на зависимость размера чаевых от пола человека, оплатившего заказ? Без информации о полной стоимости совершенного заказа? Вот это и является второй задачей - визуализацией разброса измеренных значений внутри каждой категории:

sns.catplot(x='sex',
            y='tip',
            data=tips);

График разброса внутри категорий, построенный в Seaborn

В принципе, мы могли бы построить точно такой же график и с помощью функции scatterplot() (или relplot() которая строит график разброса по умолчанию):

sns.scatterplot(x='sex',
                y='tip',
                data=tips);

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

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

sns.catplot(x='day',
            y='tip',
            data=tips);

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

При этом функция catplot() так же позволяет выделять дополнительные подмножества, скажем, можно посмотреть, как на размер чаевых влияет не только пол человека, оплатившего заказ, но еще и время дня:

sns.catplot(x='sex',
            y='tip',
            hue='time',
            data=tips);

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

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

sns.catplot(x='size',
            y='tip',
            hue='time',
            data=tips);

График разброса внутри внутри нескольких числовых категорий с параметром hue, построенный в Seaborn с помощью catplot


Распределение внутри категорий

График разброса по категориям удобен для небольших наборов данных, так как по мере увеличения количества точек они все равно начнут перекрывать друг друга и сливаться. Что бы преодолеть эти трудности, лучше воспользоваться графиками, которые сами содержат некоторую информацию о распределении внутри категорий. Один из таких графиков - это "ящик с усами" или boxplot. Его можно построить с помощью той же функции catplot с параметром kind, установленным в значение 'box'.

Прежде чем демонстрировать, как устроен boxplot в Seaborn, давайте сначала разберемся с тем как устроен этот "ящик с усами" (он же диаграмма размаха). Наверное проще всего начать с данных, которые подчиняются нормальному распределению:

График boxplot и его описание

Границы ящика (прямоугольника) располагаются между нижним и верхним квартилем, или, как еще говорят, они располагаются между первым и третьим квартилем. В общем, они располагаются между \(Q_{1}\) и \(Q_{3}\). \(Q_{1}\) - это, по сути, 0.25 квантиль, т.е. это такое значение в данных, ниже которого располагаются 25% всех остальных значений. \(Q_{3}\) - это 0.75 квантиль, как вы поняли, ниже этого значения находится 75% всех остальных значений массива данных. Еще внутри ящика распологается полоска, которая соответствует медиане. Медианой, в свою очередь, так же является второй квартиль \(Q_{2}\), он же 0.5 квантиль - значение, ниже которого расположена ровно половина всех остальных значений массива данных.

Длина ящика \(IQR\) равна межквартильному интервалу, т.е. \(IQR = Q_{3} - Q_{1}\), причем внутри этого интервала располагается 50% всех значений массива данных, а точнее "центральная" часть этих данных. Так же значение \(IQR\) определяет длину "усов" ящика, которая, обычно, не превышает \(1.5 \cdot IQR\). Все значения, выходящие за границу усов, считаются выбросами, или, как еще говорят - аномальными значениями, которые обозначаются отдельными точками.

Теперь давайте немного поговорим об "усах" ящика. Во первых, длина каждого "уса" может быть разной и может быть меньше чем \(1.5 \cdot IQR\):

Усы графика boxplot и их описание

Обратите внимание, что левый "ус" короче чем правый, причем рядом с ним нет выбросов. Это означает, что все значения ниже \(Q_{1}\) уместились в его границы, а самая левая его граница теперь равна минимальному значению в наборе данных. В то же время, длина правого "уса" равна \(1.5 \cdot IQR\) и утверждать, что его правый край соответствует максимальному значению в наборе данных - нельзя, поскольку мы видим выбросы за его пределами.

Очень часто можно услышать, что границы "усов" как раз и соответствуют максимальному и минимальному значению, чисто технически, это верно только в отсутствии выбросов, например, как на нижеприведенном рисунке:

boxplot без выбросов

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

Ну а теперь давайте "порисуем" в Seaborn, напомню, что параметр kind=box, как раз и заставляет функцию catplot() рисовать ящики с усами:

sns.catplot(x='sex',
            y='tip',
            kind='box',
            data=tips);

График boxplot, нарисованный в seaborn

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

С помощью параметра hue мы можем выделить в данных некоторые подмножества:

sns.catplot(x='day',
            y='tip',
            hue='time',
            kind='box',
            data=tips);

График boxplot с параметром hue, нарисованный в seaborn

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

Графики boxplot и boxenplot в сравнении, нарисованные в seaborn

На вышеприведенном графике, boxplot и boxenplot построены для 1000 значений стандартного нормального распределения (\(\mu = 0\), \(\sigma = 1\)). Как видите, в boxenplot испоьзуется больше прямоугольников, причем границы каждого из них определяются квантилями, значения которых являются обратными для степеней двойки. Количество прямоугольников подбирается так, что бы значения внутри каждого из них считались достоверными с вероятностью 0.95% т.е. чем больше значений в выборке - тем больше прямоугольников. Цвет каждого прямоугольнока характеризует плотность значений, чем ярче - тем плотнее.

Как видите, boxenplot позволяет получить больше информации о распределении значений внутри категорий. Чем больше значений - тем больше прямоугольников, а это значит, что можно оценивать "хвосты" распределений. Но даже для небольших наборов данных, этот график может оказаться несколько информативнее, чем boxplot:

sns.catplot(x='day',
            y='tip',
            kind='boxen',
            data=tips);

График boxenplot, нарисованный в seaborn

Еще один тип графика который позволяет судить о распределении значений - это violinplot, который сочетает принципы построения графиков boxplot и kde:

sns.catplot(x='sex',
            y='tip',
            kind='violin',
            data=tips);

График violinplot, нарисованный в seaborn

Широкая и тонкая черная полоса внутри "виолончели" соответствует "ящику" и усам" boxplot-а, а белая точка внутри - это медиана. Если данные состоят только из двух подмножеств, то violinplot можно разделить пополам для каждой из них с помощью параметра split:

sns.catplot(x='time',
            y='tip',
            hue='sex',
            kind='violin',
            split=True,
            data=tips);

График violinplot с параметром split, нарисованный в seaborn

Однако, для небольшого количества наблюдений внутри каждой категории violinplot теряет информативность:

sns.catplot(x='day',
            y='tip',
            hue='sex',
            kind='violin',
            cut=0,
            data=tips);

Это связано с оценкой плотности ядра. Немного исправить ситуацию помогает параметр bw:

sns.catplot(x='day',
            y='tip',
            hue='sex',
            kind='violin',
            bw=0.15,
            data=tips);

График violinplot с параметром bw, нарисованный в seaborn

Вместо ящика с усами внутри violiplot можно отображать фактические значения данных:

sns.catplot(x='time',
            y='tip',
            hue='sex',
            inner='stick',
            kind='violin',
            split=True,
            data=tips);

График violinplot с параметром stick, нарисованный в seaborn

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

fig = plt.figure()

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

sns.boxplot(x="sex", y="tip", palette='pastel',
            ax=ax_1, data=tips)
sns.stripplot(x="sex", y="tip", ax=ax_1,
              data=tips, color=".25")

sns.violinplot(x="sex", y="tip", palette='pastel',
               ax=ax_2, data=tips)
sns.swarmplot(x="sex", y="tip", size=2, ax=ax_2,
              data=tips, color=".25")

ax_1.set_title('boxplot and stripplot',
               fontsize=15)
ax_2.set_title('violinplot and swarmplot',
               fontsize=15)

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

plt.show()

Комбинирование нескольких графиков распределения внутри категорий в seaborn


Столбчатые диаграммы

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

sns.catplot(x='day',
            y='tip',
            kind='bar',
            data=tips);

График barplot, построенный в seaborn

С помощью параметра hue можно выделять подмножества в категориях:

sns.catplot(x='day',
            y='tip',
            hue='time',
            kind='bar',
            data=tips);

График barplot с параметром hue, построенный в seaborn

Что бы отразить количество наблюдений внутри каждой категории можно воспользоваться функцией countlot(), например, вот так можно взглянуть на количество обслуженных столиков по каждому дню:

sns.catplot(x='day',
            kind='count',
            data=tips);

График countlot, построенный в seaborn

При этом мы так же можем выделять подмножества внутри каждой категории:

sns.catplot(x='sex',
            hue='time',
            kind='count',
            data=tips);

График countlot с параметром hue, построенный в seaborn


Точечные оценки

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

fig = plt.figure()

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

sns.barplot(x='day', y='tip',
            ax=ax_1, data=tips)

sns.pointplot(x='day', y='tip',
            ax=ax_2, data=tips)

ax_1.set_title('barplot',
               fontsize=15)
ax_2.set_title('pointplot',
               fontsize=15)

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

plt.show()

Сравнение графиков barplot и pointplot, построенных в seaborn

Но применение pointplot() не всегда удобно:

sns.catplot(x='day',
            y='total_bill',
            hue='sex',
            kind='point',
            data=tips);

График pointplot с параметром hue, построенный в seaborn

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

sns.catplot(x='sex',
            y='tip',
            hue='time',
            dodge=True,
            kind='point',
            data=tips);

График pointplot с параметром dodge, построенный в seaborn


Отображение всех и нескольких отношений

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

iris = sns.load_dataset('iris')
iris.head()
sepal_length sepal_width petal_length petal_width species
0 5.1 3.5 1.4 0.2 setosa
1 4.9 3.0 1.4 0.2 setosa
2 4.7 3.2 1.3 0.2 setosa
3 4.6 3.1 1.5 0.2 setosa
4 5.0 3.6 1.4 0.2 setosa
sns.catplot(data=iris,
            orient='h',
            kind='violin');

catplot для всех числовых наблюдений из объекта dataframe, построенный в seaborn

Однако, catplot() так же как и relplot() позволяет строить решетчатые графики, в которых каждый столбец или строка могут отражать взаимное влияние разных факторов на характер распределения. Например, вот так может быть визуализирован размер чаевых в зависимости от дня и пола:

sns.catplot(x='day',
            y='tip',
            col='sex',
            aspect=1.2,
            kind='boxen',
            data=tips);

catplot с несколькими столбцами, построенный в seaborn

А вот так мы можем добавить еще и время дня:

sns.catplot(x='day',
            y='tip',
            col='sex',
            row='time',
            height = 3,
            aspect=1.7,
            kind='boxen',
            data=tips);

catplot с несколькими столбцами и строками, построенный в seaborn