Визуализация статистических зависимостей

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

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

График разброса

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

tips = sns.load_dataset('tips')
tips.head(10)
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

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

  • total_bill - общий счет, т.е. стоимость заказанной еды в долларах США (с учетом налогов);
  • tip - размер чаевых в долларах США;
  • sex - пол человека, который оплачивает счет;
  • smoker - наличие курильщика среди участников одного заказа;
  • day - день недели;
  • time - время суток (lunch - первая половина дня, dinner - вторая половина дня);
  • size - количество людей в группе, выполнившей один заказ.

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

sns.relplot(x='total_bill', y='tip', data=tips)
<seaborn.axisgrid.FacetGrid at 0x218a4853548>

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

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

В качестве параметров функции relplot() мы указали всего три аргумента:

  • data - это имя таблицы (объекта DataFrame). Важно что бы таблица имела 'длинную' форму, т.е. что бы каждый столбец соответствовал одной переменной, а каждая строка - одному наблюдению.
  • x, y - имена столбцов в таблице.

Наверняка, вы обратили внимание на строчку <seaborn.axisgrid.FacetGrid at 0x218a4853548>, так Seaborn выводит служебную информацию о рисунке, от которой нет никакой практической пользы. Что бы подавить вывод строк в ячейках блокнота Jupyter используется оператор ;. Например, если вы вополните вот такой код в одной из ячеек:

val = 2**20
val

То увидите значение переменной val:

1048576

Но если выполните такой код:

val = 2**20
val;     #  точка с запятой подавляет вывод

То никакого вывода значения переменной вы не увидите. Именно по этой причине в документации по Seaborn или Matplotlib можно очень часто встретить символ ;. Использование точки с запятой в коде Python считается крайне не рекомендуемым, но при работе в Jupyter оно является более чем допустимым, а иногда даже необходимым, например, если вы конвертируете блокнот в html для его дальнейшего распространения среди людей, которых странные строки над графиками могут сбить с толку. Поэтому давайте и мы заведем привычку ставить ; когда это необходимо:

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

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

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

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

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

Можно заметить, что во второй половине дня заказов несколько меньше, однако, теперь отчетливей видно, что очень много чаевых имеют фиксированный размер в 2, 3, 4 и 5 долларов. Кстати в таблице есть еще один столбец с категориальными данными - smoker:

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

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

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

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

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

Вообще, на графике можно отразить поведение четырех переменных:

sns.relplot(x='total_bill', y='tip', 
            hue='smoker', 
            style='time', 
            data=tips);

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

Казалось бы, что это довольно круто, ведь на графике отражены данные сразу из четырех столбцов: total_bill, tip, smoker, time. Но так лучше не делать, потому что это усложняет восприятие. Это связано с тем что восприятие цвета, гораздо сильнее чем восприятие формы и какие-то важные зависимости могут остаться без должного внимания.

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

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

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

Обратите внимание на легенду графика. У нас, что на самом деле были заказы которые совершались группами размером ноль человек? Просто по умолчанию, легенда строится с автоматически выделенным шагом оттенка, что бы не загромождать область рисунка. Но если нам нужно отразить ее полностью, то достаточно воспользоваться параметром legend и присвоить ему значение full:

sns.relplot(x='total_bill',
            y='tip', 
            hue='size',
            legend='full',
            data=tips);

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

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

С помощью параметра palette можно изменить цветовую палитру:

sns.relplot(x='total_bill',
            y='tip', 
            hue='size',
            legend='full',
            palette='ch: s=0.1, r=0.9',
            data=tips);

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

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

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

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

Мы можем одновременно установить как палитру, так и произвольный размер маркеров с помощью параметра sizes:

sns.relplot(x='total_bill', 
            y='tip', 
            hue='size',
            size='size', 
            sizes=(40, 150),
            legend='full',
            data=tips);

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


График линии

График разброса позволяет обнаруживать некоторые закономерности в совместном распределении переменных, но он не очень удобен для визуализации функциональной зависимости. Например, изменение средней температуры воздуха на планете от суммарного объема выбросов углекислого газа гораздо удобнее отображать в виде линии, нежели точек. Что бы нарисовать линейный график с помощью функции relplot() достаточно установить параметр kind в значение 'line'.

Для примера давайте сначала смоделируем простые данные:

t = np.arange(200)
v = t**0.5 + np.random.randn(200)
df = pd.DataFrame(dict(Time=t, Value=v))

А теперь визуализируем их:

sns.relplot(x='Time', 
                y='Value',
                kind='line',
                data=df);

Простейший график линии, построенный с помощью функции relplot в Seaborn

По умолчанию, все значения переменной, присвоенной переменной x сортируются. Допустим если мы перемешаем все пары значений x и y, а затем снова их нарисуем, то мы не заметим никакой разницы:

perm = np.random.permutation(200)
t = t[perm]
v = v[perm]

df = pd.DataFrame(dict(Time=t, Value=v))

sns.relplot(x='Time', 
                y='Value',
                kind='line',
                data=df);

График линии, с параметром sort=True relplot в Seaborn

Такое поведение связано с тем, что в статистике, как правило, одному значению может соответствовать несколько наблюдейний (например рост 10 людей одинакового возраста). Если сортировка ненужна, то достаточно установить параметр sort в значение False:

sns.relplot(x='Time', 
                y='Value',
                sort=False,
                kind='line',
                data=df);

График линии, с параметром sort=False relplot в Seaborn

Но Seaborn может легко визуализировать и более сложные наборы данных. Давайте продемонстрируем это на данных для fMRI:

fmri = sns.load_dataset('fmri')
fmri.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

Если изобразить эти данные с помощью графика разброса то мы увидим следующее:

sns.relplot(x='timepoint',
            y='signal',
            kind='scatter',
            data=fmri);

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

Заметьте, что одним и тем же значениям из столбца timepoint соответствует несколько разных значений из столбца signal. В этом легко убедиться, для чего посмотрим на все значения сигнала при timepoint = 0:

sns.scatterplot(x='timepoint',
                y='signal',
                data=fmri[fmri['timepoint'] == 0]);

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

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

sns.histplot(x='signal',
             kde=True,
             data=fmri[fmri['timepoint'] == 0]);

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

Обычно, в таких ситуациях говорят о погрешности измерений, т.е. о том, что величина ошибки имеет какоето статистическое распределение и что истинное значение можно оценить с помощью доверительного интервала. По умолчанию, в таких случаях Seaborn выполняет сортировку данных (агрегирование данных по точкам на оси x), а затем строит среднее значение для каждой группы измерений и его 95%-й доверительный интервал:

sns.relplot(x='timepoint',
            y='signal',
            kind='line',
            data=fmri);

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

Доверительный интервал очень удобен, так как показывает ту область внутри которой находится истинное значение измеренной величины с определенной вероятностью. Например на рисунке выше построен 95%-й интервал, а это означает, что вероятность попадания в него истинного значения величины равна 0.95. Однако, вычисление интервалов выполняется методом bootstrap, в основе которого лежит метод Монте-Карло, так что если вы исследуете очень большой набор данных, то лучше отключить данную функцию, приравняв параметр ci значению None:

sns.relplot(x='timepoint',
            y='signal',
            ci=None,
            kind='line',
            data=fmri);

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

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

sns.relplot(x='timepoint',
            y='signal',
            n_boot=30,
            kind='line',
            data=fmri);

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

Еще вместо доверительного интервала можно строить стандартное отклонение для каждой группы измерений, что очень полезно для оценки разброса значений, для этого нужно установить параметр ci в значение 'sd':

sns.relplot(x='timepoint',
            y='signal',
            kind='line',
            ci='sd',
            data=fmri);

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


Визуальная семантика линий

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

sns.relplot(x='timepoint',
            y='signal',
            hue='region',
            kind='line',
            data=fmri);

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

Точно так же можно выделить цветом данные измерений для разных событий (значений в столбце event):

sns.relplot(x='timepoint',
            y='signal',
            hue='event',
            kind='line',
            data=fmri);

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

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

sns.relplot(x='timepoint',
            y='signal',
            hue='subject',
            kind='line',
            ci=None,
            data=fmri);

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

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

fmri['num_subj'] = [int(s[1:]) for s in fmri['subject']]

sns.relplot(x='timepoint',
            y='signal',
            hue='num_subj',
            kind='line',
            ci=None,
            data=fmri);

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

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

sns.relplot(x='timepoint',
            y='signal',
            hue='event',
            style='region',
            ci=None,
            kind='line',
            data=fmri);

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

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

sns.relplot(x='timepoint',
            y='signal',
            hue='event',
            style='region',
            dashes=True,
            markers=True,
            ci='sd',
            kind='line',
            data=fmri);

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

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

sns.relplot(x='timepoint',
            y='signal',
            hue='region',
            size='event',
            ci=None,
            kind='line',
            data=fmri);

График линии с параметром size, построенный в Seaborn

Параметр size может принимать и числовые данные, но это не всегда облегчает восприятие:

sns.relplot(x='timepoint',
            y='signal',
            hue='num_subj',
            size='num_subj',
            ci=None,
            kind='line',
            data=fmri);

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

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


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

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

t = pd.date_range(start='2019-01-01', end='2019-01-25')
v = 2*np.arange(25) + np.random.randint(0, 15, 25)

df = pd.DataFrame(dict(Time=t, Value=v))

sns.relplot(x='Time', y='Value', kind='line', data=df);

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

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

t = pd.date_range(start='2019-01-01', end='2019-01-25')
v = 2*np.arange(25) + np.random.randint(0, 15, 25)

df = pd.DataFrame(dict(Time=t, Value=v))

g = sns.relplot(x='Time', y='Value', kind='line', data=df)
g.fig.autofmt_xdate()

График линии с настроенным отображением даты и времени по оси x, построенный в Seaborn


relplot() для нескольких графиков

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

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

График разброса в relplot  снесколькими колонками

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

sns.relplot(x='total_bill',
            y='tip',
            hue='time',
            row='smoker',
            data=tips);

График разброса в relplot  снесколькими строками

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

sns.relplot(x='total_bill',
            y='tip',
            col='smoker',
            row='time',
            height=4,
            data=tips);

График разброса в relplot снесколькими строками и столбцами

Что бы график не получился очень большим, мы немного уменьшили его с помощью параметра height.

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

cat_size = [str(s) for s in tips['size']]
tips['cat_size'] = cat_size

sns.relplot(x='total_bill',
            y='tip',
            hue='time',
            col='cat_size',
            height=4,
            data=tips);

График разброса в relplot снесколькими колонками для числовых данных.

Что бы график стал более презентабельным, можно с помощью параметра col_wrap задать количество подграфиков в одной строке, а соотношение сторон подграфиков определить в параметре aspect:

sns.relplot(x='total_bill',
            y='tip',
            hue='time',
            col='cat_size',
            height=4,
            col_wrap=3,
            aspect=1.5, 
            data=tips);

График разброса в relplot с параметрами col_wrap и aspect.

Такие "решетчатые" графики гораздо полезнее, одного сложного, так как позволяют гораздо легче выявлять закономерности (или выявлять отклонения от закономерностей). Именно в этом заключается практическая польза функции relplot().