groupby в Pandas — полный гайд с примерами
Коротко
groupby — аналог GROUP BY из SQL в pandas. Разбивает DataFrame на группы по значениям столбца, после чего к каждой группе применяется агрегация, трансформация или произвольная функция. На собеседованиях аналитиков groupby спрашивают почти всегда — и в теории, и в live-coding.
Принцип работы: split — apply — combine. Сначала данные разбиваются на группы, затем к каждой группе применяется функция, потом результаты собираются обратно.
Базовый синтаксис
import pandas as pd
df = pd.DataFrame({
'city': ['Москва', 'Питер', 'Москва', 'Казань', 'Питер', 'Москва'],
'category': ['еда', 'еда', 'транспорт', 'еда', 'транспорт', 'еда'],
'amount': [500, 300, 150, 200, 100, 450]
})
# Сумма по городам
df.groupby('city')['amount'].sum()
# city
# Казань 200
# Москва 1100
# Питер 400
# Несколько агрегаций сразу
df.groupby('city')['amount'].agg(['sum', 'mean', 'count'])groupby() сам по себе ничего не считает — он возвращает объект DataFrameGroupBy. Результат появляется только после вызова агрегирующей функции: sum(), mean(), count(), max(), min(), agg().
Агрегация: agg()
agg() — самый гибкий способ агрегации. Позволяет применять разные функции к разным столбцам и задавать имена результатов.
# Разные функции к одному столбцу
df.groupby('city')['amount'].agg(['sum', 'mean', 'max'])
# Разные функции к разным столбцам
df.groupby('city').agg(
total=('amount', 'sum'),
avg_amount=('amount', 'mean'),
categories=('category', 'nunique')
)
# total avg_amount categories
# city
# Казань 200 200.0 1
# Москва 1100 366.7 2
# Питер 400 200.0 2Named aggregation через кортежи (столбец, функция) — самый чистый синтаксис. Его и стоит использовать на собеседовании.
Можно передавать и собственные функции:
df.groupby('city')['amount'].agg(lambda x: x.quantile(0.75))transform vs agg
Ключевое различие: agg() возвращает сжатый результат (одна строка на группу), а transform() возвращает Series той же длины, что исходный DataFrame. Это позволяет добавить агрегат обратно в каждую строку — без merge.
# agg — одна строка на группу
df.groupby('city')['amount'].sum()
# Казань 200
# Москва 1100
# Питер 400
# transform — значение в каждой строке
df['city_total'] = df.groupby('city')['amount'].transform('sum')
# city category amount city_total
# 0 Москва еда 500 1100
# 1 Питер еда 300 400
# 2 Москва транспорт 150 1100
# ...
# Доля каждой покупки от суммы по городу
df['share'] = df['amount'] / df.groupby('city')['amount'].transform('sum')Типичная задача на собеседовании: «Посчитайте долю каждого заказа от общей суммы по клиенту». Решается именно через transform.
apply
apply() для GroupBy принимает функцию, которая получает подтаблицу (DataFrame группы) и возвращает скаляр, Series или DataFrame. Используйте, когда логика слишком сложная для agg или transform.
# Топ-2 покупки в каждом городе
df.groupby('city').apply(lambda g: g.nlargest(2, 'amount')).reset_index(drop=True)
# Кастомная метрика: разница между макс и мин
df.groupby('city')['amount'].apply(lambda x: x.max() - x.min())apply медленнее agg и transform, потому что не использует внутренние оптимизации pandas. Используйте его только когда стандартных агрегаций недостаточно.
Группировка по нескольким столбцам
# Группировка по двум столбцам
result = df.groupby(['city', 'category'])['amount'].sum()
# city category
# Казань еда 200
# Москва еда 950
# транспорт 150
# Питер еда 300
# транспорт 100
# Результат — MultiIndex. Чтобы получить обычный DataFrame:
result = df.groupby(['city', 'category'])['amount'].sum().reset_index()
# Или через as_index=False
result = df.groupby(['city', 'category'], as_index=False)['amount'].sum()reset_index() после groupby — хорошая привычка. Без него дальнейшие операции с MultiIndex могут быть неудобными.
Практические примеры
Retention по когортам
# Считаем retention: доля пользователей, вернувшихся через 7 дней
events['cohort'] = events.groupby('user_id')['date'].transform('min')
events['day_diff'] = (events['date'] - events['cohort']).dt.days
retention = (
events[events['day_diff'] <= 7]
.groupby(['cohort', 'day_diff'])['user_id']
.nunique()
.reset_index()
.pivot(index='cohort', columns='day_diff', values='user_id')
)
# Делим на размер когорты (day_diff=0)
retention = retention.div(retention[0], axis=0)Выручка по сегментам
revenue = (
orders
.groupby('segment')
.agg(
total_revenue=('amount', 'sum'),
avg_check=('amount', 'mean'),
orders_count=('order_id', 'count'),
users=('user_id', 'nunique')
)
)
revenue['arpu'] = revenue['total_revenue'] / revenue['users']Конверсия по этапам воронки
funnel = (
events
.groupby('step')['user_id']
.nunique()
.reset_index(name='users')
.sort_values('users', ascending=False)
)
funnel['conversion'] = funnel['users'] / funnel['users'].iloc[0]Типичные ошибки
Забыть reset_index(). После groupby результат имеет индекс по группирующему столбцу. Если потом фильтровать или мержить — будут ошибки.
Использовать apply вместо transform. Если нужно добавить агрегат в каждую строку, transform работает в десятки раз быстрее.
Группировать по столбцу с NaN. По умолчанию pandas отбрасывает строки с NaN в группирующем столбце. Чтобы сохранить их как отдельную группу, передайте dropna=False:
df.groupby('city', dropna=False)['amount'].sum()Путать count и size. count() не считает NaN, size() считает все строки в группе, включая NaN.
Вопросы с собеседований
Как работает groupby под капотом? Принцип split-apply-combine: данные разбиваются на группы по ключу, к каждой группе применяется функция, результаты объединяются. Объект GroupBy ленивый — вычисления запускаются только при вызове агрегации.
В чём разница между transform и agg?
agg возвращает одну строку на группу (сжатие). transform возвращает результат той же длины, что исходный DataFrame — удобно для добавления агрегата обратно к строкам без join.
Как посчитать долю каждой строки от суммы по группе?
df['share'] = df['amount'] / df.groupby('group_col')['amount'].transform('sum'). Без transform пришлось бы делать отдельный groupby, а потом merge.
Чем отличается count от size в groupby?
count() считает только не-NaN значения для каждого столбца. size() считает все строки в группе, включая NaN. Ещё: size() возвращает Series, а count() — DataFrame.
Когда использовать apply вместо agg? Когда нужна логика, которую нельзя выразить стандартными агрегациями: например, достать top-N строк из каждой группы или применить сложную функцию, которая зависит от нескольких столбцов сразу.
Потренируйтесь решать задачи — откройте тренажёр с 1500+ вопросами для подготовки к собеседованиям аналитиков.
FAQ
Как убрать MultiIndex после groupby?
Используйте reset_index() после агрегации или передайте as_index=False прямо в groupby(). Второй вариант чище.
Можно ли группировать по вычисляемому выражению?
Да. Можно передать Series, функцию или массив вместо имени столбца: df.groupby(df['date'].dt.month)['amount'].sum() — группировка по месяцу.
Как отфильтровать группы по условию?
Используйте filter(): df.groupby('city').filter(lambda g: g['amount'].sum() > 500) — оставит только города, где сумма покупок больше 500. Возвращает строки исходного DataFrame, а не агрегат.
Где потренировать groupby на реальных задачах?
В тренажёре Карьерник — задачи по pandas из реальных собеседований. Также смотрите шпаргалку по pandas, map vs apply и примеры вопросов.