Собесов

Retentioneering: анализ эффективности товаров в Яндекс.Маркете (DRR/ROAS)

PythonМаркетинговая аналитикаСредняяMiddle

Условие

Дан CSV dataset_ymarket_offers.csv со статистикой по магазинам и товарам в Яндекс.Маркете за месяц:

  • store_id, offer_id, clicks, cost, orders_number, revenue_value, orders_number_conf, revenue_value_conf, profit.

«Conf» = выкупленные заказы. Берём только позиции с clicks > 3 и revenue_value > 1000.

Задачи:

  1. Топ-10 прибыльных offer_id — распределение расходов и доходов.
  2. Посчитать ДРР (доля рекламных расходов), ROAS, какие ещё показатели добавили бы.
  3. Найти товары, где ДРР по размещённому заказу хороший (≤10%), а по выкупу — плохой (>10%).
  4. Найти товары, эффективные по соотношению прибыли и расходов.
  5. Найти товары, отстающие по выкупаемости от остальных.
  6. Предложить правило увеличения/уменьшения бюджета на эффективные/неэффективные товары.

Решение

Подход

Метрики:

  • ДРР = cost / revenue × 100% (доля рекламных расходов в доходе).
  • ROAS = revenue / cost (обратная ДРР, безразмерная).
  • Выкупаемость = orders_number_conf / orders_number.
  • CPO = cost / orders_number (cost per order).
  • AOV = revenue / orders_number (средний чек).
  • Margin / CAC ratio = profit / cost.

Реализация

import pandas as pd
import numpy as np
 
df = pd.read_csv("dataset_ymarket_offers.csv")
 
# Полезная выборка
df = df[(df["clicks"] > 3) & (df["revenue_value"] > 1000)].copy()
 
# Метрики
df["drr_placed"]  = df["cost"] / df["revenue_value"] * 100
df["drr_conf"]    = df["cost"] / df["revenue_value_conf"].replace(0, np.nan) * 100
df["roas_placed"] = df["revenue_value"] / df["cost"]
df["roas_conf"]   = df["revenue_value_conf"] / df["cost"]
df["redemption_rate"] = df["orders_number_conf"] / df["orders_number"].replace(0, np.nan)
df["cpo"] = df["cost"] / df["orders_number"].replace(0, np.nan)
df["margin_per_cost"] = df["profit"] / df["cost"]
 
# 1. Топ-10 прибыльных
top10 = (df.groupby("offer_id", as_index=False)
           .agg(profit=("profit","sum"), cost=("cost","sum"),
                revenue=("revenue_value_conf","sum"))
           .sort_values("profit", ascending=False)
           .head(10))
 
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(10,5))
top10.plot(x="offer_id", y=["cost","revenue"], kind="bar", ax=ax)
ax.set_title("Top-10 profitable offers: cost vs revenue (confirmed)")
plt.tight_layout()
 
# 3. ДРР по размещённому хороший, по выкупу плохой
problem = df[(df["drr_placed"] <= 10) & (df["drr_conf"] > 10)]
print(problem[["store_id","offer_id","drr_placed","drr_conf","redemption_rate"]].head(20))
 
# 4. Эффективные по profit/cost
df["score"] = df["profit"] / df["cost"]
top_efficient = df.sort_values("score", ascending=False).head(20)
 
# 5. Низкая выкупаемость относительно остальных
median_redem = df["redemption_rate"].median()
laggards = df[df["redemption_rate"] < median_redem * 0.7]
print(laggards.shape, "товаров с низкой выкупаемостью")

Бюджетная логика

Простое правило:

def budget_action(row, drr_target=10):
    if row["drr_conf"] is np.nan:
        return "stop"  # нет выкупа
    if row["drr_conf"] <= drr_target * 0.7:
        return "increase"
    elif row["drr_conf"] <= drr_target * 1.0:
        return "keep"
    elif row["drr_conf"] <= drr_target * 1.5:
        return "decrease"
    else:
        return "stop"
 
df["action"] = df.apply(budget_action, axis=1)
print(df["action"].value_counts())

Более «умная» формула — мультипликативный коэффициент к текущему бюджету:

k=DRRtargetDRRactualcap(0.5,2.0)k = \frac{DRR_{target}}{DRR_{actual}} \cdot \text{cap}(0.5, 2.0)

Cap нужен, чтобы не дёргать бюджет в 10 раз за итерацию (волатильность).

Дополнительные метрики

  • Profit per clickprofit / clicks. Защищает от ложных «эффективных», у которых случайно один большой заказ.
  • CR = orders / clicks — насколько эффективен сам товар при клике.
  • Доля выкупа в категории — товар может казаться плохим, но это норма для категории (например, одежда — 60%, электроника — 90%).
  • Доверительные интервалы на ДРР через bootstrap — бюджет надо менять с учётом неопределённости.

Подводные камни

  1. Деление на ноль. Если clicks = 0 или cost = 0, ROAS уходит в inf. Заранее replace(0, np.nan).
  2. Маленькая выборка. Пара заказов на товар = огромная дисперсия. Условие clicks > 3 фильтрует, но 4 кликов всё равно мало для уверенного решения. Накладывайте дополнительный порог revenue_value > X или используйте байесовский смуш.
  3. Сезонность. Месячная выгрузка может попасть на распродажи, новогоднюю активность. Сравнивайте товары между собой, а не с собой во времени.
  4. «Хороший по placed, плохой по conf» = высокая возвратность. Это структурная проблема: либо товар реально не нравится, либо самовывоз/доставка ломаются.
  5. Категория. Без категории-нормы ДРР, ROAS-сравнение между товарами некорректно. Минимум — нормировать на медиану по категории.
  6. Бюджет ≠ перформанс монотонно. При увеличении ставки CPC растёт, ROAS падает. Линейная формула «k = drr_target/drr_actual» — упрощение.

Эталонный ответ

Считаем ДРР, ROAS, выкупаемость, CPO, AOV, profit/cost. Эффективные товары — высокий profit/cost при достаточном объёме. Проблемные — drr_placed ≤ 10%, drr_conf > 10% (высокая возвратность). Бюджетное правило: k = drr_target/drr_actual с capping [0.5, 2.0] и stop при низком объёме. Учитываем категорию и неопределённость через bootstrap-CI.

Хочешь увидеть разбор?

Зарегистрируйся бесплатно — откроется развёрнутое решение этой задачи и ещё 4 на выбор.

Зарегистрироваться и увидеть разбор
Уже есть аккаунт? Войти