filter групп, pivot_table и crosstab

groupby умеет не только агрегировать, но и отбирать целые группы, а pivot_table и crosstab превращают длинные данные в наглядные сводки.

filter оставляет группы целиком по условию на группу. pivot_table — сводная таблица: значения раскладываются по строкам и столбцам с агрегацией.

filter: отбор целых групп

Иногда нужно оставить не отдельные строки, а целые группы, удовлетворяющие условию: «только города, где больше 100 заказов», «только клиенты с суммарной покупкой выше порога». Это делает filter — он получает под-DataFrame группы и должен вернуть True (оставить всю группу) или False (выбросить целиком).

# оставить только города, где суммарный чек > 1500
df.groupby("город").filter(lambda g: g["чек"].sum() > 1500)

Покажем механику на чистом Python — сначала считаем агрегат по группе, потом решаем судьбу всей группы:

orders = [
    ("Москва", 1200), ("Сочи", 800), ("Москва", 1800),
    ("Сочи", 1000), ("Москва", 600), ("Тверь", 300),
]

# split
groups = {}
for city, chek in orders:
    groups.setdefault(city, []).append(chek)

# условие на ВСЮ группу: суммарный чек > 1500
keep_cities = {c for c, v in groups.items() if sum(v) > 1500}
print("оставляем города:", sorted(keep_cities))

# filter: оставляем строки, чья группа прошла условие
result = [(c, chek) for c, chek in orders if c in keep_cities]
for row in result:
    print(row)

Вывод:

оставляем города: ['Москва', 'Сочи']
('Москва', 1200)
('Сочи', 800)
('Москва', 1800)
('Сочи', 1000)
('Москва', 600)

Москва (сумма 3600) и Сочи (1800) прошли порог 1500, поэтому сохранились все их строки. Тверь (300) не прошла — её единственная строка пропадает целиком. Здесь важно увидеть принцип: filter решает судьбу всей группы по агрегату на группу, а не каждой строки по отдельности.

pivot_table: сводная таблица

pivot_table — это группировка сразу по двум измерениям: одно идёт в строки, другое в столбцы, а на пересечении — агрегат. Это та самая «сводная таблица», что в Excel, но программно и воспроизводимо.

df.pivot_table(
    index="город",      # строки
    columns="месяц",    # столбцы
    values="чек",       # что агрегируем
    aggfunc="sum",      # как агрегируем (по умолчанию mean)
    fill_value=0,       # чем заполнить пустые пересечения
    margins=True,       # добавить строку/столбец "All" с итогами
)
#         янв   фев   All
# город
# Москва  1800  1800  3600
# Сочи     800  1000  1800
# All     2600  2800  5400

Ключевые параметры: aggfunc (можно список: ["sum", "mean"]), fill_value (вместо NaN в пустых ячейках), margins (итоги по строкам и столбцам). По сути это groupby([index, columns]).agg(...), но сразу разложенный в матрицу.

crosstab: частотная таблица

Когда нужно просто посчитать, сколько раз встречается каждая комбинация двух категорий (таблица сопряжённости), удобнее crosstab — частный случай pivot_table для подсчёта частот:

pd.crosstab(df["город"], df["тип_оплаты"])
#              карта  наличные
# город
# Москва          5         2
# Сочи            3         4

# доли по строкам вместо абсолютных чисел
pd.crosstab(df["город"], df["тип_оплаты"], normalize="index")

Параметр normalize превращает счётчики в доли (по строкам, столбцам или всей таблице) — удобно для анализа структуры. По сути crosstab — это удобная обёртка над pivot_table с aggfunc="count": если передать ему values= и свой aggfunc=, он перестанет просто считать частоты и начнёт агрегировать значения, превратившись в обычную сводную таблицу.

pivot_table и groupby — одно и то же под разным углом

Полезно увидеть, что pivot_table — это «разложенный в матрицу» groupby по двум ключам. Эти два кода эквивалентны по содержанию, отличается только форма:

# длинный результат (MultiIndex: город × месяц)
df.groupby(["город", "месяц"])["чек"].sum()

# тот же результат, развёрнутый в матрицу
df.groupby(["город", "месяц"])["чек"].sum().unstack()

# и это же — одной командой
df.pivot_table(index="город", columns="месяц", values="чек", aggfunc="sum")

Связь с разделом про реструктуризацию прямая: pivot_table = groupby(...).agg(...) + unstack(). Понимание этого избавляет от заучивания: вы выбираете форму (длинную или широкую) под задачу, а не запоминаете отдельные «магические» функции.

Работа с результатом группировки

Результат groupby/pivot_table — обычный DataFrame или Series, но с ключами группировки в индексе (часто MultiIndex). Полезные приёмы дальнейшей работы:

res = df.groupby("город")["чек"].sum()
res.reset_index()           # вернуть индекс-город обратно в столбец
res.sort_values(ascending=False)  # отсортировать по значению
res.nlargest(3)             # топ-3 группы
g = df.groupby(["город", "месяц"])["чек"].sum()
g.unstack()                 # развернуть второй уровень индекса в столбцы

reset_index() часто нужен, чтобы отдать результат в график или дальнейший merge. unstack() превращает MultiIndex-результат группировки в широкую таблицу — фактически тот же эффект, что у pivot_table (подробнее о stack/unstack — в разделе про реструктуризацию).

Подводные камни

  • filter возвращает строки, а не группы. На выходе — отфильтрованный DataFrame из исходных строк, а не сводка по группам; не путайте с agg.
  • NaN в pivot_table. Пустые пересечения дают NaN; используйте fill_value=0, если это уместно (но не для средних — там ноль исказит смысл).
  • aggfunc по умолчанию — mean. Частая ошибка: ждать сумму, а получить среднее. Указывайте aggfunc явно.
  • crosstab считает частоты, а не значения. Чтобы агрегировать значения, передайте values= и aggfunc= — тогда это уже почти pivot_table.

Лучшие практики

  • Нужно отобрать целые группы по условию на группу — это filter.
  • Сводка «строки × столбцы × агрегат» — pivot_table с явным aggfunc и осмысленным fill_value.
  • Подсчёт комбинаций категорий — crosstab, при необходимости с normalize.
  • Перед выгрузкой/визуализацией приводите результат в порядок: reset_index(), сортировка, unstack().

Итог

  • filter оставляет или выбрасывает группы целиком по условию на группу.
  • pivot_table — сводка по двум измерениям с агрегацией и итогами.
  • crosstab — частотная таблица комбинаций категорий, с опцией долей.
  • Результат группировки — DataFrame с ключами в индексе; reset_index/unstack приводят его к нужной форме.
Проверьте себя
1. Что возвращает df.groupby('город').filter(lambda g: len(g) > 10)?
AПо одной строке на каждую группу
BИсходные строки, но только из групп, где больше 10 строк
CЧисло групп больше 10
DСписок названий групп
2. Какой aggfunc используется в pivot_table по умолчанию?
Asum
Bcount
Cmean
Dmax
3. Для чего удобнее всего crosstab?
AДля интерполяции пропусков
BДля подсчёта частот комбинаций двух категорий (таблица сопряжённости)
CДля сортировки данных
DДля приведения типов
Поддержать проект