Семестр 3 · Модуль 2 · Неделя 6

Группировка и агрегация

groupby · agg · pivot_table · crosstab · merge/join/concat · cut/qcut

2 ч теории. Презентация — 30 мин мини-лекция, далее live-coding и задачи семинара.

Прежде чем начать: вы умеете чистить данные. Сегодня научитесь задавать им вопросы: «какая категория приносит больше всего», «как выручка меняется по месяцам» — и собирать ответ в таблицы-отчёты.

Зачем вам группировка на 2–3 курсе

Какой вопрос о ваших данных вы зададите первым делом, если у вас есть таблица продаж за год?

groupby: паттерн split-apply-combine

Любая группировка — это три шага (одна команда делает все три):

ШагЧто происходит
Splitтаблица разрезается на группы по значениям колонки
Applyк каждой группе применяется функция (mean/sum/…)
Combineрезультаты склеиваются обратно в таблицу
sales.groupby('category')['revenue'].sum() category Книги 90200 Одежда 339500 Электроника 3146000 Name: revenue, dtype: int64
Что вернёт `df.groupby('col')` сам по себе — таблицу или специальный объект?

groupby возвращает GroupBy-объект

Забывают присвоить результат — главная ошибка недели:

sales.groupby("category") # GroupBy-объект, НЕ таблица sales.groupby("category").mean() # <- вот теперь результат
sales.groupby('category')['revenue'].mean() category Книги 5638.0 Одежда 16167.0 Электроника 136783.0 Name: revenue, dtype: float64

`.size()` — число строк в группе, `.count()` — непустых значений по колонке; они различаются при пропусках.

Почему `sales.groupby('category').mean()` печатает таблицу, хотя сам по себе `groupby` — объект?

agg: разные колонки — разные функции

sales.groupby('category').agg({ 'price': 'mean', 'qty': 'sum', 'product': 'nunique', }) price qty product category Книги 1781.25 51 4 Одежда 5642.86 62 4 Электроника 45217.39 68 4

Когда нужны разные метрики по разным колонкам — agg({'col': 'fun'}).

Что делает `agg({'price': 'mean', 'qty': 'sum'})`? Зачем словарь, а не просто `.mean()`?

pivot_table — двумерная сводка

«Месяц × категория → выручка»: у `groupby` один ключ на ось, у `pivot_table` — строки (index) И столбцы (columns):

pd.pivot_table(sales, index='month', columns='category', values='revenue', aggfunc='sum') category Книги Одежда Электроника month 4 5400.0 42000.0 1050000.0 7 12900.0 115500.0 116000.0

NaN — в месяце не было продаж этой категории. `margins=True` добавит итоги.

Чем `pivot_table` отличается от `groupby`? Когда нужна именно сводная таблица?

crosstab — таблицы частот

Частоты/доли по двум категориальным колонкам (группировка без колонки значений):

pd.crosstab(sales['region'], sales['category']) pd.crosstab(sales['region'], sales['category'], normalize='index') category Книги Одежда Электроника region Москва 0.19 0.33 0.48

normalize='index' — доли по строкам (в пределах региона).

Что покажет `crosstab` с `normalize='index'`: абсолютные числа или доли по регионам?

Соединения: merge, join, concat

# merge — по ключу (как SQL JOIN) pd.merge(sales, regions, on='region', how='left')
# concat — склейка по осям pd.concat([feb, mar], ignore_index=True)

merge — соединяет по общему ключу (left/right/inner/outer); concat — просто склеивает по строкам/колонкам, без ключа. join — merge по индексу.

Что сделает `merge` с дубликатами ключей? А `concat` двух таблиц с разными колонками?

Ошибка недели: merge размножает строки

Если ключ встречается в обеих таблицах несколько раз — many-to-many merge перемножает строки:

pd.merge(a, b, on="region") # ключ с дубликатами -> строк стало больше!
print('до:', len(sales)) # 60 merged = pd.merge(sales, regions, on='region') print('после:', len(merged)) # 60, если regions без дубликатов

Правило: проверяй `.shape` до и после merge и уникальность ключей в обеих таблицах.

Почему число строк после merge может вырасти? Как это проверить?

Бинирование: cut и qcut

Превратить непрерывный признак в категории (сегменты чеков, возрастные группы):

# cut: равные интервалы pd.cut(sales['revenue'], 4)
# qcut: равные частоты pd.qcut(sales['revenue'], 4)

cut — диапазоны равной ширины; qcut — в каждой группе примерно равное число наблюдений (квантили).

Для чека от 100 до 1000 руб.: в каком случае в сегменте «дорогие» может оказаться 1 покупка — cut или qcut?

Собираем EDA: одна строка заменяет цикл

sales['revenue'] = sales['price'] * sales['qty'] sales.groupby('product')['revenue'].sum().nlargest(5) product Ноутбук 1980000 Смартфон 750000 Монитор 280000 Куртка 153000 Наушники 136000 Name: revenue, dtype: int64

Это и есть «векторное мышление»: то, что на 1–2 курсе делалось циклом по строкам, здесь — одна команда.

Как посчитать выручку по категориям и месяцам одним выражением (подсказка: `groupby` с двумя ключами)?

Типичные ошибки недели 6

  1. Забывают присвоить: df.groupby(...) без присваивания — группа не «применилась».
  2. Мутация в цикле: df.groupby(...).agg(...) в цикле копит предупреждения.
  3. Merge с дубликатами ключей — внезапно разросшийся DataFrame.
  4. Путаница merge vs concat (по ключу vs по позициям).
  5. Непонимание, зачем `agg` с dict, когда все колонки агрегируются по-разному.
Студент сделал `df.groupby('category').agg({'price': 'mean'})` и не понимает, почему колонка `qty` пропала. Что объяснить?

Вопросы для проверки понимания

  1. Чем `merge` отличается от `concat` и когда что использовать?
  2. Как посчитать выручку по категориям и месяцам одним выражением?
  3. Что делает `agg({'price': 'mean', 'qty': 'sum'})`?
  4. Чем `cut` отличается от `qcut`?

Ответьте себе письменно за 2 минуты — это и есть мини-самооценка перед семинаром.

Переход: на семинаре вы проанализируете продажи и соберёте отчёт «месяц × категория» — это финальный навык модуля.

Что дальше

Готовы? Откройте семинар-6.md и sales.csv.