Задачи на агрегацию в pandas
Зачем эти задачи
Группировка — ежедневная работа аналитика в pandas: сумма по категориям, среднее по когортам, процентили по сегментам, накопительные итоги для отчётов. Связка groupby + agg / transform / apply — одна из тем, которую почти наверняка спросят на собесе по Python.
На собеседованиях любят проверять нюансы: чем отличается agg от transform (первый возвращает по одной строке на группу, второй — столбец длиной со всю таблицу), когда apply работает медленно по сравнению со встроенными функциями, как применить разные агрегации к разным колонкам, как отфильтровать группы по условию через groupby().filter. За кодом интервьюер смотрит, понимаете ли вы, что именно возвращает каждая операция и во что она разворачивается — а не просто зазубрили синтаксис.
Ниже — 12 задач с решениями на типовом датасете (user_id, category, amount, date). От простых groupby().sum() до продвинутых паттернов: топ-N в группе, накопительный итог, доля в категории через transform, условная агрегация.
Dataset
import pandas as pd
df = pd.DataFrame({
'user_id': [1, 1, 2, 2, 3, 3, 3],
'category': ['A', 'B', 'A', 'A', 'B', 'B', 'A'],
'amount': [100, 200, 150, 250, 300, 100, 200],
'date': pd.to_datetime(['2026-01-01', '2026-01-02', '2026-01-01',
'2026-02-01', '2026-02-01', '2026-03-01', '2026-03-02'])
})Задача 1. Сумма по пользователю
Базовый groupby: разбить строки на группы по user_id и просуммировать amount в каждой. Результат — Series, где индекс это user_id.
df.groupby('user_id')['amount'].sum()Задача 2. Количество заказов на пользователя
size() считает все строки группы, включая пустые значения; count() по колонке считает только непустые. Если в amount могут быть NaN — разница важна.
df.groupby('user_id').size()
# или
df.groupby('user_id')['amount'].count()Задача 3. Несколько агрегатов одновременно
Именованная агрегация (named aggregation) — самый читаемый способ сразу посчитать несколько метрик и сразу задать имена колонок. Каждый кортеж это (колонка, функция).
df.groupby('user_id').agg(
total=('amount', 'sum'),
avg=('amount', 'mean'),
count=('amount', 'count')
)Задача 4. Средний amount по (user_id, category)
Группировать можно сразу по нескольким колонкам — тогда в индексе результата будет MultiIndex (user_id, category).
df.groupby(['user_id', 'category'])['amount'].mean()Задача 5. Pivot по месяцу × категории
pivot_table разворачивает данные в широкий формат: по строкам — месяцы, по столбцам — категории, в ячейках — сумма. fill_value=0 заполняет пустые пересечения нулями, чтобы не было NaN.
df['month'] = df['date'].dt.to_period('M')
df.pivot_table(
values='amount',
index='month',
columns='category',
aggfunc='sum',
fill_value=0
)Задача 6. Пользователи с 2+ заказами
Два способа. Через isin мы сначала считаем размеры групп, затем оставляем строки нужных пользователей. Через groupby().filter — короче: фильтр возвращает все строки тех групп, где условие истинно.
counts = df.groupby('user_id').size()
df[df['user_id'].isin(counts[counts >= 2].index)]
# или через filter — компактнее
df.groupby('user_id').filter(lambda g: len(g) >= 2)Задача 7. Топ-2 заказа в каждой категории
Сортируем внутри категории по убыванию суммы и берём первые две строки каждой группы через head(2). Это питоновский аналог ROW_NUMBER() ... QUALIFY <= 2 из SQL.
df.sort_values(['category', 'amount'], ascending=[True, False]) \
.groupby('category') \
.head(2)Задача 8. Накопительный итог (running total) в группе
cumsum внутри группы даёт нарастающую сумму. Критично сначала отсортировать по дате — иначе «накопительный» итог накопится в случайном порядке строк и будет бессмысленным.
df['running_total'] = df.sort_values('date') \
.groupby('user_id')['amount'].cumsum()Задача 9. Доля пользователя в категории
Здесь нужен transform, а не agg: transform('sum') возвращает сумму по категории, «размазанную» обратно по всем строкам (длина совпадает с исходным df). Поэтому её можно поэлементно поделить на amount. Это ключевое отличие transform от agg и любимый вопрос на собесе.
df['share_in_category'] = df['amount'] / \
df.groupby('category')['amount'].transform('sum')Задача 10. Первое и последнее значение в группе
df.groupby('user_id').agg(
first_date=('date', 'min'),
last_date=('date', 'max'),
first_amount=('amount', 'first'),
last_amount=('amount', 'last')
)Важный подвох: first / last берут первую и последнюю строку в текущем порядке данных, а не самую раннюю и позднюю по дате. Чтобы получить именно хронологически первый и последний заказ, сначала отсортируйте по дате:
df.sort_values('date').groupby('user_id').first()
df.sort_values('date').groupby('user_id').last()Задача 11. Своя функция агрегации (custom aggregation)
Если встроенной функции не хватает, в agg можно передать любую свою — она получает Series значений группы и возвращает одно число. Здесь считаем размах (максимум минус минимум).
def range_func(s):
return s.max() - s.min()
df.groupby('user_id')['amount'].agg(range_func)Задача 12. Условная агрегация (conditional aggregation)
Посчитать, сколько у каждого пользователя заказов дороже 100. Вариант через apply работает, но на больших данных он медленный, потому что вызывает питон-функцию для каждой группы. Вариант со вспомогательной булевой колонкой векторизован и обычно заметно быстрее.
# читаемо, но медленно на больших данных
df.groupby('user_id').apply(lambda g: (g['amount'] > 100).sum())
# векторизованный вариант — быстрее
df.assign(over_100=df['amount'] > 100).groupby('user_id')['over_100'].sum()Частые ошибки
- Забыть выбрать колонку после
groupby. Без явной колонки агрегация применится ко всем числовым столбцам сразу — это медленно и часто даёт неожиданные лишние колонки в результате. - Путать
transformиagg.aggвозвращает по строке на группу,transform— столбец длиной со всю таблицу. Если результат нужно записать обратно вdfкак новую колонку, нуженtransform. - Медленный
apply. Для простых операций (сумма, среднее, счётчик по условию) встроенные и векторизованные методы в разы быстрее питон-лямбды вapply.applyоставляют только для логики, которую иначе не выразить. - Забыть
reset_index. Послеgroupbyключи уезжают в индекс (иногда MultiIndex), и дальнейшие join или запись в файл начинают путаться. Часто удобнее сразуgroupby(..., as_index=False)илиreset_index(). - Не учесть, что
groupbyпо умолчанию выкидывает группы сNaNв ключе. Если в колонке группировки есть пропуски, эти строки молча исчезнут из результата — при необходимости передавайтеdropna=False.
Связанные темы
FAQ
groupby или pivot_table?
groupby — универсальный инструмент для агрегаций в «длинном» формате: одна метрика, результат в виде Series или узкого DataFrame. pivot_table удобнее, когда нужен «широкий» формат — метрика на пересечении двух измерений (строки × столбцы), плюс он умеет margins для итогов. По сути pivot_table — это обёртка над groupby с последующим unstack.
Чем transform отличается от agg?
agg схлопывает каждую группу в одну строку (длина результата равна числу групп). transform возвращает значение для каждой исходной строки, сохраняя её позицию (длина результата равна длине df). Поэтому долю от суммы группы, z-score внутри группы или заполнение пропусков средним по группе делают именно через transform.
Почему apply работает медленно?
apply с питон-функцией вызывает эту функцию по одной на каждую группу и не векторизуется. На тысячах групп это заметно медленнее встроенных агрегаций (sum, mean, count), которые работают на уровне C. Если задачу можно выразить встроенной функцией или булевой маской — берите их, apply оставляйте на нестандартную логику.
Как применить разные агрегации к разным колонкам?
Через именованную агрегацию: df.groupby('user_id').agg(total=('amount','sum'), users=('user_id','nunique')). Каждый аргумент — это имя_колонки=(исходная_колонка, функция). Так вы одновременно считаете разные метрики по разным полям и сразу задаёте понятные имена столбцов.
Как посчитать долю группы от общего итога?
Разделите значение на сумму по группе, полученную через transform('sum'): df['share'] = df['amount'] / df.groupby('category')['amount'].transform('sum'). transform возвращает сумму, «размазанную» обратно по всем строкам, поэтому деление идёт поэлементно и результат — доля каждой строки внутри своей категории.
Тренируйте Python для аналитики — откройте тренажёр с 1500+ вопросами для собесов.