Условие
Маркетинг управляет рекламными кампаниями: запускает тестовые → переводит успешные в постоянные → мониторит расходы (COST), доходы (REVENUE) и их соотношение.
Главный KPI постоянных кампаний — ROAS-60: какой процент расходов кампания возвращает на 60-й день.
Гипотеза маркетинга: чем больше COST, тем ниже ROAS-60 (рост CPI при увеличении объёма закупки при сохранении LTV-60).
Задачи:
- Подтвердить или опровергнуть гипотезу. Считаем, что зависимость CPI от объёма нелинейная — нужно её корректно описать.
- Для каждой кампании рассчитать суточный бюджет, максимизирующий абсолютную прибыль (REVENUE-60 − COST).
- На основе п. 2 — рекомендация: увеличить / уменьшить / остановить кампанию.
- Решить проблему кампаний, у которых ещё нет полных 60 дней (предсказать ROAS-60).
Решение
Подход
Это классическая задача marketing mix optimization. Ключевые шаги:
- EDA: связь COST → CPI → installs → ROAS-60 для каждой кампании.
- Модель кривой CPI(COST) — нелинейная.
- Оптимизация прибыли — найти точку, где
d(REVENUE-COST)/d(COST) = 0. - Прогноз 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. Оптимизация суточного бюджета
Прибыль кампании в день:
С оценённой installs(C) = C / CPI(C) = C^{1-b} / a:
Производная по C и приравнивание к нулю:
Численно для каждой кампании:
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:
- Для кампаний с историей 60+ дней построить кривые ROAS(t):
ROAS(t) = revenue_to_day_t / cost. - Усреднить:
r(t) = ROAS(t) / ROAS(60)— т.н. maturation curve (доля «зрелого» revenue, накопленная к дню t). - Для незрелой кампании на дне
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).
Подводные камни
- CPI не зависит только от COST. Меняется креатив, сезон, конкуренция в аукционе. Без контроля на эти факторы регрессия даёт смещённую оценку.
- «Нелинейная зависимость» — какая именно? Power, logistic, log — разные формы дают разные оптимумы. Нужно проверять goodness-of-fit (R², residuals).
- LTV-60 не константа. При росте бюджета мы покупаем «худший» трафик, у него LTV ниже. Уравнение прибыли с константным LTV даёт overoptimistic ответ.
- Эндогенность: COST устанавливаем мы сами по бизнес-логике. Если повышали бюджет именно на хороших кампаниях — корреляция искусственная.
- Прогноз ROAS-60 от ранних дней неточный для F2P: revenue хвост сильно тяжёлый, день 7 может составлять 30% LTV-60, а может 10%.
- Малая выборка на одну кампанию: если у кампании 5 дней — fit нестабилен. Нужны pooling / mixed-effects.
- «Остановить» — не значит «навсегда». Кампания может оживиться при смене креатива или сезона.
- Стоимость экспериментов. Чтобы построить хорошую кривую CPI(C), нужно реально менять бюджеты — это дорого и рискованно.
Альтернативы
- Bayesian Marketing Mix Modeling (PyMC / Robyn) — учитывает adstock, saturation, sezonality, спилловеры между кампаниями.
- Multi-armed bandits для распределения бюджета между кампаниями (Thompson sampling) — динамическая оптимизация.
Эталонный ответ
Структура:
- Гипотеза: проверяем регрессией CPI =
a * cost^b. Гипотеза подтверждается, еслиb > 0статзначимо. - Оптимум бюджета: аналитически из условия
d(profit)/d(cost) = 0или численно черезminimize_scalar. ПолучаемC*для каждой кампании. - Решение: сравниваем текущий COST с C* → увеличить / уменьшить / остановить (если прибыль < 0 при любом C).
- Незрелые кампании: maturation curve для прогноза ROAS-60.
Главные предостережения: LTV не константа, эндогенность COST, форма кривой важна.