Ilya Ginsburg 11 февраля 2021

📊 Коллекция продвинутой визуализации в Matplotlib и Seaborn с примерами

Мы уже рассматривали графические библиотеки Python. Продолжая тему, разберем продвинутые методы визуализации с помощью Matplotlib и Seaborn.

Текст публикуется в переводе, автор оригинальной статьи – Rashida Nasrin Sucky.

***

В Python'е есть очень богатые графические библиотеки. Я уже писала о визуализации с помощью Pandas и Matplotlib. В основном это были основы, и мы слегка притронулись к некоторым продвинутым методам. Сейчас вы читаете еще одну обучающую статью по визуализации.

Я решила написать статью о продвинутых методах визуализации. В этой статье не будет базовых приемов визуализации – все примеры, приведенные в этой статье, будут продвинутыми. Если вам нужно освежить базовые приемы, пожалуйста, обратитесь к статье «Ваша повседневная шпаргалка по Matplotlib».

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

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

Давайте импортируем необходимые пакеты и набор данных:

        import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
import seaborn as sns
import warnings
warnings.filterwarnings(action="once")
df = pd.read_csv("nhanes_2015_2016.csv")
    

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

        df.columns
    

Вывод:

        Index(['SEQN', 'ALQ101', 'ALQ110', 'ALQ130', 'SMQ020', 'RIAGENDR', 'RIDAGEYR',
       'RIDRETH1', 'DMDCITZN', 'DMDEDUC2', 'DMDMARTL', 'DMDHHSIZ', 'WTINT2YR',
       'SDMVPSU', 'SDMVSTRA', 'INDFMPIR', 'BPXSY1', 'BPXDI1', 'BPXSY2',
       'BPXDI2', 'BMXWT', 'BMXHT', 'BMXBMI', 'BMXLEG', 'BMXARML', 'BMXARMC',
       'BMXWAIST', 'HIQ210'], dtype='object')
    

Вы наверняка думаете, что названия столбцов совершенно непонятные! Да, так и есть, но не волнуйтесь, я все объясню по мере использования данных, и вы все поймете.

В наборе данных есть несколько качественных (categorical) столбцов, которые мы будем широко использовать – такие, как пол (RIAGENDR), семейное положение (DMDMARTL) и уровень образования (DMDEDUC2). Я хочу преобразовать их значения в осмысленные вместо каких-то чисел.

        df["RIAGENDRx"] = df.RIAGENDR.replace({1: "Male", 2: "Female"})
df["DMDEDUC2x"] = df.DMDEDUC2.replace({1: "<9", 2: "9-11", 3: "HS/GED", 4: "Some college/AA", 5: "College", 7: "Refused", 9: "Don't know"})
df["DMDMARTLx"] = df.DMDMARTL.replace({1: "Married", 2: "Widowed", 3: "Divorced", 4: "Separated", 5: "Never married", 6: "Living w/partner", 77: "Refused"})
    

Диаграммы рассеяния

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

В этом демонстрационном примере я выведу дистолическое давление крови (BPXDI1) по отношению к систолическому (BPXSY1). В качестве небольшой модификации я буду выводить точки разными цветами в зависимости от семейного положения. Будет интересно посмотреть, оказывает ли семейное положение какое-нибудь влияние на давление крови.

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

        category = df["DMDMARTLx"].unique()
category
    

Вывод:

        array(['Married', 'Divorced', 'Living w/partner', 'Separated',
       'Never married', nan, 'Widowed', 'Refused'], dtype=object)
    

Теперь выберем цвет для каждой категории.

        colors = [plt.cm.tab10(i/float(len(category)-1)) for i in range(len(category))]
colors
    

Вывод:

        [(0.12156862745098039, 0.4666666666666667, 0.7058823529411765, 1.0),
 (1.0, 0.4980392156862745, 0.054901960784313725, 1.0),
 (0.17254901960784313, 0.6274509803921569, 0.17254901960784313, 1.0),
 (0.5803921568627451, 0.403921568627451, 0.7411764705882353, 1.0),
 (0.5490196078431373, 0.33725490196078434, 0.29411764705882354, 1.0),
 (0.4980392156862745, 0.4980392156862745, 0.4980392156862745, 1.0),
 (0.7372549019607844, 0.7411764705882353, 0.13333333333333333, 1.0),
 (0.09019607843137255, 0.7450980392156863, 0.8117647058823529, 1.0)]
    

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

        plt.figure(figsize=(16, 10), dpi=80, facecolor="w", edgecolor="k")
for i, cat in enumerate(category):
    plt.scatter("BPXDI1", "BPXSY1",
               data=df.loc[df.DMDMARTLx == cat, :],
                          s = 20, c=colors[i], label=str(cat))
    
plt.gca().set(xlabel='BPXDI1', ylabel='BPXSY1')
    
plt.xticks(fontsize=12)
plt.yticks(fontsize=12)
plt.title("Marital status vs Systolic blood pressure", fontsize=18)
plt.legend(fontsize=12)
plt.show()
    

В этот набор данных можно добавить еще одну переменную, значение которой будет управлять размером точек. Для этого я включу в набор данных индекс массы тела (BMXBMI). Я создам отдельный столбец под названием 'dot_size', в котором будет храниться индекс массы тела, умноженный на 10.

        df["dot_size"] = df.BMXBMI*10
    

Теперь сделаем нашу новую визуализацию:

        fig = plt.figure(figsize=(16, 10), dpi= 80, facecolor='w', edgecolor='k')    
for i, cat in enumerate(category):
    plt.scatter("BPXDI1", "BPXSY1", data=df.loc[df.DMDMARTLx == cat, :], s='dot_size', c=colors[i], label=str(cat), edgecolors='black')
plt.gca().set(xlabel='Diastolic Blood Pressure ', ylabel='Systolic blood Pressure')
plt.xticks(fontsize=12)
plt.yticks(fontsize=12)
plt.legend(fontsize=12)    
plt.show()
    

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

Один из путей к решению таких проблем – это взять случайную выборку из нашего набора данных. Поскольку наш набор данных слишком велик, если мы возьмем выборку из 500 элементов, визуализацию этого типа будет намного проще понять.

В следующей диаграмме я нарисую только первые 500 элементов из набора данных, предполагая, что весь набор организован случайным образом. Но я добавлю к этому набору еще один трюк. Я добавлю еще одну переменную – возраст, поскольку возраст может влиять на давление крови. Здесь я окружу те данные, для которых возраст больше 40. Вот этот код.

        from scipy.spatial import ConvexHull

df2 = df.loc[:500, :]
fig = plt.figure(figsize=(16, 10), dpi= 80, facecolor='w', edgecolor='k')
for i, cat in enumerate(category):
    plt.scatter("BPXDI1", "BPXSY1", data=df2.loc[df2.DMDMARTLx==cat, :], s='dot_size', c=colors[i], label=str(cat), edgecolors='black', alpha = 0.6, linewidths=.5)
    
def encircle(x,y, ax=None, **kw):
    if not ax: ax=plt.gca()
    p = np.c_[x,y]
    hull = ConvexHull(p)
    poly = plt.Polygon(p[hull.vertices,:], **kw)
    ax.add_patch(poly)
    
# Select data where age is more than 40
df_encircle = df2.loc[(df2["RIDAGEYR"] > 40), :].dropna()
# Drawing a polygon surrounding vertices    
encircle(df_encircle.BPXDI1, df_encircle.BPXSY1, ec="k", fc="gold", alpha=0.1)
encircle(df_encircle.BPXDI1, df_encircle.BPXSY1, ec="firebrick", fc="none", linewidth=1.5)

plt.gca().set(xlabel='BPXDI1', ylabel='BPXSY1')
plt.xticks(fontsize=12); plt.yticks(fontsize=12)
plt.title("Bubble Plot with Encircling", fontsize=22)
plt.legend(fontsize=12)    
plt.show()
    

Что мы можем узнать из этой диаграммы?

Кружки, очерченные многоугольником, соответствуют людям, которым за 40 лет, из нашей выборки в 500 человек.

Размер кружков соответствует индексу массы тела – чем больше кружок, тем выше индекс. Я не смогла найти никакой зависимости между давлением крови и индексом массы тела из этой диаграммы.

Цвета показывают различное семейное положение. Видите ли вы доминирование одного цвета в какой-то определенной области? Едва ли. Я тоже не вижу никаких зависимостей между семейным положением и давлением крови.

Точечные диаграммы (stripplot)

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

Для этой демонстрации я нарисую зависимость систолического давления крови от индекса массы тела.

        fig, ax = plt.subplots(figsize=(16, 8), dpi=80)
sns.stripplot(df2.BPXSY1, df2.BMXBMI, jitter=0.45, size=8, ax=ax, linewidth=0.5)
plt.title("Systolic Blood pressure vs Body mass index")
plt.tick_params(axis='x', which='major', labelsize=12, rotation=90)
plt.show()
    

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

        fig, ax = plt.subplots(figsize=(16,10), dpi= 80)    
sns.stripplot(df2.BPXDI1, df2.BPXSY1, s=10, hue = df2.RIDRETH1, ax=ax)
plt.title("Stripplot for Systolic vs Diastolic Blood Pressure", fontsize=20)
plt.tick_params(rotation=90)
plt.show()
    

Точечные диаграммы с «ящиками»

Точечные диаграммы можно нарисовать с «ящиками с усами». Если набор данных очень большой, и точек много, это дает намного больше информации. Проверьте сами:

        fig, ax = plt.subplots(figsize=(30, 12))
ax = sns.boxplot(x="BPXDI1", y = "BPXSY1", data=df)
ax.tick_params(rotation=90, labelsize=18)
ax = sns.stripplot(x = "BPXDI1", y = "BPXSY1", data=df)
    

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

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

Точечные диаграммы со скрипичными

Мы выведем зависимость семейного положения (DMDMARTLx) от возраста (RIDAGEYR). Сначала посмотрим, как она выглядит, а потом сможем поговорить о ней дальше.

        fig, ax = plt.subplots(figsize=(30, 12))
ax = sns.violinplot(x= "DMDMARTLx", y="RIDAGEYR", data=df, inner=None, color="0.4")
ax = sns.stripplot(x= "DMDMARTLx", y="RIDAGEYR", data=df)
ax.tick_params(rotation=90, labelsize=28)
    

Эта диаграмма показывает семейное положение для каждого диапазона возраста. Посмотрите на скрипичную диаграмму для "Married" (женаты) – она почти одинаковой толщины независимо от возраста, с небольшими утолщениями. "Living with partner" («Живу с партнером») имеет максимальную толщину для возрастов около 30, а после 40 становится намного тоньше. Таким же образом вы можете сделать выводы и из других скрипичных диаграмм.

Скрипичные диаграммы, разделенные по полу

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

        fig = plt.figure(figsize=(16, 8), dpi=80)
grid=plt.GridSpec(4, 4, hspace=0.5, wspace=0.2)

ax_main = fig.add_subplot(grid[:, :-1])
ax_right = fig.add_subplot(grid[:, -1], xticklabels=[], yticklabels=[])

sns.violinplot(x= "DMDMARTLx", y = "BPXDI1", hue = "RIAGENDRx", data = df, color= "0.2", ax=ax_main)
sns.stripplot(x= "DMDMARTLx", y = "BPXDI1", data = df, ax=ax_main)

ax_right.hist(df.BPXDI1, histtype='stepfilled', orientation='horizontal', color='grey')
ax_main.title.set_fontsize(14)
ax_main.tick_params(rotation=10, labelsize=14)

plt.show()
    

Здорово, правда? Посмотрите, как много информации можно получить из этой диаграммы! Этот вид диаграмм может быть очень полезным как для презентации, так и для исследовательского отчета.

Диаграммы с линией линейной регрессии

К диаграмме рассеяния можно добавить линию, показывающую ближайшее приближение распределения к линии. На этот раз мы выведем зависимость роста (BMXHT) от веса (BMXWT), разделенные по полу (RIAGENDR). Я объясню кое-что еще после вывода диаграммы.

        g = sns.lmplot(x='BMXHT', y='BMXWT', hue = 'RIAGENDRx', data = df2,
              aspect = 1.5, robust=True, palette='tab10',
              scatter_kws=dict(s=60, linewidths=.7, edgecolors='black'))
plt.title("Height vs weight with line of best fit grouped by Gender", fontsize=20)
plt.show()
    

В этой диаграмме можно увидеть разделение на мужчин и женщин, выполняемое параметром 'hue'. Из этого рисунка очевидно, что рост и вес мужской популяции в среднем выше, чем женской. Как для мужчин, так и для женщин выведены линии линейной регрессии.

Индивидуальные диаграммы с линией регрессии

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

В этом пункте мы нарисуем lmplot'ы в различных диаграммах. Рост и вес могут быть разными для различного этнического происхождения (RIDRETH1). Вместо пола мы выведем рост и вес для каждой этнической группы в разных диаграммах.

        fig = plt.figure(figsize=(20, 8), dpi=80)
g = sns.lmplot(x='BMXHT', y='BMXWT', data = df2, robust = True,
              palette="Set1", col="RIDRETH1",
              scatter_kws=dict(s=60, linewidths=0.7, edgecolors="black"))
plt.xticks(fontsize=12, )
plt.yticks(fontsize=12)
plt.show()
    

Парные диаграммы

Парные диаграммы очень популярны при исследовательском анализе данных (exploratory data analysis, EDA). Парная диаграмма показывает зависимость каждой переменной от любой другой. Для примера я нарисую парную диаграмму для роста, веса, индекса массы тела и размеров по талии, разделенные по этнической группе. Я беру только первую 1000 элементов, поскольку это может сделать диаграмму немного более понятной.

        df3 = df.loc[:1000, :]
plt.figure(figsize=(10,8), dpi= 80)
sns.pairplot(df3[['BMXWT', 'BMXHT', 'BMXBMI', 'BMXWAIST', "RIDRETH1"]], kind="scatter", hue="RIDRETH1", plot_kws=dict(s=30))
plt.show()
    

Расходящиеся столбики

Диаграмма с расходящимися столбиками (diverging bars) дает быстрый взгляд на данные. Буквально одним взглядом вы можете оценить, насколько данные отклоняются от одной метрики. Я покажу два вида диаграмм с расходящимися столбиками: в первой будет одна качественная переменная по оси x, а во второй – действительные переменные по обоим осям.

Вот первая из них. Я выведу размер дома (качественная переменная) по оси y, а по оси x будет выводиться нормализованное систолическое давление крови. Мы нормализуем систолическое давление с помощью стандартной формулы нормализации, и разделим данные в этом месте.

В этой диаграмме будет два цвета. Красный цвет будет отмечать отрицательные значения, а синий – положительные. Эта диаграмма позволит вам одним взглядом оценить, как распределяется давление крови в зависимости от размеров дома.

        x = df.loc[:, "BPXSY1"]
df["BPXSY1_n"] = (x - x.mean())/x.std()
df['colors'] = ['red' if i < 0 else 'blue' for i in df["BPXSY1_n"]]
df.sort_values("BPXSY1_n", inplace=True)
df.reset_index(inplace=True)
plt.figure(figsize=(16, 10), dpi=80)
plt.hlines(y = df.DMDHHSIZ, xmin=0, xmax = df.BPXSY1_n, color=df.colors, linewidth=3)
plt.gca().set(ylabel="DMDHHSIZ", xlabel = "BPXSY1_n")
plt.yticks(df.DMDHHSIZ, fontsize=14)
plt.grid(linestyle='--', alpha=0.5)
plt.show()
    

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

Я нарисую другую диаграмму, в которой покажу зависимость систолического давления крови от возраста. Мы уже нормализовали систолическое давление для предыдущей диаграммы, поэтому давайте просто погрузимся в нашу диаграмму.

        x = df.loc[:, "BPXSY1"]
df['colors'] = ['coral' if i < 0 else 'lightgreen' for i in df["BPXSY1_n"]]
y_ticks = np.arange(16, 82, 8)
plt.figure(figsize=(16, 10), dpi=80)
plt.hlines(y = df.RIDAGEYR, xmin=0, xmax = df.BPXSY1_n, color=df.colors, linewidth=3)
plt.gca().set(ylabel="RIDAGEYR", xlabel = "BPXSY1")
plt.yticks(y_ticks, fontsize=14)
plt.grid(linestyle='--', alpha=0.5)
plt.show()
    

Этот вариант диаграммы выглядит таким очевидным. Систолическое давление крови в целом растет с возрастом. Не правда ли?

Заключение

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

Источники

МЕРОПРИЯТИЯ

Комментарии 0

ВАКАНСИИ

Golang Backend Developer
Казань, от 70000 RUB до 150000 RUB
Менеджер по маркетингу
от 50000 RUB до 100000 RUB
Программист Go
по итогам собеседования
Project manager
от 50000 RUB до 100000 RUB

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

BUG