Условие
Дана city-level выгрузка по 4 онлайн-магазинам Bolt Market — таблица htdata с колонками:
Time— день,Category level 0/1/2— иерархия категорий («Fresh Food → 🧀 Cheese → Hard Cheese»),Unique SKUs Listed— число уникальных SKU в активном статусе и видимых в приложении,Unique SKUs Sold— число уникальных SKU, по которым была хотя бы 1 продажа,# of sold SKU items— суммарный объём проданных штук,Price before Discount (EUR)— выручка до скидок (нетто? брутто? — уточнить),Discount Value (EUR)— общая скидка на проданные SKU,Item COGS (net VAT)— себестоимость проданных SKU (нетто НДС),Waste, # of items— списания по сроку годности.
Задание: обеспечить Commercial и Supply Chain команды релевантными инсайтами из датасета. Дать рекомендации и гипотезы для дальнейшей валидации.
Это open-ended кейс: оценивают глубину аналитического мышления и умение превращать сырые цифры в action items.
Решение
Подход
Senior-уровень требует структурированного подхода. Каркас «инсайтов»:
- EDA + sanity-checks (нет ли дыр в данных, выбросов).
- KPI первого уровня для коммерции и supply chain.
- Декомпозиция и сегменты: где «жжёт».
- Гипотезы с измеримым ожиданием эффекта.
Шаг 1. Метрики для коммерции
| Метрика | Формула | Что говорит |
|---|---|---|
| Sell-through rate | Unique SKUs Sold / Unique SKUs Listed |
сколько % ассортимента реально продаётся |
| Velocity per SKU | # of sold items / Unique SKUs Sold |
средняя скорость продажи |
| AOV (gross) | Price before Discount / # of sold items |
средний чек на единицу |
| Margin % | (Price - Discount - COGS) / Price |
маржинальность |
| Discount intensity | Discount Value / Price before Discount |
агрессивность скидки |
| Revenue net | Price - Discount |
то, что реально получили |
| Gross profit | Price - Discount - COGS |
прибыль до операционных затрат |
Шаг 2. Метрики для Supply Chain
| Метрика | Формула | Что говорит |
|---|---|---|
| Waste rate | Waste / # of sold items |
какая часть из «движения» в товаре уходит в списание |
| Waste cost | Waste * (Price/qty) или приближение через COGS |
потерянная выручка |
| Unsold-listed share | (Listed - Sold) / Listed |
% SKU, ни одна штука которых не продана за день |
| Out-of-stock proxy | дни, когда Listed = 0 (в категориях с обычным Listed > 0) |
потенциальный stock-out |
Шаг 3. Реализация EDA
import pandas as pd
import matplotlib.pyplot as plt
df = pd.read_excel("htdata.xlsx", parse_dates=["Time"])
# 1. Базовые KPI
df["revenue_net"] = df["Price before Discount (EUR)"] - df["Discount Value (EUR)"]
df["gross_profit"] = df["revenue_net"] - df["Item COGS (net VAT)"]
df["margin_pct"] = df["gross_profit"] / df["revenue_net"]
df["sell_through"] = df["Unique SKUs Sold"] / df["Unique SKUs Listed"]
df["velocity"] = df["# of sold SKU items"] / df["Unique SKUs Sold"]
df["discount_share"] = df["Discount Value (EUR)"] / df["Price before Discount (EUR)"]
df["waste_rate"] = df["Waste, # of items"] / df["# of sold SKU items"]
# 2. По категориям level 1
agg = df.groupby("Category level 1").agg(
revenue=("revenue_net", "sum"),
gp=("gross_profit", "sum"),
margin=("margin_pct", "mean"),
waste_items=("Waste, # of items", "sum"),
listed_avg=("Unique SKUs Listed", "mean"),
sold_avg=("Unique SKUs Sold", "mean"),
).assign(margin_overall=lambda d: d["gp"] / d["revenue"])
print(agg.sort_values("revenue", ascending=False))Шаг 4. Инсайты, которые ищем
1. Топ и хвост ассортимента (правило Парето). Какие 20% SKU дают 80% выручки? Какие SKU вообще не продаются (мертвый ассортимент)? Кандидаты на де-листинг или на промо.
sku_perf = df.groupby("Category level 2").agg(
rev=("revenue_net", "sum"),
sold=("# of sold SKU items", "sum"),
listed=("Unique SKUs Listed", "mean"),
).sort_values("rev", ascending=False)
sku_perf["cum_share"] = sku_perf["rev"].cumsum() / sku_perf["rev"].sum()
print(sku_perf[sku_perf.cum_share <= 0.80])2. Категории с высоким списанием. Свежие продукты (мясо, рыба, овощи) ожидаемо имеют waste. Но если waste rate в категории > 10% — это уже потеря маржи. Сопоставить с margin% и понять, окупает ли продажа списание.
3. Дисконт-зависимая выручка.
Категории с discount_share > 15% — вопрос: эта скидка драйвит спрос или просто «съедает маржу»? Оценить эластичность через изменение velocity при росте discount_share (по дням / категориям).
4. Сезонность / day-of-week. По датам — есть ли провалы по дням недели или временные тренды? Можно ли сократить заказы поставщику в low-days?
df["dow"] = df["Time"].dt.day_name()
print(df.groupby("dow")[["revenue_net", "Waste, # of items"]].sum())5. Out-of-stock / under-listing.
Категории, где sell_through > 0.95 — почти весь ассортимент продаётся. Это либо отлично подобрана матрица, либо не хватает SKU (товары быстро улетают). Гипотеза: расширение ассортимента поднимет revenue.
6. Cannibalisation / комплементарные категории.
Если по дням есть отрицательная корреляция между revenue двух категорий, возможно, одна каннибализирует другую. Например, готовая еда vs продукты для готовки.
Шаг 5. Рекомендации (формулировка)
Структура каждой рекомендации:
Что увидели в данных: Категория «Bakery → Gluten Free» имеет 1 SKU listed, 0 sold за весь период. waste_rate = 100%.
Гипотеза: Этот SKU не находит спрос либо из-за плохой видимости в приложении, либо из-за высокой цены. Альтернатива — спрос есть, но SKU-микс не угадан.
Что сделать: Снять SKU с матрицы или заменить на популярный альтернативный gluten-free продукт. Параллельно — A/B-тест размещения категории в приложении.
Метрика успеха: revenue в подкатегории > X EUR/день в течение 2 недель после запуска.
Эталонный отчёт — список из 4–6 таких рекомендаций, отсортированных по ожидаемому бизнес-эффекту.
Подводные камни
- «Discount Value» = только user-discount. В легенде — «discounts made for users». Поставщические скидки и промо могут быть в другом месте. Нельзя считать
revenue_netбез уточнения. Waste≠ потеря выручки 1:1. Списанная единица товара не была бы продана с вероятностью 100%. Реальная потеря —Waste * P(sale | not listed) * AvgPrice. Без модели спроса это оценка сверху.- Items vs SKUs.
Unique SKUs Sold(сколько разных) ≠# of sold items(сколько штук). Ошибка в формуле = инсайт неверный. - Симпсон по городам. Если данные city-level (4 магазина), агрегат «весь датасет» может скрывать сильно разное поведение по городам. Всегда проверять разбивку.
- Свежесть данных. Период (Jul–Sep 2023) — летний. Зимние категории (горячая выпечка, согревающие напитки) выглядят пусто. Не делать стратегические выводы по короткому сезонному окну.
Listed = 0— это либо stock-out, либо категории не было в магазине. По одному значению не отличить — нужен контекст от Supply Chain.- Outliers в
velocity. Один день с резкой акцией дорогого SKU всё ломает. Использовать медиану / робастные метрики при сравнении. - Единицы измерения. Цены в EUR, COGS — net VAT. Если сравнивать с Price (брутто или нетто? — в легенде неоднозначно), маржа может быть искажена.
Эталонный ответ (структура)
- Шапка: 5–7 ключевых KPI за период, разбивка по категориям level 1.
- Парето-анализ ассортимента: топ SKU, мёртвые SKU, рекомендации по матрице.
- Waste hot-spots: топ категорий по
waste_rate, оценка потерь, рекомендации (заказ меньшими партиями, динамические скидки на товар у срока годности). - Скидочная политика: где скидка реально драйвит спрос, где «жжёт маржу».
- Out-of-stock/under-listing: категории с
sell_through > 95%— кандидаты на расширение. - Day-of-week / сезонность: оптимизация заказов поставщикам.
- Гипотезы для дальнейшей валидации (минимум 3–4) с метрикой и ожидаемым эффектом.
Senior-разница от middle: не отчёт «вот цифры», а «вот цифры → вот что они значат → вот что делать → вот как измерить эффект».