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

Матричные модели - это общее название моделей для представления которых используются матрицы или проще говоря таблицы. В Seaborn для данных моделей существуют два вида графиков: heatmap() - для создания тепловых карт и clustermap() - для создания кластерных карт. Обе функции создают графики на уровне Axes и в определенных ситуациях могут оказаться крайне полезны.

Перед тем как приводить примеры, давайте как обычно выполним все необходимые импорты:

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

Тепловые карты

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

data = np.random.randint(0, 30, size=(10, 10))
sns.heatmap(data);

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

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

fig = plt.figure()

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

data_1 = np.random.randint(0, 30, size=(25, 5))
data_2 = np.random.randint(0, 30, size=(5, 25))

sns.heatmap(data_1, ax=ax_1);
sns.heatmap(data_2, ax=ax_2);

ax_1.set_title('"длинные" данные',
               fontsize=15)
ax_2.set_title('"широкие" дфнные',
               fontsize=15)


fig.set_figwidth(14)
fig.set_figheight(6)

plt.show()

График тепловой карты для длинных и широких данных, построенный в Seaborn с помощью heatmap

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

import matplotlib.gridspec as gridspec

fig = plt.figure(figsize=(14, 7))
gs = gridspec.GridSpec(nrows=2, ncols=10)

ax_1 = fig.add_subplot(gs[0, 0:8])
ax_2 = fig.add_subplot(gs[1, :])

data = np.random.randint(0, 30, 10)

ax_1.bar(range(len(data)), data)
ax_1.set_xlim(-0.5, 9.5)
sns.heatmap(data.reshape(1, 10), annot=True, ax=ax_2);

ax_1.set_title('Столбчатая диаграмма',
               fontsize=15)
ax_2.set_title('Тепловая карта',
               fontsize=15)


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

plt.show()

Сравнение столбчатой диаграммы и тепловой карты

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

flights = sns.load_dataset("flights")
flights.head()
year month passengers
0 1949 Jan 112
1 1949 Feb 118
2 1949 Mar 132
3 1949 Apr 129
4 1949 May 121

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

flights = flights.pivot("month", "year", "passengers")
flights
year 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960
month
Jan 112 115 145 171 196 204 242 284 315 340 360 417
Feb 118 126 150 180 196 188 233 277 301 318 342 391
Mar 132 141 178 193 236 235 267 317 356 362 406 419
Apr 129 135 163 181 235 227 269 313 348 348 396 461
May 121 125 172 183 229 234 270 318 355 363 420 472
Jun 135 149 178 218 243 264 315 374 422 435 472 535
Jul 148 170 199 230 264 302 364 413 465 491 548 622
Aug 148 170 199 242 272 293 347 405 467 505 559 606
Sep 136 158 184 209 237 259 312 355 404 404 463 508
Oct 119 133 162 191 211 229 274 306 347 359 407 461
Nov 104 114 146 172 180 203 237 271 305 310 362 390
Dec 118 140 166 194 201 229 278 306 336 337 405 432

А теперь построим тепловую карту для этой таблицы:

f, ax = plt.subplots(figsize=(11, 9))
sns.heatmap(data=flights,
            annot=True,
            fmt="d",
            cmap="Purples",
            linewidths=.1);

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

Как видите, теперь, визуальное восприятие тех же данных стало более простым.

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

t = np.linspace(0, 5, 200)* np.ones((7, 200)) + \
    np.random.randn(7, 200).cumsum(axis=1)
a = np.array([0.5, 1, 2.1, -1.3, -0.1, 1.9, -2.9]).reshape(-1, 1)
b = np.array([-3, -4, -10, 4, 1, -5, 6]).reshape(-1, 1)
Y = (a*t + b).T

df = pd.DataFrame(Y, columns=list('ABCDEFG'))

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

f, ax = plt.subplots(figsize=(11, 9))
sns.lineplot(data=df, sort=False);

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

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

pearson = df.corr()
sns.heatmap(data=pearson,
            annot=True);

Тепловая карта для корреляционной матрицы Пирсона, построенный в Seaborn

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

spearman = df.corr(method='spearman')
sns.heatmap(data=spearman,
            annot=True);

Тепловая карта для корреляционной матрицы Спирмена, построенный в Seaborn


Кластерная карта

Кластерные карты удобны... но с первого взгляда они вообще непонятны. Давайте начнем с чего-нибудь простого и пока просто разберемся что такое иерархическая кластеризация и дендрограммы.

Допустим у нас есть семь точек со следующими координатами:

dots = np.array([[2, 1],
                 [5, 1],
                 [9, 1],
                 [10, 1],
                 [21, 1],
                 [23, 1],
                 [27, 1]])

А вот как выглядят эти точки на графике:

sns.scatterplot(x=dots.T[0], y=dots.T[1], s=150);

Семь точек для визуализации дендрограммы.

Что бы объединить эти точки в кластеры, нам нужна какая-то мера. Самая проста мера - это расстояние в Евклидовом пространстве. Вычислить расстояние, попарно, между всеми точками мы можем следующим образом:

from scipy.spatial.distance import pdist
pdist(dots)
array([ 3.,  7.,  8., 19., 21., 25.,  4.,  5., 16., 18., 22.,  1., 12.,
       14., 18., 11., 13., 17.,  2.,  6.,  4.])

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

dist = np.zeros((7, 7))
triu_ind = np.triu_indices_from(dist, k=1)
dist[triu_ind] = pdist(dots)
dist
array([[ 0.,  3.,  7.,  8., 19., 21., 25.],
       [ 0.,  0.,  4.,  5., 16., 18., 22.],
       [ 0.,  0.,  0.,  1., 12., 14., 18.],
       [ 0.,  0.,  0.,  0., 11., 13., 17.],
       [ 0.,  0.,  0.,  0.,  0.,  2.,  6.],
       [ 0.,  0.,  0.,  0.,  0.,  0.,  4.],
       [ 0.,  0.,  0.,  0.,  0.,  0.,  0.]])

Далее мы можем на основе этой матрицы расстояний, объединять точки в кластеры на основе их близости:

from scipy.cluster.hierarchy import dendrogram, linkage

Z = linkage(dots, 'single')
Z
array([[ 2.,  3.,  1.,  2.],
       [ 4.,  5.,  2.,  2.],
       [ 0.,  1.,  3.,  2.],
       [ 7.,  9.,  4.,  4.],
       [ 6.,  8.,  4.,  3.],
       [10., 11., 11.,  7.]])

Каждая строка полученной матрицы имеет слудующий вид [ind_1, ind_2, dist, sample_count], т.е. на первой итерации в кластер были объединены всего две точки т.к. sample_count = 2 с координатами [9, 1] и [10, 1] и расстоянием между ними равным 1. И так до четвертой итерации в кластеры объединяются по две точки. Но на четвертой итерации происходит что-то очень странное, появляются индексы ind_1 = 7 и ind_2 = 9, которые выходят за пределы массива dots. Эти новые индексы соответствуют двум получившимся на предыдущей итерации кластерам: первый из кластеров состоит из точек {[2, 1], [5, 1]}, второй из {[9, 1], [10, 1]}. Так как в функции linkage() был указан метод 'single', то это соответствует вычислению расстоянию между ближайшими точками в двух кластерах, которое равно 9 - 5 = 4. Попробуйте сами проследить как происходит объединение и вы убедитесь, что на каждой итерации всегда выполняется слияние двух самых близких подмножеств.

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

fig, ax = plt.subplots()
dendrogram(Z, ax=ax)

plt.show()

Дендрограмма кластеров из семи точек.

При первом знакомстве все может показаться немного запутаным (уж не стреляйте в пианиста (автора) - он играет (объясняет), как может), но при определенной практике, одного взгляда на дендрограмму достаточно, что бы понять что к чему. А теперь давайте перейдем собственно к кластерным картам в Seaborn. Сначала сгенерируем какие-нибудь данные, пусть это будут два кластера:

from sklearn.datasets import make_blobs

X, y = make_blobs(n_samples=100,
                  centers=2,
                  cluster_std=0.60)

sns.scatterplot(x = X[:, 0], y=X[:, 1]);

Выглядеть кластеры будут вот так:

Два кластера.

А теперь "скормим" те же данные функции clustermap():

sns.clustermap(X);

График clustermap построенныей в Seaborn.

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

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

X, y = make_blobs(n_samples=1000,
                  centers=10,
                  cluster_std=0.60)

sns.scatterplot(x = X[:, 0], y=X[:, 1]);

График с изображением десяти кластеров.

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

sns.clustermap(X);

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

Как видите, дендрограмма соответствует действительности.

Наверняка, вам хочется спросить, а почему бы вообще не строить вместо кластерных карт те же самые графики разброса. Дело в количестве переменных, если их больше 3-х, то очевидного и простого способа построения графика разброса нет. А вот кластерная карта покажет близость подмножеств. Допустим у нас есть 10 кластеров, но уже не в двухмерном а 15-мерном пространстве признаков:

X, y = make_blobs(n_samples=1000,
                  centers=10,
                  n_features=15,
                  cluster_std=0.60)

sns.clustermap(X);

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

Вероятнее всего, выполняя этот пример, вы получили предупреждение:

UserWarning: Clustering large matrix with scipy.
Installing `fastcluster` may give better performance.

Это означает, что исходная матрица очень большая и ее обработка выполняется средствами SciPy и что установка библиотеки Fastcluster ускорит вычисления.

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