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

Pandas groupby и merge: как собрать аналитический отчёт из таблиц

Практический разбор groupby, merge и pivot_table в pandas: как агрегировать заказы, присоединять справочники и не посчитать выручку дважды.

КПКейсПрактика28 июля 2026 г.18 мин

В рабочей папке редко лежит одна идеальная таблица. Заказы находятся отдельно, пользователи — в справочнике, а маркетинговый канал приходит ещё одним файлом. В 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 сначала укажи измерение, затем выбери поля и агрегаты. Для продуктового отчёта часто нужны не только сумма выручки, но и количество заказов, уникальные покупатели и средний чек. Явные имена агрегатов читаются лучше, чем цепочка из нескольких промежуточных таблиц.

Условная выручка по каналам после groupby

Пример показывает форму результата, а не реальные данные. Для решения добавь период и размер сегмента.

Выручка, тыс. ₽
pythonВыручка и покупатели по каналу
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" полезен как предохранитель: он упадёт, если справа ключ оказался неуникальным.

pythonПрисоединить справочник пользователей и проверить ключ
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 сразу после операции

Сравни количество строк до и после, долю _merge == left_only и уникальность ключа справа. Если число строк выросло неожиданно, это почти всегда сигнал о дубликатах в ключе или неверном зерне.

pivot_table: сделать отчёт читаемым

После группировки данные часто удобны для кода, но неудобны для просмотра. pivot_table раскладывает измерение по строкам и колонкам: например, выручку по каналам и планам. Это хороший последний шаг перед графиком или выгрузкой, но не замена проверке исходного зерна.

pythonМатрица выручки по каналу и тарифу
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 действительно нужна, сначала объясни, почему. Часто это означает, что выбран неправильный уровень анализа. Для отчёта на уровне заказа сначала агрегируй позиции, а для отчёта на уровне товара не используй готовую сумму заказа как будто она относится к каждой позиции.

pythonПроверить справочник до соединения
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 отдельно от нулевых значений.
Продолжить чтение
Вся библиотека