Groupby в Pandas: шпаргалка для собеседования
Зачем аналитику groupby в Pandas
Если SQL-ный GROUP BY вы знаете вдоль и поперёк — в pandas сюрпризы всё равно будут. Особенно с transform и apply.
Groupby в pandas мощнее SQL-го: он позволяет не только агрегировать, но и обогащать исходные строки, фильтровать группы целиком, применять произвольные функции. На собесе от middle-аналитика ждут владения всеми этими сценариями.
Базовый синтаксис
df.groupby('column').agg_func()
df.groupby(['col1', 'col2']).agg_func()
# Разные агрегаты для разных колонок
df.groupby('city').agg({
'amount': 'sum',
'user_id': 'nunique'
})Основные агрегаты
# Один агрегат
df.groupby('city')['amount'].sum()
df.groupby('city')['amount'].mean()
df.groupby('city')['user_id'].nunique()
df.groupby('city').size() # количество строк
df.groupby('city').count() # count по каждой колонке (не-NaN)
# Несколько агрегатов
df.groupby('city')['amount'].agg(['sum', 'mean', 'count'])
# С алиасами (named aggregation)
df.groupby('city').agg(
total=('amount', 'sum'),
avg=('amount', 'mean'),
users=('user_id', 'nunique')
)Named aggregation — современный pandas-way, читабельнее .agg({...}).
size() vs count() vs nunique()
Постоянный источник путаницы:
df.groupby('city').size()
# количество строк в каждой группе
df.groupby('city')['email'].count()
# количество НЕ-NaN email в каждой группе
df.groupby('city')['email'].nunique()
# количество УНИКАЛЬНЫХ email в каждой группеНа собесе путают size и count. Запомните: size — все строки, count — только не-NaN, nunique — уникальные не-NaN.
transform — обогащение строк
Возвращает Series той же длины, что и исходная — для колонки-агрегата по группе:
# Добавить к каждой строке среднее по городу
df['city_avg'] = df.groupby('city')['amount'].transform('mean')
# Доля заказа в общей сумме пользователя
df['pct_of_user'] = df['amount'] / df.groupby('user_id')['amount'].transform('sum')
# Ранг внутри группы
df['rank'] = df.groupby('user_id')['amount'].transform('rank', ascending=False)Отличие от .agg() — результат имеет тот же индекс, что и исходная таблица, можно напрямую присваивать в колонку.
Тренироваться на таких вопросах можно в Telegram-боте Карьерник — там 1500+ задач с реальных собесов с разборами.
filter — фильтрация целых групп
# Оставить только города с более 10 заказами
result = df.groupby('city').filter(lambda g: len(g) > 10)
# Только пользователи, у которых суммарная покупка > 10000
result = df.groupby('user_id').filter(lambda g: g['amount'].sum() > 10000)Отличие от обычного фильтра — оставляет все строки группы, которая прошла условие. Или выбрасывает всю группу.
apply — самое гибкое
# Самый большой заказ каждого пользователя
df.groupby('user_id').apply(lambda g: g.loc[g['amount'].idxmax()])
# Произвольная функция
def summary(g):
return pd.Series({
'n': len(g),
'sum': g['amount'].sum(),
'first': g.iloc[0]['created_at'],
'last': g.iloc[-1]['created_at']
})
df.groupby('user_id').apply(summary)Минус apply — медленный. Если можно решить через agg/transform — лучше через них.
multiindex — группировка по нескольким
df.groupby(['city', 'device'])['amount'].sum()
# Получаем Series с двойным индексом
df.groupby(['city', 'device'])['amount'].sum().unstack()
# Превращаем в матрицу: строки — города, столбцы — устройстваСброс индекса
После groupby индекс — группа. Если нужен plain DataFrame:
df.groupby('city')['amount'].sum().reset_index()
# city, amount как обычные колонкиИли сразу .reset_index() в цепочке.
Сортировка после groupby
# Сортировка по результату
(df.groupby('city')['amount'].sum()
.sort_values(ascending=False)
.head(10))
# В SQL-стиле через reset_index + sort_values
(df.groupby('city')
.agg(total=('amount', 'sum'))
.reset_index()
.sort_values('total', ascending=False))cumulative агрегаты
Аналог оконных функций SQL — считают нарастающий итог:
df['cumsum'] = df.groupby('user_id')['amount'].cumsum()
df['cummax'] = df.groupby('user_id')['amount'].cummax()
df['rank'] = df.groupby('user_id')['amount'].rank()К слову, набить руку на таких кейсах удобно через тренажёр в Telegram — разбирайте по 10 вопросов в день, через 2 недели тема становится рефлексом.
rolling по группам
# Скользящее среднее за 7 дней для каждого пользователя
df['ma_7d'] = (
df.sort_values('date')
.groupby('user_id')['amount']
.rolling(window=7)
.mean()
.values
)10 задач с собеседований
1. Топ-5 пользователей по выручке
(df.groupby('user_id')['amount'].sum()
.sort_values(ascending=False).head(5))2. Средний чек по городам с фильтром
(df.groupby('city')
.agg(avg=('amount', 'mean'), cnt=('amount', 'count'))
.query('cnt > 10')
.sort_values('avg', ascending=False))3. Количество уникальных товаров на пользователя
df.groupby('user_id')['product_id'].nunique()4. Первый и последний заказ пользователя
df.groupby('user_id').agg(
first_order=('created_at', 'min'),
last_order=('created_at', 'max')
)5. Доля каждой категории
total = df['amount'].sum()
df.groupby('category')['amount'].sum() / total * 1006. Топ-1 товар в каждой категории
idx = df.groupby('category')['sold'].idxmax()
df.loc[idx]7. Заполнить NaN средним по группе
df['amount'] = df.groupby('city')['amount'].transform(
lambda x: x.fillna(x.mean())
)8. Пользователи с retention D7
users_d0 = df[df['days_since_signup'] == 0]['user_id'].unique()
users_d7 = df[df['days_since_signup'] == 7]['user_id'].unique()
retention = len(set(users_d7) & set(users_d0)) / len(users_d0)9. Ранг заказов пользователя
df['order_num'] = df.groupby('user_id')['created_at'].rank(method='first')10. Когорты: сколько вернулось через N дней
df['cohort'] = df.groupby('user_id')['date'].transform('min')
df['days_since'] = (df['date'] - df['cohort']).dt.days
cohort_counts = df.groupby(['cohort', 'days_since'])['user_id'].nunique().unstack()
retention = cohort_counts.divide(cohort_counts[0], axis=0)Как тренироваться
Groupby в pandas — самая важная тема. Владение transform, filter, apply отличает middle от junior. Практикуйте на Kaggle-датасетах.
Тренажёр Карьерник содержит блок задач на pandas groupby: агрегаты, transform, filter, named aggregation, rolling. Каждая с разбором.
Совет: если apply медленный — почти всегда можно переписать через agg + transform. Это вопрос, который задают интервьюеры: «а как ускорить этот код?»
Читайте также
- Pandas шпаргалка для аналитика
- Groupby в pandas: полный гайд
- Задачи на Python на собеседовании
- GROUP BY SQL: шпаргалка
FAQ
agg или apply?
agg если возвращаете скаляры или фиксированный набор агрегатов. apply — для произвольной логики и возврата произвольной формы. agg быстрее.
transform или agg + merge?
transform проще и быстрее для обогащения строк групповым агрегатом. agg + merge — когда нужно сначала агрегировать в отдельный DataFrame, потом присоединить обратно.
Как сделать pivot после groupby?
Через .unstack() (если multiindex) или .pivot()/.pivot_table() сразу без groupby. pivot_table универсальнее.
Почему groupby медленный на больших данных?
Медленным его делают обычно apply с функциями Python. Векторизованные agg и transform быстрые. Для больших данных можно использовать Dask или Polars.