Задачи на агрегацию в pandas

Проверь себя · 1/3разбор после ответа
В цикле по списку заказов вы хотите пропустить невалидную запись и продолжить обработку остальных. Какую инструкцию использовать внутри цикла?

Зачем эти задачи

Группировка — ежедневная работа аналитика в 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)
Закрепи Python для аналитика
200+ задач по pandas, numpy и работе с данными — с разборами
Тренировать Python в Telegram

Задача 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+ вопросами для собесов.