Распределения в 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.
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: меньше единицы — кривая прижимается к данным, больше — расплывается.
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, — выброс, и на графике его нарисуют отдельной точкой. У наших десяти цен выбросов нет, но в реальных данных они есть почти всегда.
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 оставляет чистую скрипку для минималистичных слайдов.
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 — вообще отдельная история: разброс вдвое больше центра, дешёвые и прилично дорогие квартиры живут на одной улице. Заметьте, как среднее и медиана почти совпадают в каждом районе — скошенности почти нет, но разброс районы различает именно ящик, а не сводная таблица.
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))
1. Что показывает жирная горизонтальная линия внутри ящика boxplot?
2. Как Seaborn рисует усы boxplot по умолчанию?
3. Что делает параметр kde=True в sns.histplot?
4. Чем violinplot принципиально отличается от boxplot?
5. Как одним вызовом сравнить распределения трёх числовых колонок таблицы?
В редакторе — 12 квартир двух районов: столбцы district и price (тыс. руб. за метр). Постройте boxplot цены по району: тема whitegrid, заголовок «Цена метра: центр против спального района», подпись оси Y «Тыс. руб. за метр». После построения ответьте глазами: у какого района медиана выше?
Что показывает 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-канал или свой блог — так о проекте узнают новые читатели.
Что читать дальше
Matplotlib / Seaborn · Урок 7
Seaborn для начинающих: красивые графики одной строкой
Seaborn рисует статистические графики из DataFrame одной строкой: темы set_theme, countplot, barplot с доверительными интервалами и разбивка hue — прямо в браузере.
Matplotlib / Seaborn · Урок 3
Столбчатые диаграммы и гистограммы в Matplotlib: bar и hist
bar и barh для сравнения категорий, hist для распределений, параметр bins, план против факта и подписи значений над столбцами.
Matplotlib / Seaborn · Урок 9
Тепловые карты и pairplot: heatmap и матрицы диаграмм
Пять строк — и весь датасет на экране: heatmap рисует корреляционную матрицу цветом, pairplot раскладывает все пары переменных в матрицу диаграмм. Разведочный анализ, который раньше занимал день, теперь занимает минуту.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
NumPy · Урок 6
Агрегирующие функции NumPy: sum, mean, min, max, std на реальных данных
Считаем статистику на данных сети кофеен: sum, mean, median, std и percentile, загадка axis=0 против axis=1 и nan-функции для пропусков.
numpy percentile квартилимедиана numpy
NumPy · Урок 9
Случайные числа в NumPy: модуль random и генерация данных
Генератор default_rng: целые, дробные и нормальные случайные числа, choice с весами, shuffle, seed и воспроизводимость — плюс симуляции монетки, кубика и роста тысячи людей.
numpy normal распределениеnumpy для анализа данных
Matplotlib / Seaborn · Урок 10
Проект по визуализации: дашборд данных о фильмах на Matplotlib и Seaborn
Финальный проект курса: датасет из 200 фильмов, EDA, дашборд 2x2 с трендом, жанрами, гистограммой и scatter — плюс чек-лист «график готов к показу» и итоги десяти уроков.
matplotlib seabornвизуализация данных python
Проверьте знания по Matplotlib / Seaborn
В челлендже — 20 задач по Matplotlib / Seaborn, по 2 из каждого урока этого раздела. Формат: фрагмент кода и четыре варианта — что напечатает. После ответа — вердикт и объяснение со ссылкой на урок-источник.
Тест по Matplotlib / Seaborn: 20 задач