Собесов

Skytec Games — оптимизация бюджета рекламных кампаний по ROAS-60

Кейсы и метрикиМаркетинг и юнит-экономикаСложнаяMiddle

Условие

Маркетинг управляет рекламными кампаниями: запускает тестовые → переводит успешные в постоянные → мониторит расходы (COST), доходы (REVENUE) и их соотношение.

Главный KPI постоянных кампаний — ROAS-60: какой процент расходов кампания возвращает на 60-й день.

Гипотеза маркетинга: чем больше COST, тем ниже ROAS-60 (рост CPI при увеличении объёма закупки при сохранении LTV-60).

Задачи:

  1. Подтвердить или опровергнуть гипотезу. Считаем, что зависимость CPI от объёма нелинейная — нужно её корректно описать.
  2. Для каждой кампании рассчитать суточный бюджет, максимизирующий абсолютную прибыль (REVENUE-60 − COST).
  3. На основе п. 2 — рекомендация: увеличить / уменьшить / остановить кампанию.
  4. Решить проблему кампаний, у которых ещё нет полных 60 дней (предсказать ROAS-60).

Решение

Подход

Это классическая задача marketing mix optimization. Ключевые шаги:

  1. EDA: связь COST → CPI → installs → ROAS-60 для каждой кампании.
  2. Модель кривой CPI(COST) — нелинейная.
  3. Оптимизация прибыли — найти точку, где d(REVENUE-COST)/d(COST) = 0.
  4. Прогноз ROAS-60 для незрелых кампаний — экстраполяция кумулятивной кривой.

Реализация — Шаг 1. Проверка гипотезы маркетинга

Гипотеза: «чем больше COST, тем ниже ROAS-60». Это означает, что CPI растёт быстрее, чем растёт installs (saturation в каналах рекламы).

import pandas as pd
import numpy as np
from scipy.optimize import curve_fit
import matplotlib.pyplot as plt
 
# Спекулятивные имена колонок (адаптируйте)
costs = pd.read_csv("costs.csv")     # campaign_id, date, cost, installs
revs  = pd.read_csv("revenue.csv")   # campaign_id, install_date, days_after_install, revenue
 
# Считаем CPI на день
costs["cpi"] = costs["cost"] / costs["installs"]
 
# Для одной кампании
camp = "campaign_X"
sub = costs[costs["campaign_id"] == camp].sort_values("cost")
 
# Подгонка CPI = f(cost)
# Гипотеза: CPI растёт сублинейно, например, степенная: CPI = a * cost^b
def power(x, a, b): return a * np.power(x, b)
popt, _ = curve_fit(power, sub["cost"], sub["cpi"])
a, b = popt
print(f"CPI = {a:.4f} * cost^{b:.3f}")
# Если b > 0 — CPI растёт с cost (гипотеза маркетинга подтверждается)

Альтернативные функциональные формы:

  • Power: CPI = a * cost^b — простая, b интерпретируется как «эластичность».
  • Logistic: installs = L / (1 + exp(-k(cost - x0))) — saturation explicit.
  • Hill function (Adstock): installs = L * cost^k / (M^k + cost^k).

Регрессия по всем кампаниям с random effects на campaign_id — корректнее (mixed-effects model в pymer4 / lme4).

Подтверждение / опровержение гипотезы

Если оценённый b > 0 статзначимо (t-test на коэффициент) → CPI растёт с COST → гипотеза маркетинга подтверждается.

Контролировать:

  • LTV-60 должен быть стабильным (тогда падение ROAS-60 = рост CPI).
  • Если LTV-60 при росте COST падает — это другая проблема (низкокачественный трафик). Корректно проверить эластичность LTV(COST), а не только CPI(COST).

Шаг 2. Оптимизация суточного бюджета

Прибыль кампании в день: Π(C)=installs(C)LTV60C\Pi(C) = \text{installs}(C) \cdot \text{LTV}_{60} - C

С оценённой installs(C) = C / CPI(C) = C^{1-b} / a:

Π(C)=C1baLTV60C\Pi(C) = \frac{C^{1-b}}{a} \cdot \text{LTV}_{60} - C

Производная по C и приравнивание к нулю:

dΠdC=(1b)CbaLTV601=0\frac{d\Pi}{dC} = \frac{(1-b) C^{-b}}{a} \cdot \text{LTV}_{60} - 1 = 0 C=[(1b)LTV60a]1/bC^* = \left[\frac{(1-b) \cdot \text{LTV}_{60}}{a}\right]^{1/b}

Численно для каждой кампании:

from scipy.optimize import minimize_scalar
 
def neg_profit(cost, a, b, ltv60):
    installs = cost ** (1 - b) / a  # из CPI = a * cost^b → installs = cost / CPI
    return -(installs * ltv60 - cost)
 
ltv60 = 5.0  # средний LTV60 — оцените по revenue.csv
res = minimize_scalar(neg_profit, args=(a, b, ltv60),
                      bounds=(1, 100000), method="bounded")
print(f"Optimal daily budget: {res.x:.0f}, max profit: {-res.fun:.0f}")

Шаг 3. Рекомендация по каждой кампании

Текущий COST Оптимум Рекомендация
< C* и прибыль > 0 C* увеличить
≈ C* C* оставить
> C* и прибыль > 0 C* уменьшить
Прибыль отрицательная при любом C остановить

«Прибыль отрицательная при любом C» = когда LTV-60 < CPI(C_min) для самого маленького разумного C. Кампания структурно убыточна.

Шаг 4. Прогноз ROAS-60 для незрелых кампаний

Подход — экстраполяция кумулятивной кривой revenue:

  1. Для кампаний с историей 60+ дней построить кривые ROAS(t): ROAS(t) = revenue_to_day_t / cost.
  2. Усреднить: r(t) = ROAS(t) / ROAS(60) — т.н. maturation curve (доля «зрелого» revenue, накопленная к дню t).
  3. Для незрелой кампании на дне t < 60: ROAS_60_predicted = ROAS(t) / r(t).
# Для кампаний 60+
mature = costs.groupby("campaign_id").agg(...)  # ROAS(7), ROAS(14), ROAS(30), ROAS(60)
# Усреднённая кривая
maturation = pd.Series({
    7:  0.30,
    14: 0.55,
    30: 0.80,
    60: 1.00,
})
 
# Незрелая кампания (например, день 14)
roas_14_obs = 0.40
roas_60_pred = roas_14_obs / maturation[14]   # ≈ 0.73

Сложности: maturation curve может зависеть от:

  • сезона запуска,
  • гео,
  • креатива,
  • сегмента игроков (paid vs organic mix).

В проде используйте Bayesian hierarchical прогноз с приорами от похожих кампаний (Stan / numpyro).

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

  1. CPI не зависит только от COST. Меняется креатив, сезон, конкуренция в аукционе. Без контроля на эти факторы регрессия даёт смещённую оценку.
  2. «Нелинейная зависимость» — какая именно? Power, logistic, log — разные формы дают разные оптимумы. Нужно проверять goodness-of-fit (R², residuals).
  3. LTV-60 не константа. При росте бюджета мы покупаем «худший» трафик, у него LTV ниже. Уравнение прибыли с константным LTV даёт overoptimistic ответ.
  4. Эндогенность: COST устанавливаем мы сами по бизнес-логике. Если повышали бюджет именно на хороших кампаниях — корреляция искусственная.
  5. Прогноз ROAS-60 от ранних дней неточный для F2P: revenue хвост сильно тяжёлый, день 7 может составлять 30% LTV-60, а может 10%.
  6. Малая выборка на одну кампанию: если у кампании 5 дней — fit нестабилен. Нужны pooling / mixed-effects.
  7. «Остановить» — не значит «навсегда». Кампания может оживиться при смене креатива или сезона.
  8. Стоимость экспериментов. Чтобы построить хорошую кривую CPI(C), нужно реально менять бюджеты — это дорого и рискованно.

Альтернативы

  • Bayesian Marketing Mix Modeling (PyMC / Robyn) — учитывает adstock, saturation, sezonality, спилловеры между кампаниями.
  • Multi-armed bandits для распределения бюджета между кампаниями (Thompson sampling) — динамическая оптимизация.

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

Структура:

  1. Гипотеза: проверяем регрессией CPI = a * cost^b. Гипотеза подтверждается, если b > 0 статзначимо.
  2. Оптимум бюджета: аналитически из условия d(profit)/d(cost) = 0 или численно через minimize_scalar. Получаем C* для каждой кампании.
  3. Решение: сравниваем текущий COST с C* → увеличить / уменьшить / остановить (если прибыль < 0 при любом C).
  4. Незрелые кампании: maturation curve для прогноза ROAS-60.

Главные предостережения: LTV не константа, эндогенность COST, форма кривой важна.

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

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

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