Загружаем научный разбор
Подготавливаем текст, источники и редакционные примечания без изменения разметки страницы.
Подготавливаем текст, источники и редакционные примечания без изменения разметки страницы.
Автор: Казачкин Даниил Михайлович · Обновлено
Научная основа pandas GroupBy: split–apply–combine, различия agg, transform, filter и apply, пропущенные ключи, инварианты и границы статистических выводов.
Выражение df.groupby(...).agg(...) выглядит как один вызов, но за ним стоит общий способ рассуждать о данных: разделить наблюдения по ключу, независимо выполнить операцию над каждой частью и собрать результаты в новую структуру. Хэдли Уикхем назвал эту схему split–apply–combine и показал её как систематическую стратегию анализа, а не только как удобную функцию конкретной библиотеки.
Идея помогает писать код, но ещё важнее — проверять смысл результата. До запуска агрегации аналитик должен решить, что образует группу, что функция возвращает для одной группы и какую форму будет иметь объединение. Ошибки в этих трёх решениях могут дать правдоподобную таблицу без исключений.
Split. Каждой строке назначается ключ: одно значение, кортеж нескольких колонок, временной интервал или вычисляемая категория. Строки с одинаковым ключом образуют часть.
Apply. Для каждой части выполняется функция. Это может быть сводка до одной строки, преобразование той же длины, фильтр всей группы или произвольная операция.
Combine. Частичные результаты собираются в Series или DataFrame. Индекс, названия колонок, число строк и порядок зависят от вида операции и параметров группировки.
Такое описание отделяет математическую постановку от синтаксиса. Например, «средняя выручка по магазину и товару» задаёт ключ (store, product), функцию среднего и таблицу с одной строкой на ключ. «Доля выручки строки внутри магазина» использует тот же split, но apply возвращает по одному значению на исходную строку; это уже transform, а не aggregation.
Воспроизводимый отчёт лучше начинать с явной схемы результата. Named aggregation связывает имя новой колонки с исходной колонкой и функцией:
import pandas as pd
sales = pd.DataFrame(
{
"store": ["north", "north", "south", "south", None],
"product": ["book", "book", "book", "pen", "book"],
"quantity": [2, 1, 4, 5, 3],
"revenue": [1_000, 600, 2_400, 500, 1_800],
}
)
summary = (
sales.groupby(["store", "product"], dropna=False, as_index=False)
.agg(
rows=("revenue", "size"),
sold=("quantity", "sum"),
revenue=("revenue", "sum"),
average_receipt=("revenue", "mean"),
)
.sort_values(["store", "product"], na_position="last")
)
assert summary["rows"].sum() == len(sales)
assert summary["revenue"].sum() == sales["revenue"].sum()
print(summary)Параметр dropna=False здесь является предметным решением: строка без магазина сохраняется как отдельная группа. При другом контракте неизвестные ключи можно предварительно отвергнуть или маркировать. Молчаливое исчезновение строк из группировки особенно опасно для финансовой суммы, поэтому инвариант сравнивает итог с исходной таблицей.
size считает строки группы, а count для конкретной колонки считает только её непустые значения. Эти величины совпадают лишь при отсутствии пропусков. Название «количество заказов» без указания правила может скрыть разницу.
Transform нужен, когда групповая статистика должна вернуться к каждому наблюдению. Результат выравнивается с индексом исходного объекта:
known_store = sales["store"].notna()
store_total = sales.loc[known_store].groupby("store")["revenue"].transform("sum")
sales.loc[known_store, "share_in_store"] = (
sales.loc[known_store, "revenue"] / store_total
)
share_sum = sales.loc[known_store].groupby("store")["share_in_store"].sum()
assert share_sum.round(10).eq(1.0).all()Здесь делитель вычисляется отдельно для каждой группы, но итог остаётся построчным. Если вместо transform('sum') использовать обычный agg('sum'), получится Series с индексом магазинов; прямое присваивание в таблицу может выровнять несовместимые индексы и создать пропуски. pandas выравнивает данные по labels, а не обещает позиционное совпадение только потому, что длины кажутся похожими.
Transform подходит для нормализации внутри группы, заполнения пропусков групповой медианой и сравнения строки с групповым baseline. Он не гарантирует статистическую корректность такой нормализации: слишком маленькие группы дают нестабильные оценки, а использование будущих строк при подготовке ML-признака создаёт утечку данных.
Групповой filter вычисляет условие для части и оставляет либо удаляет относящиеся к ней строки. Например, можно анализировать только магазины с не менее чем ста наблюдениями. Это отличается от обычной маски sales['revenue'] > 0, которая решает судьбу каждой строки независимо.
Фильтр после просмотра результата является аналитическим выбором. Порог, подобранный так, чтобы получить красивый эффект, меняет исследуемую популяцию. Его следует обосновать до сравнения групп или показать sensitivity analysis для нескольких порогов.
GroupBy.apply позволяет передать функцию, возвращающую почти произвольный объект. Гибкость полезна, когда задача действительно не выражается через aggregation, transform или filter. Однако pandas должен понять и объединить неоднородные результаты, а Python-функция часто исполняется для каждой группы отдельно.
Официальное руководство предупреждает, что композиция встроенных GroupBy-операций обычно эффективнее пользовательской apply. Есть и инженерная причина выбирать узкую операцию: контракт формы заметен из кода. agg обещает сводку, transform — построчный результат, filter — отбор групп. У произвольной функции нужно отдельно проверить индекс, колонки, типы и поведение на пустой части.
Взвешенное среднее хорошо показывает, что краткость не должна скрывать знаменатель:
weighted = sales.assign(weighted_value=sales["quantity"] * sales["revenue"])
totals = weighted.groupby("store", dropna=False).agg(
numerator=("weighted_value", "sum"),
denominator=("quantity", "sum"),
)
totals["weighted_mean"] = totals["numerator"] / totals["denominator"]В предметной задаче нужно сначала уточнить, имеет ли смысл взвешивать выручку количеством: возможно, revenue уже является суммой строки и такое умножение ошибочно. GroupBy корректно выполнит заданную формулу, но не проверит её смысл.
Ключи нуждаются в том же аудите, что и измеряемые колонки.
"Москва", "москва" и "Москва " образуют разные группы без нормализации.dropna.observed влияет на присутствие ненаблюдавшихся комбинаций.Нормализация ключей должна быть явной и обратимой настолько, насколько требует аудит. Безусловное приведение строки к lower case может ошибочно объединить идентификаторы, для которых регистр значим. Сначала фиксируют предметную эквивалентность, потом кодируют её.
Параметр sort=False может избежать сортировки ключей и сохранить порядок их появления в конкретном входе, но бизнес-отчёт не должен зависеть от случайного порядка загрузки. Если порядок является частью результата, его задают финальным sort_values с явными колонками и правилами для пропусков.
Некоторые агрегаты можно безопасно считать частями и объединять. Для суммы складывают частичные суммы; для количества — частичные количества; среднее восстанавливают из пары «сумма, число», а не усредняют средние групп одинакового веса.
Медиану нельзя в общем случае получить как медиану медиан. Точный distinct count также не сводится к сумме distinct counts, если один объект встречается в нескольких частях. Эта граница важна при переходе от pandas в памяти к распределённой системе: синтаксически похожий groupby не гарантирует тот же алгоритм, порядок или точность.
Даже сумма чисел с плавающей точкой зависит от порядка сложения из-за округления. Разные разбиения могут дать немного отличающиеся младшие разряды. Для денежных величин обычно выбирают целые минимальные единицы или подходящий decimal-контракт, а допустимую погрешность научных вычислений фиксируют заранее.
Split–apply–combine организует вычисления, но не делает наблюдения независимыми и не устраняет смешение факторов. Средние по двум группам могут поменять отношение после разбиения по третьему фактору — классическая форма парадокса Симпсона. Поэтому аналитик обязан описать единицу наблюдения, знаменатель и возможные confounders.
Если один пользователь создал тысячу событий, а другой одно, среднее по событиям и среднее по пользователям отвечают на разные вопросы. Первый пользователь получает больший вес в событийной агрегации. Правильный split может сначала собрать показатель пользователя, а затем сравнить пользователей — но только если именно пользователь является единицей вывода.
Для проверки отчёта полезны инварианты:
Такие проверки обнаруживают не все смысловые ошибки, но превращают предположения combine-этапа в исполняемые утверждения.
Статья Уикхема вводит и демонстрирует стратегию на пакете plyr для R. Она не доказывает, что современный pandas реализует каждую операцию тем же алгоритмом или с той же производительностью. Поведение конкретных параметров нужно проверять по актуальной официальной документации и на зафиксированной версии.
Split–apply–combine не гарантирует ускорения и не означает параллельное выполнение. pandas может использовать оптимизированные пути для встроенных агрегатов, но DataFrame в памяти ограничен доступной памятью, а пользовательская функция способна стать узким местом.
Схема не доказывает статистическую причинность. Корректно посчитанная разница групп остаётся описательной, если дизайн данных не позволяет исключить альтернативные объяснения. GroupBy не выбирает правильную единицу анализа и не исправляет selection bias.
Наконец, совпадение с SQL GROUP BY на простом примере не является полной семантической эквивалентностью. Пропуски, типы, порядок, категориальные значения и форма результата задаются средой. Надёжный анализ начинается с явного контракта split, apply и combine, а заканчивается проверкой сохранения строк, знаменателей и схемы.
Самостоятельный редакционный разбор ЯдроКода по работе Hadley Wickham, статье Wes McKinney и официальной документации pandas. Идеи пересказаны своими словами, код создан редакцией.
Примеры DataFrame, таблицы, формулировки инвариантов и весь объяснительный текст созданы редакцией; материалы источников не копируются.