AI-медиамикс моделирование (Media Mix Modeling)
Media Mix Modeling (MMM) — эконометрический подход к оценке вклада каждого маркетингового канала в продажи. В условиях деградации cookie-трекинга и ограничений GDPR MMM переживает ренессанс как privacy-safe метод атрибуции. Современный MMM объединяет байесовскую статистику с ML для устойчивых, интерпретируемых оценок.
Основы MMM
Задача: Имея исторические данные о расходах по каналам (ТВ, digital, OOH, radio, search) и продажах — оценить вклад каждого канала и оптимальный бюджетный сплит.
Adstock трансформация: Реклама имеет отложенный эффект (carry-over). Adstock моделирует это:
def adstock_transform(spend_series, decay_rate=0.3, lag=None):
"""
Adstock: каждая точка = spend + decay × предыдущий adstock
decay_rate: 0 = нет памяти, 0.9 = долгая память
"""
adstock = np.zeros(len(spend_series))
adstock[0] = spend_series[0]
for t in range(1, len(spend_series)):
adstock[t] = spend_series[t] + decay_rate * adstock[t-1]
return adstock
# Геометрический vs. Weibull adstock
def weibull_adstock(spend, shape=2.0, scale=4.0, max_lag=13):
"""
Weibull PDF: гибкая форма распределения отложенного эффекта
shape < 1: убывающий (мгновенное воздействие)
shape > 1: delayed peak (реклама накапливается)
"""
pdf = scipy.stats.weibull_min.pdf(np.arange(max_lag), shape, scale=scale)
pdf = pdf / pdf.sum()
return np.convolve(spend, pdf, mode='full')[:len(spend)]
Saturation (diminishing returns): Чем больше тратим, тем меньше прирост. Hill function:
def hill_saturation(spend, alpha=2.0, gamma=0.5):
"""
alpha: форма кривой (крутизна)
gamma: половина насыщения (при каком spend эффект = 50% максимума)
"""
return spend**alpha / (gamma**alpha + spend**alpha)
Байесовский MMM
PyMC/Stan реализация:
import pymc as pm
import numpy as np
with pm.Model() as mmm_model:
# Priors для параметров каждого канала
beta_tv = pm.HalfNormal('beta_tv', sigma=1.0)
beta_digital = pm.HalfNormal('beta_digital', sigma=1.0)
beta_search = pm.HalfNormal('beta_search', sigma=1.0)
# Decay priors (0-1)
decay_tv = pm.Beta('decay_tv', alpha=3, beta=3) # peak около 0.5
decay_digital = pm.Beta('decay_digital', alpha=2, beta=5) # < 0.5 для digital
# Saturation priors
gamma_tv = pm.HalfNormal('gamma_tv', sigma=0.5)
# Transformed media
tv_adstock = adstock_transform(tv_spend, decay_tv)
tv_saturated = hill_saturation(tv_adstock, gamma=gamma_tv)
# Baseline и тренды
trend = pm.Deterministic('trend', np.arange(len(y)))
seasonality = pm.Deterministic('seasonality', fourier_features(n_harmonics=4))
intercept = pm.Normal('intercept', mu=y.mean(), sigma=y.std())
# Модель
mu = (intercept +
beta_tv * tv_saturated +
beta_digital * digital_saturated +
beta_search * search_saturated +
trend_coef * trend +
seasonality_coefs @ seasonality)
sigma = pm.HalfNormal('sigma', sigma=y.std() * 0.2)
y_obs = pm.Normal('y_obs', mu=mu, sigma=sigma, observed=y)
trace = pm.sample(2000, tune=1000, target_accept=0.95)
Преимущества байесовского подхода:
- Posterior распределение → доверительные интервалы для каждого вклада
- Информативные priors от бизнеса: "TV decay обычно 0.3-0.7"
- Устойчивость к мультиколлинеарности (частая проблема MMM)
Robyn (Meta MMM Framework)
# Открытый инструмент от Meta для автоматизированного MMM
# Ridge regression + Nevergrad оптимизатор + Pareto-optimal решения
from robyn import Robyn
robyn = Robyn(
date_var='date',
dep_var='revenue',
dep_var_type='revenue',
paid_media_spends=['tv_spend', 'digital_spend', 'search_spend'],
paid_media_vars=['tv_imp', 'digital_clicks', 'search_clicks'],
context_vars=['holidays', 'promotions'],
season_var='yearmonth'
)
output = robyn.model_run(
iterations=2000,
trials=5,
ts_validation=True
)
Pareto front решений: Robyn возвращает не одно решение, а фронт Парето по NRMSE (ошибка на трейне) и DECOMP.RSSD (сумма квадратов отклонений вкладов от априорных ожиданий).
Budget Optimization
Маржинальный ROI:
def marginal_roi_curve(channel, spend_range, channel_model):
"""
При каком объёме расходов маржинальный ROI = 1?
Это оптимальная точка инвестирования (при прочих равных)
"""
revenues = [channel_model.predict(spend) for spend in spend_range]
marginal_roi = np.diff(revenues) / np.diff(spend_range)
optimal_spend = spend_range[np.argmin(np.abs(marginal_roi - 1))]
return optimal_spend, marginal_roi
Portfolio оптимизация бюджета:
from scipy.optimize import minimize
def optimize_budget(total_budget, channel_models, current_allocation):
"""
Максимизация суммарного lift по всем каналам
при ограничении на суммарный бюджет
"""
def total_revenue(allocation):
return -sum(channel_models[c].predict(allocation[i])
for i, c in enumerate(channels))
constraints = [
{'type': 'eq', 'fun': lambda x: sum(x) - total_budget}
]
bounds = [(0, total_budget)] * len(channels)
result = minimize(total_revenue, x0=current_allocation,
bounds=bounds, constraints=constraints)
return result.x
Валидация и ограничения
In-sample и out-of-sample:
- MAPE на holdout-периоде: < 10% — хорошая модель
- Decomposition check: суммарный вклад каналов + baseline = 100% продаж
Geo-experiments для калибровки: Изменение бюджета в одном регионе при прочих равных → "quasi-experiment" для верификации модельных оценок.
Принципиальные ограничения MMM:
- Агрегированные данные → не работает на уровне пользователя
- Короткие временные ряды (< 2 лет еженедельных данных) → нестабильные оценки
- Новые каналы без исторических данных → prior-зависимость
Сроки: данные подготовка + Adstock трансформации + базовый Bayesian MMM — 4-5 недель. Robyn интеграция, saturation curves, budget optimizer, geo-calibration — 2-3 месяца.







