groupby и split-apply-combine
groupby — это не одна операция, а схема из трёх шагов: разбить данные на группы, применить функцию к каждой, собрать результат.
split-apply-combine — модель группировки: split делит строки на группы по ключу, apply применяет функцию к каждой группе, combine склеивает результаты в одну таблицу.
Идея split-apply-combine
Почти любой вопрос вида «посчитать что-то в разрезе чего-то» — это группировка. «Средний чек по городам», «сумма продаж по месяцам», «число заказов на клиента» — все они укладываются в три шага:
- Split — разбить строки на группы по значению ключа (город, месяц, клиент).
- Apply — внутри каждой группы посчитать агрегат (сумму, среднее, количество).
- Combine — собрать по одному результату на группу в итоговую таблицу, где ключ группы становится индексом.
В pandas всё это пишется в одну строку:
df.groupby("город")["чек"].mean()
# ^split^ ^что^ ^apply^ → combine происходит автоматически
Реализуем split-apply-combine на чистом Python
Лучший способ понять groupby — собрать его руками. Это ровно то, что pandas делает внутри, только на C и для целых массивов:
orders = [
{"город": "Москва", "чек": 1200},
{"город": "Сочи", "чек": 800},
{"город": "Москва", "чек": 1800},
{"город": "Сочи", "чек": 1000},
{"город": "Москва", "чек": 600},
]
# SPLIT: раскладываем строки по группам (ключ → список значений)
groups = {}
for o in orders:
groups.setdefault(o["город"], []).append(o["чек"])
# APPLY + COMBINE: для каждой группы считаем средний чек
for city in sorted(groups):
cheks = groups[city]
print(city, "->", sum(cheks) / len(cheks))
Вывод:
Москва -> 1200.0 Сочи -> 900.0
Словарь groups — это шаг split: ключ города → список чеков. Цикл с делением — apply (среднее в каждой группе). Печать по одной строке на город — combine. df.groupby("город")["чек"].mean() делает то же самое и возвращает Series, где индекс — города, а значения — средние чеки.
Ленивость: groupby ничего не считает сразу
Важная деталь: df.groupby("город") сам по себе не вычисляет ничего — он возвращает объект GroupBy, который лишь «знает», как разбить данные. Реальная работа происходит, только когда вы вызываете агрегат (.mean(), .sum(), .agg(...)). Это позволяет переиспользовать одну группировку для разных подсчётов:
g = df.groupby("город") # ленивый объект, вычислений нет
g["чек"].mean() # вот теперь считается среднее
g["чек"].sum() # и здесь — сумма, по той же разбивке
g.size() # число строк в каждой группе
Итерация по группам
Объект GroupBy можно перебрать в цикле — он отдаёт пары «ключ группы, под-DataFrame». Это полезно для отладки или нестандартной обработки (но для агрегатов всегда предпочитайте встроенные методы — они быстрее):
for city, group_df in df.groupby("город"):
print(city, len(group_df)) # имя группы и сколько в ней строк
Частые агрегаты и группировка по нескольким ключам
df.groupby("город")["чек"].sum() # сумма
df.groupby("город")["чек"].count() # число НЕпустых значений
df.groupby("город").size() # число строк (включая пустые)
df.groupby("город")["чек"].agg(["min", "max", "mean"]) # несколько сразу
df.groupby(["город", "месяц"])["чек"].sum() # по двум ключам → MultiIndex
Группировка по нескольким ключам даёт иерархический индекс (MultiIndex) — каждая уникальная пара (город, месяц) становится отдельной группой. Различие count() (не считает NaN) и size() (считает все строки) — частый источник расхождений в отчётах.
as_index и reset_index: ключ как индекс или столбец
По умолчанию ключ группировки уходит в индекс результата. Это удобно для дальнейшего выравнивания, но неудобно для выгрузки в файл или график, где хочется обычный столбец. Два способа вернуть его столбцом:
df.groupby("город", as_index=False)["чек"].sum() # сразу столбцом
df.groupby("город")["чек"].sum().reset_index() # превратить индекс в столбец после
Оба дают одинаковый плоский результат. Выбор стиля — дело вкуса, но важно осознавать, где у вас ключ: в индексе или в столбце. От этого зависит, как обращаться к результату дальше.
Прямая аналогия с SQL GROUP BY
Кто знает SQL, освоит groupby мгновенно: это тот же GROUP BY. Сравним на живой SQLite-песочнице:
CREATE TABLE orders (city TEXT, chek INTEGER);
INSERT INTO orders VALUES
('Москва', 1200), ('Сочи', 800), ('Москва', 1800),
('Сочи', 1000), ('Москва', 600);
SELECT city, AVG(chek) AS avg_chek, COUNT(*) AS n
FROM orders
GROUP BY city
ORDER BY city;
Вывод:
Москва|1200.0|3 Сочи|900.0|2
Соответствие почти дословное: GROUP BY city ≈ df.groupby("город"), AVG(chek) ≈ ["чек"].mean(), COUNT(*) ≈ .size(), ORDER BY ≈ сортировка индекса. pandas даёт ровно те же средние (1200 и 900), что и наш ручной Python выше — модель split-apply-combine универсальна.
Где pandas гибче SQL? В том, что между «применить» и «собрать» вы не ограничены набором встроенных агрегатов. Можно подсунуть любую функцию, в том числе свою, вернуть несколько значений на группу, или вообще не агрегировать, а преобразовать каждую группу (об этом — следующий урок про transform). SQL для такого требует оконных функций и подзапросов, а в pandas это один вызов. Обратная сторона — pandas работает в памяти одной машины, тогда как СУБД считает группировку по терабайтам на диске. Поэтому на практике часто комбинируют: тяжёлую первичную агрегацию делают в SQL, а тонкую доводку — в pandas.
Подводные камни
- count() против size().
count()игнорируетNaNпо столбцу,size()считает все строки группы. В отчётах это разные числа. - Ключ группировки уходит в индекс. Результат имеет город индексом, а не столбцом; чтобы вернуть его столбцом, добавьте
reset_index()илиas_index=False. - NaN в ключе группировки. Строки с пропуском в ключе по умолчанию выпадают из группировки — это легко не заметить. Управляется параметром
dropna=False. - Не итерируйте ради агрегатов. Цикл
for ... in groupbyчитаем, но медленный; встроенные.sum()/.agg()на порядки быстрее.
Лучшие практики
- Формулируйте задачу как «что считаем в разрезе чего» — и она почти всегда ложится на
groupby. - Сохраняйте ленивый объект
g = df.groupby(...), если нужно несколько разных агрегатов по той же разбивке. - Помните про
reset_index()/as_index=False, когда ключ нужен как обычный столбец. - Если знаете SQL — переносите интуицию GROUP BY напрямую.
Итог
- groupby = split (разбить) + apply (посчитать) + combine (собрать).
- Объект GroupBy ленив: считает только при вызове агрегата.
- Ключ группировки становится индексом результата.
- Это прямой аналог SQL GROUP BY.