Pandas groupby и merge: как собрать аналитический отчёт из таблиц
Практический разбор groupby, merge и pivot_table в pandas: как агрегировать заказы, присоединять справочники и не посчитать выручку дважды.
Содержание статьи
В рабочей папке редко лежит одна идеальная таблица. Заказы находятся отдельно, пользователи — в справочнике, а маркетинговый канал приходит ещё одним файлом. В pandas аналитический отчёт обычно собирается в три шага: понять зерно каждой таблицы, агрегировать факты и только потом соединить их по ключу.
Коротко
groupby отвечает на вопрос “как посчитать показатель по группам”. merge соединяет таблицы по ключу. pivot_table превращает длинный результат в компактную матрицу. Все три операции простые по синтаксису, но ошибки в зерне легко превращают 10 заказов в 100 или незаметно теряют пользователей.
- Перед операцией запиши, что означает одна строка в каждой таблице.
- Сначала агрегируй факты до нужного уровня, потом присоединяй справочники.
- Для контроля соединения проверяй количество строк и долю пропусков после merge.
- Не используй
sumдля показателя, который уже был агрегирован выше.
Начни с зерна таблиц
Представим учебный набор: в orders одна строка — один заказ, в order_items одна строка — одна товарная позиция, в users одна строка — один пользователь. Если присоединить users к order_items, строки не должны умножиться: для каждого user_id в справочнике должна быть максимум одна запись.
| Таблица | Одна строка | Ключ для соединения |
|---|---|---|
| orders | один заказ | order_id, user_id |
| order_items | одна позиция внутри заказа | order_id, product_id |
| users | один пользователь | user_id |
| products | один товар | product_id |
groupby: агрегируй факты по смысловой группе
В groupby сначала укажи измерение, затем выбери поля и агрегаты. Для продуктового отчёта часто нужны не только сумма выручки, но и количество заказов, уникальные покупатели и средний чек. Явные имена агрегатов читаются лучше, чем цепочка из нескольких промежуточных таблиц.
Пример показывает форму результата, а не реальные данные. Для решения добавь период и размер сегмента.
channel_report = (
orders.groupby('channel', as_index=False)
.agg(
orders=('order_id', 'nunique'),
buyers=('user_id', 'nunique'),
revenue=('revenue', 'sum'),
average_order=('revenue', 'mean'),
)
.sort_values('revenue', ascending=False)
)
channel_report['revenue_share'] = (
channel_report['revenue'] / channel_report['revenue'].sum()
)merge: соединяй таблицы после агрегации
Если тебе нужен канал в отчёте по заказам, сначала собери одну строку на заказ, а затем добавь атрибуты пользователя. how="left" сохраняет все строки левой таблицы и позволяет заметить пользователей, для которых справочник не нашёлся. validate="many_to_one" полезен как предохранитель: он упадёт, если справа ключ оказался неуникальным.
order_report = (
orders[['order_id', 'user_id', 'channel', 'revenue']]
.merge(
users[['user_id', 'plan', 'country']],
on='user_id',
how='left',
validate='many_to_one',
indicator=True,
)
)
missing_users = (
order_report['_merge'].eq('left_only').mean()
)Сравни количество строк до и после, долю _merge == left_only и уникальность ключа справа. Если число строк выросло неожиданно, это почти всегда сигнал о дубликатах в ключе или неверном зерне.
pivot_table: сделать отчёт читаемым
После группировки данные часто удобны для кода, но неудобны для просмотра. pivot_table раскладывает измерение по строкам и колонкам: например, выручку по каналам и планам. Это хороший последний шаг перед графиком или выгрузкой, но не замена проверке исходного зерна.
revenue_matrix = pd.pivot_table(
order_report,
index='channel',
columns='plan',
values='revenue',
aggfunc='sum',
fill_value=0,
)
revenue_matrix = revenue_matrix.sort_values('pro', ascending=False)Как не посчитать выручку дважды
Самая дорогая ошибка возникает, когда заказ присоединяют к таблице позиций, а затем суммируют сумму заказа. Один заказ с четырьмя позициями появится четыре раза, и revenue вырастет в четыре раза. Если нужна аналитика товаров, агрегируй order_items по order_id или используй сумму позиций, но не смешивай её с готовой суммой заказа.
- Сохрани контрольные числа: заказы, покупатели, выручка до соединения.
- После merge сравни
nunique(order_id)с исходным значением. - Для one-to-many заранее реши, нужен ли разворот на уровень позиции.
- Не заполняй пропуски нулём, пока не понял, означает ли пропуск отсутствие факта.
Проверяй cardinality до merge
У соединения есть кардинальность: one-to-one, many-to-one, one-to-many или many-to-many. В аналитическом отчёте чаще всего ожидается many-to-one: много заказов могут ссылаться на одного пользователя. Если справочник пользователей содержит две строки на user_id, merge тихо удвоит часть заказов. validate превращает такое предположение в проверку.
После соединения сравни не только количество строк, но и уникальные ключи, сумму денег и долю строк без совпадения. indicator=True показывает, какие записи пришли только слева или только справа. Это особенно важно для справочника каналов: неизвестный канал не должен исчезать из отчёта только потому, что в mapping нет ключа.
Если связь many-to-many действительно нужна, сначала объясни, почему. Часто это означает, что выбран неправильный уровень анализа. Для отчёта на уровне заказа сначала агрегируй позиции, а для отчёта на уровне товара не используй готовую сумму заказа как будто она относится к каждой позиции.
assert users['user_id'].notna().all()
assert users['user_id'].is_unique
order_report = orders.merge(
users[['user_id', 'plan']],
on='user_id',
how='left',
validate='many_to_one',
indicator=True,
)
print(order_report['_merge'].value_counts())Агрегируй на том уровне, на котором задаётся вопрос
Одна и та же таблица может дать разные ответы в зависимости от зерна. Средний чек считается на уровне заказа, средняя сумма позиции — на уровне строки товара, а ARPU — на уровне пользователя за период. Если сначала посчитать среднее по строкам, а потом усреднить группы, крупные и маленькие группы получат одинаковый вес. Для корректного общего среднего храни сумму и количество.
Перед groupby запиши строку результата словами: «одна строка — канал за месяц» или «одна строка — пользователь за день». После операции проверь уникальность этого ключа. Такая привычка делает код ближе к SQL и помогает заметить, что в отчёт случайно попал ещё один dimension.
Сортировка и pivot_table должны быть последним слоем. Не используй красивую широкую матрицу как источник следующего расчёта, если можно работать с длинной таблицей. Длинный формат проще проверять, соединять и строить из него несколько визуализаций.
| Показатель | Одна строка результата | Что агрегировать |
|---|---|---|
| средний чек | канал и день | сумма заказов / число заказов |
| ARPU | канал и месяц | выручка / уникальные пользователи |
| товарная выручка | товар и месяц | сумма позиций |
| активность | день | уникальные пользователи |
Как ловить размножение строк на маленьком примере
Если merge выглядит подозрительно, не отлаживай сразу на миллионах строк. Возьми один проблемный order_id, выгрузи все строки слева и справа и покажи, сколько комбинаций образовалось. В маленьком примере сразу видно, что справочник содержит повторный user_id или таблица позиций была присоединена к фактам заказа без агрегации.
Полезно сохранить контрольные числа до соединения: число заказов, уникальных покупателей и revenue. После each merge проверяй те же показатели. Если цель — добавить атрибут, сумма заказа обычно не должна измениться. Если она изменилась, это не обязательно ошибка, но причина должна быть ясна и записана.
Для сложной цепочки делай промежуточные имена вроде orders_with_users, order_items_by_order и report_by_channel. Один длинный вызов с четырьмя merge экономит строки, но усложняет ревью и скрывает место, где поменялось зерно.
Если ты присоединяешь только план пользователя, количество заказов и сумма до и после merge должны совпасть. Исключение — осознанный фильтр или изменение уровня данных, которое нужно объяснить отдельно.
Практика: отчёт по каналам без двойного счёта
Собери тестовый отчёт в четыре шага: очисти справочник пользователей, агрегируй позиции до заказа, присоедини канал и собери итог по каналу и месяцу. На каждом шаге зафиксируй зерно и контрольные числа. Такой порядок кажется длиннее, но позволяет переиспользовать промежуточный слой для среднего чека, количества покупателей и retention.
Отдельно посмотри на пользователей без канала. Не выбрасывай их до того, как оценишь долю. Если unknown занимает 12% выручки, это не косметическая проблема группировки, а вопрос качества источника и полноты атрибуции.
В конце сравни отчёт с независимой суммой по исходным заказам. Если общий revenue не совпал, разбери разницу по шагам. Пока контрольное число не объяснено, не переходи к красивой диаграмме и бизнес-выводу.
- Проверить уникальность ключей справочников.
- Определить зерно результата до groupby.
- Агрегировать one-to-many до нужного уровня.
- Сравнить строки, ключи и суммы после каждого merge.
- Показать unknown отдельно от нулевых значений.
Материалы по теме

Как тестировать аналитические расчёты на Python и pandas
Практический гайд по тестам для аналитика: проверить метрики на маленьком датасете, поймать регрессию и защитить расчёт от тихих изменений.

Как ускорить pandas: память, типы и обработка больших файлов
Что делать, если pandas медленно работает или не помещает файл в память: категории, downcast, chunksize, Parquet и контроль размера данных.

Pandas apply и векторизация: как писать быстрее и понятнее
Когда использовать apply, почему векторные операции быстрее и как переписать медленный построчный расчёт в pandas.