Продажи по акции выросли: что произошло с остальными товарами категории

У товара по акции продажи выросли с 10 до 25 штук в день. В презентации легко написать «рост в 2,5 раза», но закупщик замечает, что соседний товар перестали покупать. Посмотрим на всю категорию: сколько новых единиц она продала и что произошло с валовой прибылью после скидки.

Выбираем уровень сравнения

Берём два периода одинаковой длины: две недели до акции и две недели во время неё. В каждом есть все дни недели. Магазины, ассортимент и часы работы одинаковы. Для каждого SKU известны проданные штуки, выручка после скидок и себестоимость реализованного товара. Возвраты должны обрабатываться одинаково в обоих периодах.

Сравнение до и во время описывает наблюдавшееся изменение. Причину оно само не устанавливает: на продажи влияют погода, реклама, наличие и сезонность. Здесь задача аналитика — проверить масштаб результата и подготовить следующий вопрос. Чтобы измерить причинный эффект акции, понадобится корректный эксперимент или обоснованный дизайн сравнения.

Сначала видим товары, затем итог категории

В категории два товара A и B. Акция действует на A. Таблица содержит агрегаты за периоды before и promo: объём, среднюю фактическую цену и себестоимость единицы. Для компактности в примере цена внутри периода постоянна. В рабочей выгрузке сначала суммируют выручку и себестоимость строк продаж, а уже затем получают средние.

Сводка по каждому товару нужна, чтобы не спрятать перераспределение спроса. Итог категории нужен, чтобы не принять увеличение одной строки за новый спрос. Посчитаем изменения количества, выручки и валовой прибыли. Эти величины отвечают на разные вопросы и могут двигаться в разные стороны.

Собираем расчёт в pandas

Пара период–товар уникальна. Поэтому используем pivot без скрытого усреднения дублей. Если в исходнике несколько магазинов или дней, сначала агрегируйте их по определённым правилам. Для этой учебной категории ожидаем оба товара в обоих периодах и проверяем полноту до вычитания.

import pandas as pd
data = pd.DataFrame([
    ('before','A',140,100,60), ('before','B',280,80,50),
    ('promo','A',350,75,60), ('promo','B',140,80,50)
],columns=['period','sku','units','price','cost'])
assert not data.duplicated(['period','sku']).any()
data['revenue'] = data.units*data.price
data['gross_profit'] = data.units*(data.price-data.cost)
wide = data.pivot(index='sku',columns='period',values=['units','revenue','gross_profit'])
assert wide.notna().all().all()
delta = pd.DataFrame({metric:wide[(metric,'promo')]-wide[(metric,'before')]
                     for metric in ['units','revenue','gross_profit']})
print(delta.to_string())
totals = data.groupby('period')[['units','revenue','gross_profit']].sum()
print(totals.to_string())
assert delta.units.to_dict() == {'A':210,'B':-140}
assert int(delta.units.sum()) == 70
assert int(delta.gross_profit.sum()) == -4550
assert totals.loc['promo','gross_profit'] == 9450

Что получилось вместо красивого процента

Акционный A прибавил 210 штук, B потерял 140, вся категория выросла только на 70: с 420 до 490 единиц за две недели. Валовая прибыль A снизилась с 5600 до 5250 рублей, хотя количество проданных единиц выросло. У B она упала с 8400 до 4200. Итог категории — 9450 вместо 14000 рублей.

Такой результат требует обсуждения цели акции. Если целью была валовая прибыль категории, наблюдаемый период выглядит хуже. Если распродавался товар перед истечением срока годности, сравнение должно учитывать стоимость списания. Если привлекали новых клиентов, понадобится отдельный анализ их дальнейших покупок. Цель нельзя выбирать задним числом по тому показателю, который оказался красивее.

Где искать возможную каннибализацию

Одновременный рост A и падение B согласуется с переносом части спроса между похожими товарами. Для проверки посмотрите наличие B, его цену и выкладку, состав покупателей и продажи в магазинах без акции. Если B неделю отсутствовал на полке, падение объясняется доступностью, и перенос спроса на акционный товар нельзя считать установленным.

Полезно добавить ещё несколько периодов до акции и после неё. Покупатели могли купить товар впрок, и часть роста затем превратится в провал. Проверьте также корзину целиком: покупатель акционного товара мог добавить другие прибыльные позиции. Тогда категория и весь чек дадут разные, но содержательные ответы на разные вопросы.

Как оформить вывод

В записке разделите факт и гипотезу. Факт: объём категории вырос на 70 штук, валовая прибыль снизилась на 4550 рублей. Гипотеза: часть покупателей переключилась с B на A; её нужно проверить по наличию и сопоставимым магазинам. Следующее действие: уточнить цель акции и подготовить сравнение, которое позволит оценить её эффект, а не только динамику.

Не смешиваем эффект скидки и оплату акции

Валовая прибыль в примере учитывает снижение цены, но не включает отдельный рекламный бюджет и компенсацию поставщика. Если поставщик возвращает магазину часть скидки, добавьте подтверждённую компенсацию отдельной строкой. Если магазин оплатил размещение баннера, покажите этот расход после валовой прибыли. Тогда сравнение даст последовательный переход к результату акции после её прямых затрат.

Отдельно проверьте, как скидка распределена внутри чека. При акции «два по цене одного» нулевая выручка второй строки способна исказить сравнение отдельных SKU, если скидка целиком записана на один товар. Категорийный итог может остаться верным, а товарные вклады — стать непонятными. Используйте единое правило распределения скидки в обоих периодах.

Продолжить практику

Для таких решений мало посчитать процент роста. В «Практической аналитике данных» вы пройдёте кейс продаж, научитесь выбирать метрику, сравнивать периоды и оформлять вывод с ограничениями в аналитическую записку. Откройте программу платного курса.

Продолжить на практике

  • Практическая аналитика данных ↗

    Превратите знания SQL или Python в аналитическую работу: проверяйте данные, считайте бизнес-метрики, стройте дашборды и объясняйте выводы на трёх прикладных кейсах.