Перейти к содержанию

172 уроков, 13 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 8 из 10 Средний 40 мин 150 XP

Распределения в Seaborn: histplot, boxplot и violinplot

Boxplot отвечает на вопрос «где медиана, каков разброс и кто выбивается» быстрее любой таблицы. Разбираем histplot с kde, ящик с усами и violinplot на ценах квартир — и учимся сравнивать группы через hue.

Редакция Питоники

Объявление на сайте агентства: «средняя цена метра в районе — 215 тысяч рублей». Звучит как ответ, а на деле — только вопрос. Сколько квартир дороже 250 тысяч? Половина ли покупателей платит меньше 200? Одинаков ли разброс в центре и на окраине? На все три вопроса отвечает одна картинка — распределение. В этом уроке разберём три его изображения из Seaborn: histplot с kde-кривой, boxplot — легендарный «ящик с усами» — и violinplot, который рисует распределение целиком. А заодно научимся честно сравнивать группы: где разброс больше и почему среднее иногда врёт.

histplot: гистограмма, которая понимает DataFrame

sns.histplot — прямой наследник plt.hist из третьего урока о гистограммах: те же столбцы, тот же параметр bins, та же идея — разбить числовую ось на интервалы и посчитать, сколько значений попало в каждый. Разница в обвязке: передаёте DataFrame и имена колонок — Seaborn сам подписывает оси, подбирает цвета из темы и не заставляет думать о figsize.

Первая гистограмма через sns.histplot
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

# Цена метра, тыс. руб.: 10 объявлений в центре
center = pd.DataFrame({
    "price": [215, 228, 232, 240, 243, 245, 252, 258, 265, 270],
})

sns.set_theme()
ax = sns.histplot(data=center, x="price", bins=5)
ax.set_title("Цена метра в центре: гистограмма")
ax.set_xlabel("Тыс. руб. за метр")
plt.show()
Вывод
График отображается под редактором
Первый запуск загружает библиотеки — до минуты

bins=5 разбил диапазон от 215 до 270 на пять корзин шириной 11 тысяч, высота каждого столбца — число объявлений, попавших в корзину. Ось X подписалась именем колонки price, тема пришла из set_theme. Хотите фиксировать ширину корзины вместо их количества — передайте binwidth=15: для денег это часто нагляднее, потому что корзины получают «круглые» границы.

Практическое правило по числу корзин: слишком мало — гистограмма прячет форму за двумя-тремя столбами, слишком много — на каждой корзине по одному-два наблюдения и график превращается в расчёску. Начните с bins=10 и двигайте руками, глядя на данные: bins — параметр, который честно подбирается глазами, а не формулой.

kde=True: сглаженная кривая плотности

Столбцы зависят от того, куда пришлись корзины: сдвиньте границы на полкорзины — картинка изменится. KDE (kernel density estimation, ядерная оценка плотности) решает эту проблему: вместо корзин она рисует гладкую кривую, сглаживая каждое наблюдение маленьким «холмиком» и складывая холмики. Где кривая выше — там точки гуще. Ширину сглаживания регулирует bw_adjust: меньше единицы — кривая прижимается к данным, больше — расплывается.

Два района: kde-кривые поверх гистограмм
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

rng = np.random.default_rng(42)
center = rng.normal(loc=240, scale=25, size=60).round(0)
suburb = rng.normal(loc=180, scale=45, size=60).round(0)
flats = pd.DataFrame({
    "district": ["Центр"] * 60 + ["Спальный"] * 60,
    "price": list(center) + list(suburb),
})

sns.set_theme()
ax = sns.histplot(data=flats, x="price", hue="district",
                  element="step", stat="density", kde=True,
                  common_norm=False)
ax.set_title("Центр и спальный район: kde-кривые")
ax.set_xlabel("Тыс. руб. за метр")
plt.show()
Вывод
График отображается под редактором

Здесь три параметра, каждый по делу. element="step" рисует контуры вместо полупрозрачных столбов — два набора не заслоняют друг друга. stat="density" переводит ось Y из «штук» в плотность: у обоих районов по 60 объявлений, но если бы было 60 и 600, частоты врать бы начали, а плотности — нет. common_norm=False запрещает Seaborn нормировать обе группы на общую сумму — каждая кривая получает свою площадь под собой, и высоты сравнимы честно.

И вот что показала картинка: центр — узкий пик около 240 тысяч, спальный район — широкий холм от 100 до 260. Форма разная, и это видно до всяких формул.

boxplot: как читать ящик с усами

Гистограмма показывает форму, но точных чисел не даёт. boxplot сжимает выборку в пять чисел — «пятичисленную сводку»: медиана, первый квартиль Q1, третий квартиль Q3 и два конца усов. Посмотрим, что скрывает ящик, на тех же десяти ценах центра — только сначала посчитаем сводку руками через pandas:

Пятичисленная сводка своими руками
import pandas as pd

center = pd.Series([215, 228, 232, 240, 243, 245, 252, 258, 265, 270])
q1, med, q3 = center.quantile([0.25, 0.5, 0.75])
iqr = q3 - q1
print(q1, med, q3)
print(iqr, q1 - 1.5 * iqr, q3 + 1.5 * iqr)
Вывод
234.0 244.0 256.5
22.5 200.25 290.25

Читаем: четверть квартир дешевле 234 тысяч, медиана — 244, три четверти — дешевле 256.5. Разница Q3 − Q1 равна 22.5 — это IQR, межквартильный размах, ширина «середины» выборки, где живут центральные 50% данных. Усы дотягиваются до самого обычного наблюдения в пределах 1.5 × IQR от квартилей: всё, что левее 200.25 или правее 290.25, — выброс, и на графике его нарисуют отдельной точкой. У наших десяти цен выбросов нет, но в реальных данных они есть почти всегда.

boxplot одной колонки
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

center = pd.DataFrame({
    "price": [215, 228, 232, 240, 243, 245, 252, 258, 265, 270],
})

sns.set_theme()
ax = sns.boxplot(data=center, y="price")
ax.set_title("Цена метра в центре: ящик с усами")
ax.set_ylabel("Тыс. руб. за метр")
plt.show()
Вывод
График отображается под редактором

Ящик — от Q1 до Q3, жирная линия внутри — медиана 244, усы — до 200.25 и 290.25. Обратите внимание: мы указали только y=, без x=, и ящик лёг вертикально. Одиночный boxplot — быстрая проверка; сравнение групп — его главная работа.

Сравнение групп: x, y и hue в boxplot

Классическая раскладка boxplot: категориальный столбец в x=, числовой в y= — по одному ящику на каждую категорию. Сгенерируем три района генератором numpy (так данные детерминированы: при каждом запуске одни и те же цены) и построим:

Цена метра по трём районам
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

rng = np.random.default_rng(42)
center = rng.normal(240, 25, 30).round(0)
suburb = rng.normal(180, 45, 30).round(0)
new = rng.normal(260, 12, 30).round(0)
flats = pd.DataFrame({
    "district": ["Центр"] * 30 + ["Спальный"] * 30 + ["Новый"] * 30,
    "price": list(center) + list(suburb) + list(new),
})

sns.set_theme()
ax = sns.boxplot(data=flats, x="district", y="price")
ax.set_title("Цена метра по районам: ящик с усами")
ax.set_xlabel("Район")
ax.set_ylabel("Тыс. руб. за метр")
plt.show()
Вывод
График отображается под редактором

Три ящика — три района, и уже взгляду видно главное: у «Нового» ящик узкий и высоко, у «Спального» — широченный и низко. Медианы — жирные линии внутри. Поменяете x= и y= местами — ящики лягут горизонтально: Seaborn сам определяет ориентацию по типам данных, категория уходит на выбранную ось. А hue= добавит ещё один разрез поверх районов — например, расколет каждый ящик на «с балконом» и «без».

violinplot: распределение, свёрнутое в скрипку

violinplot — это kde-кривая, отражённая зеркально относительно вертикальной оси: ширина скрипки в каждом месте — плотность точек, широкое брюхо — много данных, узкая талия — мало. Внутри по умолчанию прячется мини-ящик (inner="box"), так что медиана и квартили никуда не делись. Вариант inner="quart" рисует три линии квартилей, inner=None оставляет чистую скрипку для минималистичных слайдов.

violinplot по районам
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

rng = np.random.default_rng(42)
center = rng.normal(240, 25, 30).round(0)
suburb = rng.normal(180, 45, 30).round(0)
new = rng.normal(260, 12, 30).round(0)
flats = pd.DataFrame({
    "district": ["Центр"] * 30 + ["Спальный"] * 30 + ["Новый"] * 30,
    "price": list(center) + list(suburb) + list(new),
})

sns.set_theme()
ax = sns.violinplot(data=flats, x="district", y="price", inner="quart")
ax.set_title("Цена метра по районам: violinplot")
ax.set_xlabel("Район")
ax.set_ylabel("Тыс. руб. за метр")
plt.show()
Вывод
График отображается под редактором

Когда скрипка лучше ящика: если в данных два горба — например, район застроен и панельками, и элитками, — boxplot покажет ровный симметричный ящик, а violinplot честно разделится на две доли. Для презентаций скрипка тоже выигрывает: форма считывается на проекторе быстрее, чем пять чисел. Когда хуже: точные значения медианы читаются с усилием, скрипки шире ящиков и съедают место, а на данных со «звёздами»-выбросами ящик с его точками информативнее.

Кейс: цены по районам — где разброс больше

Соберём приёмы урока в одну задачу, которую решают на каждом проекте: три района, одна метрика, заказчик спрашивает «а где дороже?». Сначала цифры — медиана, среднее и стандартное отклонение по каждому району (groupby мы разбирали в уроке про группировку Pandas):

Сводка по районам: медиана, среднее, разброс
import numpy as np
import pandas as pd

rng = np.random.default_rng(42)
center = rng.normal(240, 25, 30).round(0)
suburb = rng.normal(180, 45, 30).round(0)
new = rng.normal(260, 12, 30).round(0)
flats = pd.DataFrame({
    "district": ["Центр"] * 30 + ["Спальный"] * 30 + ["Новый"] * 30,
    "price": list(center) + list(suburb) + list(new),
})

stats = flats.groupby("district")["price"].agg(["median", "mean", "std"])
print(stats.round(1))
Вывод
          median   mean   std
district                     
Новый      259.0  258.8   8.3
Спальный   187.5  185.1  36.2
Центр      242.5  240.4  19.5

Медианы центра (242.5) и «Нового» (259.0) говорят: оба района дорогие. Но стандартное отклонение — 19.5 против 8.3: новый район предсказуем, в центре цена метра — лотерея. «Спальный» со средним 185.1 и отклонением 36.2 — вообще отдельная история: разброс вдвое больше центра, дешёвые и прилично дорогие квартиры живут на одной улице. Заметьте, как среднее и медиана почти совпадают в каждом районе — скошенности почти нет, но разброс районы различает именно ящик, а не сводная таблица.

Два взгляда на одни данные: boxplot и violinplot рядом
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

rng = np.random.default_rng(42)
center = rng.normal(240, 25, 30).round(0)
suburb = rng.normal(180, 45, 30).round(0)
new = rng.normal(260, 12, 30).round(0)
flats = pd.DataFrame({
    "district": ["Центр"] * 30 + ["Спальный"] * 30 + ["Новый"] * 30,
    "price": list(center) + list(suburb) + list(new),
})

sns.set_theme()
fig, axes = plt.subplots(1, 2, figsize=(11, 4.5), sharey=True)
sns.boxplot(data=flats, x="district", y="price", ax=axes[0])
axes[0].set_title("boxplot: пять чисел и выбросы")
sns.violinplot(data=flats, x="district", y="price", ax=axes[1])
axes[1].set_title("violinplot: форма распределения")
fig.suptitle("Цена метра, тыс. руб.: два взгляда на три района")
plt.show()
Вывод
График отображается под редактором

Параметр ax= — то место, где Seaborn честно признаётся, что он всего лишь надстройка: передайте ему конкретные оси из plt.subplots (пятый урок про subplots), и график нарисуется именно там. Это главный паттерн дашбордов, которые мы соберём в десятом уроке: Seaborn рисует статистику в панели, Matplotlib компонует панели.

Шпаргалка: какой график для распределения выбрать

Все четыре графика урока отвечают на один вопрос — «как распределены числа» — под разными углами. Моя рабочая привычка: boxplot для сравнения групп, violinplot для презентации одной-двух групп, histplot для одной переменной с полным разбором. Сводка:

ГрафикЧто показываетКогда брать
histplotЧастоты по корзинам; с kde=True — ещё и формаОдна переменная, знакомство с данными
kde (kde=True, kdeplot)Гладкая форма без корзинСравнение форм нескольких групп
boxplotМедиана, квартили, усы 1.5·IQR, выбросыМного групп, важны выбросы и точные числа
violinplotФорма распределения + квартили внутри1–4 группы, презентации, горбы в данных

Что дальше

Мы сравнивали районы друг с другом, но не задали главный вопрос разведочного анализа: а связаны ли цена, площадь и этаж между собой? В следующем уроке — тепловые карты heatmap и pairplot: корреляционная матрица с аннотациями, цветовые схемы cmap и весь датасет на одном рисунке за пять строк. Потом — финальный проект с дашбордом. Пришли из поиска за одним boxplot? Полный маршрут из десяти уроков — самоучитель Matplotlib, и распределения здесь восьмая ступень. До встречи попробуйте прогнать этот урок на своих данных: зарплаты, траты, время в пути. Ящик с усами один раз показал вам выброс — и вы больше не поверите средней из отчёта.

Что выведет код?

Сначала предскажи ответ в голове — это главный навык программиста.

import pandas as pd

prices = pd.Series([120, 140, 160, 180, 200, 220, 240, 260])
q1, q3 = prices.quantile([0.25, 0.75])
print(q3 - q1)
import pandas as pd
import seaborn as sns

df = pd.DataFrame({"price": [5, 7, 9, 15, 8, 6]})
ax = sns.histplot(data=df, x="price", bins=2)
print(len(ax.patches))
Проверь себя
0 / 5

1. Что показывает жирная горизонтальная линия внутри ящика boxplot?

2. Как Seaborn рисует усы boxplot по умолчанию?

3. Что делает параметр kde=True в sns.histplot?

4. Чем violinplot принципиально отличается от boxplot?

5. Как одним вызовом сравнить распределения трёх числовых колонок таблицы?

Карточки терминов
Запомнено: 0 / 6
Практика

В редакторе — 12 квартир двух районов: столбцы district и price (тыс. руб. за метр). Постройте boxplot цены по району: тема whitegrid, заголовок «Цена метра: центр против спального района», подпись оси Y «Тыс. руб. за метр». После построения ответьте глазами: у какого района медиана выше?

practice.py
Вопросы и ответы по уроку

Что показывает boxplot (ящик с усами) в Seaborn?

Пять чисел: медиану, первый и третий квартили (края ящика) и концы усов — по правилу 1.5 × IQR. Точки за усами — выбросы. Один взгляд отвечает, где центр данных, каков разброс и есть ли аномалии.

Чем violinplot лучше boxplot и когда его использовать?

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

Что такое kde в seaborn и зачем параметр kde=True?

KDE — ядерная оценка плотности: вместо корзин гистограммы рисуется гладкая кривая плотности. kde=True добавляет её поверх столбцов histplot, а sns.kdeplot рисует только кривую. Сглаживание регулирует bw_adjust.

Как сравнить распределения нескольких столбцов сразу?

Передайте список колонок в «широкой форме»: sns.boxplot(data=df[["col1", "col2", "col3"]]) — Seaborn нарисует по ящику на колонку. Следите, чтобы колонки были одного масштаба, иначе мелкие значения расплющатся у нуля.

Как boxplot определяет выбросы?

Всё, что дальше 1.5 × IQR от квартилей: концы усов лежат на Q1 − 1.5 × IQR и Q3 + 1.5 × IQR, всё за ними рисуется отдельными точками. Для десяти цен центра это 200.25 и 290.25. Правило зависит от программы: Excel рисует усы до минимума и максимума, seaborn и matplotlib — по 1.5 × IQR, поэтому одинаковые данные выглядят по-разному.

Понравился урок? Сошлитесь на него

«Ящик с усами один раз показал вам выброс — и вы больше не поверите средней из отчёта.»

Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.

Проверьте знания по Matplotlib / Seaborn

В челлендже — 20 задач по Matplotlib / Seaborn, по 2 из каждого урока этого раздела. Формат: фрагмент кода и четыре варианта — что напечатает. После ответа — вердикт и объяснение со ссылкой на урок-источник.

Тест по Matplotlib / Seaborn: 20 задач