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Для подсчёта частот комбинаций двух категорий (таблица сопряжённости)