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 * 100

6. Топ-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. Это вопрос, который задают интервьюеры: «а как ускорить этот код?»

Читайте также

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.