FacetGrid: малые кратные — сравнить группы рядом
Три города на одной гистограмме — каша, три гистограммы рядом — отчёт. FacetGrid: одна формула графика, сетка панелей по любой колонке таблицы.
Редакция Питоники
Задача из жизни аналитика: в интернет-магазин пришло три потока чеков — из Москвы, Казани и Сочи — и руководителю нужно понять, чем отличаются покупатели городов. Первый инстинкт — наложить всё на один график и раскрасить по городам. Получится каша. Правильный приём придумал задолго до pandas: Эдвард Тюфте назвал его малыми кратными — каждая группа получает свою маленькую панель, и сравнение происходит взглядом слева направо. В Seaborn за это отвечает FacetGrid.
Проблема: три распределения на одном графике
Данные урока — 15 чеков, по пять на город. Суммы в рублях: Москва заметно дороже, Сочи дешевле, но с одним крупным заказом. Сначала посмотрим, как эту задачу решают «в лоб»:
import seaborn as sns
import matplotlib.pyplot as plt
import pandas as pd
df = pd.DataFrame({
"город": ["Москва", "Москва", "Москва", "Москва", "Москва",
"Казань", "Казань", "Казань", "Казань", "Казань",
"Сочи", "Сочи", "Сочи", "Сочи", "Сочи"],
"чек": [1240, 830, 1520, 990, 1110,
620, 540, 780, 690, 610,
350, 980, 470, 390, 420],
})
sns.histplot(data=df, x="чек", hue="город", bins=12)
plt.show()
График отображается под редактором
Формально всё правильно: histplot из урока про распределения, hue раскрасил города. На практике читать невозможно: заливки налегают друг на друга, столбики мешают друг другу, а редкий сочинский чек растягивает ось так, что казанская куча сжимается в узкую полоску. Сводка числами подтверждает, что разница есть, но не показывает её форму:
import pandas as pd
df = pd.DataFrame({
"город": ["Москва", "Москва", "Москва", "Москва", "Москва",
"Казань", "Казань", "Казань", "Казань", "Казань",
"Сочи", "Сочи", "Сочи", "Сочи", "Сочи"],
"чек": [1240, 830, 1520, 990, 1110,
620, 540, 780, 690, 610,
350, 980, 470, 390, 420],
})
summary = df.groupby("город")["чек"].agg(["count", "mean", "max"]).round(0)
print(summary)
count mean max город Казань 5 648.0 780 Москва 5 1138.0 1520 Сочи 5 522.0 980
FacetGrid: одна формула — много панелей
FacetGrid работает в два шага. Первый: g = sns.FacetGrid(df, col="город") — контейнер создаёт сетку, по одной панели на значение колонки «город». Второй: g.map(функция, "колонка") — функция рисуется в каждую панель, причём ей отдаётся только подмножество строк своей группы. Формула графика пишется один раз, а применяется столько раз, сколько панелей:
import seaborn as sns
import matplotlib.pyplot as plt
import pandas as pd
df = pd.DataFrame({
"город": ["Москва", "Москва", "Москва", "Москва", "Москва",
"Казань", "Казань", "Казань", "Казань", "Казань",
"Сочи", "Сочи", "Сочи", "Сочи", "Сочи"],
"чек": [1240, 830, 1520, 990, 1110,
620, 540, 780, 690, 610,
350, 980, 470, 390, 420],
})
g = sns.FacetGrid(df, col="город", height=3)
g.map(sns.histplot, "чек", binwidth=200)
g.set_axis_labels("Сумма чека, руб.", "Число чеков")
g.set_titles("город: {col_name}")
plt.show()
График отображается под редактором
Три одинаковые оси — и сравнение честное: Москва стоит на 800-1500, Казань компактно в районе 600-800, у Сочи основная масса до 500 плюс одиночный чек под тысячу. Обратите внимание на binwidth=200: одинаковая ширина корзин на всех панелях делает столбики сопоставимыми, а set_titles вставляет значение категории в заголовок через шаблон {col_name}.
map и map_dataframe
g.map принимает любую функцию первого аргументом — не только sns-функции. Правило простое: map вызывает её как функция(подмножество_по_оси, ...) и подходит для готовых seaborn-графиков. Для собственной функции удобнее g.map_dataframe: она передаёт в функцию целое подмножество таблицы как data, и внутри доступны все колонки. Текущую панель функция берёт через plt.gca() — «get current axes». Добавим на каждую панель вертикальную линию среднего:
import seaborn as sns
import matplotlib.pyplot as plt
import pandas as pd
df = pd.DataFrame({
"город": ["Москва", "Москва", "Москва", "Москва", "Москва",
"Казань", "Казань", "Казань", "Казань", "Казань",
"Сочи", "Сочи", "Сочи", "Сочи", "Сочи"],
"чек": [1240, 830, 1520, 990, 1110,
620, 540, 780, 690, 610,
350, 980, 470, 390, 420],
})
def add_mean(data, **kws):
ax = plt.gca()
ax.axvline(data["чек"].mean(), color="crimson", linestyle="--", linewidth=2)
g = sns.FacetGrid(df, col="город", height=3)
g.map_dataframe(sns.histplot, x="чек", binwidth=200)
g.map_dataframe(add_mean)
g.set_axis_labels("Сумма чека, руб.", "Число чеков")
plt.show()
График отображается под редактором
Каждая панель получила пунктир на своём среднем: 1138 у Москвы, 648 у Казани, 522 у Сочи. map_dataframe вызывает add_mean отдельно для каждого подмножества строк — именно поэтому data["чек"].mean() внутри каждый раз считает свою цифру, а не общую.
Внутри панелей работает и hue: каждая панель раскрашивается по своей группе, а общую легенду собирает g.add_legend(). Так сравнивают уже два разреза — города рядом, каналы цветом:
import seaborn as sns
import matplotlib.pyplot as plt
import pandas as pd
df = pd.DataFrame({
"город": (["Москва"] * 6) + (["Казань"] * 6),
"канал": (["онлайн", "магазин"] * 6),
"чек": [1240, 830, 1520, 990, 1110, 700,
620, 540, 780, 690, 610, 650],
})
g = sns.FacetGrid(df, col="город", hue="канал", height=3)
g.map(sns.histplot, "чек", binwidth=200, alpha=0.6)
g.add_legend()
g.set_axis_labels("Сумма чека, руб.", "Число чеков")
plt.show()
График отображается под редактором
row, col_wrap и размер панелей
Параметр row добавляет второе измерение сетки: FacetGrid(df, col="город", row="канал") даст панель на каждую пару значений город × канал. Размер каждой панели, как и у catplot, задают height и aspect:
import seaborn as sns
import matplotlib.pyplot as plt
import pandas as pd
df = pd.DataFrame({
"город": (["Москва"] * 6) + (["Казань"] * 6),
"канал": (["онлайн", "магазин"] * 6),
"чек": [1240, 830, 1520, 990, 1110, 700,
620, 540, 780, 690, 610, 650],
})
g = sns.FacetGrid(df, col="город", row="канал", height=2.4, aspect=1.4)
g.map(sns.histplot, "чек", binwidth=150)
plt.show()
График отображается под редактором
Четыре панели — четыре среза данных, собранные одним вызовом. Когда категорий много — скажем, двадцать городов — сетка по колонкам не влезет в ширину: col_wrap=4 переносит панели, как текст, по 4 в ряд. Порядок панелей совпадает с порядком значений категории в таблице, так что нужный город легко поставить первым сортировкой исходного DataFrame.
| Задача | Инструмент |
|---|---|
| Стандартный категориальный график в сетке | catplot с kind: bar, box, point |
| Распределения по группам, как в уроке | FacetGrid + g.map(sns.histplot, ...) |
| Свой рисунок на каждой панели | FacetGrid + g.map_dataframe(функция) |
| Панели внутри одного графика Matplotlib | plt.subplots() и ax= |
Что дальше
FacetGrid завершает историю про сетки: catplot для стандартных видов, FacetGrid для всего остального. Дальше — Seaborn и Pandas DataFrame: как готовить таблицы к любому графику курса — groupby, melt, настоящие даты и сортировка.
Малые кратные — это когда каждая группа получает свою маленькую панель, и сравнение происходит взглядом слева направо.
Сначала предскажи ответ в голове — это главный навык программиста.
import seaborn as sns
import matplotlib.pyplot as plt
import pandas as pd
df = pd.DataFrame({
"город": ["Москва", "Казань", "Сочи"] * 3,
"чек": [1240, 620, 350, 830, 540, 980, 1520, 780, 470],
})
g = sns.FacetGrid(df, col="город")
print(g.axes.shape)
import seaborn as sns
import matplotlib.pyplot as plt
import pandas as pd
df = pd.DataFrame({
"город": ["Москва", "Казань", "Сочи", "Пенза"],
"чек": [1240, 620, 350, 500],
})
g = sns.FacetGrid(df, col="город", col_wrap=2)
print(g.axes.shape)
import pandas as pd
df = pd.DataFrame({
"город": ["Москва", "Москва", "Москва", "Москва", "Москва",
"Казань", "Казань", "Казань", "Казань", "Казань",
"Сочи", "Сочи", "Сочи", "Сочи", "Сочи"],
"чек": [1240, 830, 1520, 990, 1110,
620, 540, 780, 690, 610,
350, 980, 470, 390, 420],
})
print(df.groupby("город")["чек"].agg(["count", "mean", "max"]).round(0))
1. В чём идея малых кратных (small multiples)?
2. Что делает g.map(sns.histplot, "чек")?
3. Чем g.map_dataframe отличается от g.map?
4. Зачем нужен col_wrap?
5. Как добавить второе измерение разреза в сетку FacetGrid?
Постройте FacetGrid по товарам: по панели на каждый товар (col="товар"), в каждой — гистограмма выручки с binwidth=5, общие подписи осей «Выручка, тыс. руб.» и «Число продаж». Данные в заготовке.
Что такое малые кратные в визуализации данных?
Small multiples — приём, при котором одна формула графика повторяется для каждой группы данных в виде маленьких одинаковых панелей. Читатель сравнивает группы взглядом слева направо без расшифровки наложенных серий. Приём популяризовал Эдвард Тюфте, а в Seaborn за него отвечают FacetGrid, catplot, relplot и displot.
Урок 18: FacetGrid на данных чековЧем FacetGrid отличается от catplot?
catplot — специализированная надстройка: kind="bar" или kind="box" уже содержит готовую формулу графика, остаётся передать данные. FacetGrid — общий механизм: формулу задаёте сами через g.map() или g.map_dataframe(), поэтому он подходит для любых графиков, включая собственные функции Matplotlib. Внутри catplot использует FacetGrid.
Как поменять заголовки панелей FacetGrid?
Методом g.set_titles(): шаблон "{col_name}" вставит значение категории — g.set_titles("город: {col_name}"). Формат принимает и параметры размера шрифта, как обычный заголовок. Подписи общих осей задаются g.set_axis_labels(x_label, y_label) один раз на всю сетку.
Можно ли встроить график FacetGrid в свою фигуру с subplots?
Нет: FacetGrid — figure-level контейнер, он сам создаёт фигуру и не принимает ax=. Если панели нужно положить в собственную композицию, строите сетку через plt.subplots() и рисуете в каждую ax подмножество данных фильтрацией DataFrame. Для обычных случаев проще остаться внутри FacetGrid и управлять размером через height и aspect.
Урок 5: subplots в MatplotlibПонравился урок? Сошлитесь на него
«Малые кратные — это когда каждая группа получает свою маленькую панель, и сравнение происходит взглядом слева направо.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что читать дальше
Seaborn · Урок 17
Подписи, заголовки и аннотации: график Seaborn для отчёта
Seaborn строит график за строку, а отчёт требует заголовка, подписей осей, аннотации пика и файла в PNG. Собираем всё это через Matplotlib API — поверх любого sns-графика.
Seaborn · Урок 19
Seaborn и Pandas DataFrame: таблица как источник графиков
Seaborn ест DataFrame напрямую, но красивый график начинается до него: groupby, melt из широкого формата в длинный, настоящие даты и правильный порядок категорий.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
Matplotlib / Seaborn · Урок 8
Гистограмма и ящик с усами в Matplotlib: hist и boxplot
plt.hist и plt.boxplot на одних данных: бины и density, чтение формы распределения, медиана, квартили, усы 1.5 × IQR и выбросы — плюс сравнение двух групп обоими способами. Весь код запускается на странице.
сравнение распределений python
Matplotlib / Seaborn · Урок 5
Круговые диаграммы и subplots: несколько графиков на одном полотне
pie и donut с честными границами применимости, сетка графиков через plt.subplots, общий заголовок suptitle и мини-дашборд 2 на 2 в финале.
subplot matplotlib несколько графиковнесколько графиков на одном рисунке python
Seaborn · Урок 15
catplot: категориальные сетки графиков в Seaborn
Одна функция catplot заменяет полдесятка категориальных графиков: переключатель kind, раскладка по сетке col и row — и сравнение групп превращается в три панели рядом.
seaborn catplotseaborn сетка графиков