Як AI допомагає перерозподіляти рекламний бюджет між каналами
Ми часто бачимо, як компанії втрачають 20-40% рекламного бюджету через нелінійні ефекти насичення та delayed attribution. Ручний розподіл між Google Ads, Meta, programmatic та YouTube — це гра наосліп. Наш досвід показує: математичні моделі MMM з incrementality експериментами дають приріст ROAS на 30-50% без збільшення бюджету.
Наприклад, для одного e-commerce клієнта з бюджетом $500k/міс ми виявили, що 30% витрат на Display ads витрачалися марно через насичення каналу. Після впровадження MMM та перерозподілу частини бюджету в Search і Social ROAS виріс з 2.1 до 3.4 за два місяці.
Як MMM моделює внесок каналів
Media Mix Modeling (MMM) — це байєсівський підхід, який розкладає виручку на внески кожного каналу з урахуванням adstock (залишковий ефект) та saturation (спадна віддача). Ми будуємо Media Mix Model на ваших даних за 1+ рік і за допомогою оптимізатора знаходимо ідеальний розподіл.
Байєсівський підхід (наприклад, з використанням PyMC) дозволяє задати апріорні розподіли для коефіцієнтів, що дає більш стабільні оцінки при малій кількості даних та дозволяє враховувати експертні знання. В одному проекті ми задали decay_rate для TV = 0.7 (високий залишковий ефект) і для Search = 0.3, що покращило точність моделі на 15% порівняно зі звичайною лінійною регресією.
import pandas as pd
import numpy as np
from scipy.optimize import minimize
from scipy.special import expit
class MediaMixModel:
"""
Байєсівський Media Mix Model для аналізу внеску каналів.
Враховує saturation (спадна віддача) та adstock (залишковий ефект).
"""
def adstock_transform(self, spend: np.ndarray,
decay_rate: float = 0.7) -> np.ndarray:
"""
Adstock: реклама має залишковий ефект на наступні періоди.
decay_rate: 0.5-0.9 залежно від каналу (TV > 0.8, Search < 0.4)
"""
adstocked = np.zeros_like(spend, dtype=float)
adstocked[0] = spend[0]
for t in range(1, len(spend)):
adstocked[t] = spend[t] + decay_rate * adstocked[t - 1]
return adstocked
def saturation_transform(self, adstocked: np.ndarray,
alpha: float = 2.0,
gamma: float = 0.5) -> np.ndarray:
"""
Hill-функція насичення: спадна віддача при збільшенні інвестицій.
alpha: крутизна кривої насичення
gamma: точка напівнасичення (при якому spend ROI = 50% від максимального)
"""
return adstocked ** alpha / (adstocked ** alpha + gamma ** alpha)
def fit_channel_contributions(self, weekly_data: pd.DataFrame,
channel_cols: list[str],
revenue_col: str = 'revenue') -> dict:
"""
Регресія для оцінки внеску кожного каналу.
weekly_data: тижневі дані по spend та revenue
"""
from sklearn.linear_model import Ridge
X_transformed = {}
for ch in channel_cols:
adstocked = self.adstock_transform(weekly_data[ch].values)
saturated = self.saturation_transform(adstocked)
X_transformed[ch] = saturated
X = pd.DataFrame(X_transformed)
y = weekly_data[revenue_col].values
model = Ridge(alpha=1.0, fit_intercept=True)
model.fit(X, y)
# ROAS по каналу = coefficient * mean_saturation / mean_spend
contributions = {}
for i, ch in enumerate(channel_cols):
coef = model.coef_[i]
mean_saturation = X[ch].mean()
mean_spend = weekly_data[ch].mean()
marginal_roas = coef * mean_saturation / max(mean_spend, 1)
contributions[ch] = {
'coefficient': round(float(coef), 4),
'revenue_contribution_pct': round(
float(coef * X[ch].sum() / max(y.sum(), 1) * 100), 1
),
'marginal_roas': round(float(marginal_roas), 2),
}
return contributions
class BudgetAllocator:
"""Оптимальний розподіл бюджету між каналами"""
def __init__(self, channel_params: dict):
"""
channel_params: {channel_name: {'alpha': float, 'gamma': float, 'max_spend': float}}
"""
self.channels = channel_params
def marginal_roi(self, channel: str, spend: float) -> float:
"""Гранична віддача від додаткової гривні в каналі"""
p = self.channels[channel]
alpha = p.get('alpha', 2.0)
gamma = p.get('gamma', 1000.0)
base_roas = p.get('base_roas', 3.0)
# Похідна Hill-функції
numerator = alpha * gamma ** alpha * spend ** (alpha - 1)
denominator = (spend ** alpha + gamma ** alpha) ** 2
saturation_derivative = numerator / max(denominator, 1e-10)
return base_roas * saturation_derivative
def optimize_allocation(self, total_budget: float,
min_channel_budget: float = 100.0) -> dict:
"""
Розв'язання задачі оптимізації: максимізувати сумарний revenue.
Використовуємо метод Lagrange multipliers (вирівнювання граничних ROI).
"""
channels = list(self.channels.keys())
n = len(channels)
def total_negative_revenue(budgets):
"""Мінус сумарний revenue для мінімізації"""
total = 0
for i, ch in enumerate(channels):
p = self.channels[ch]
adstocked = budgets[i]
sat = adstocked ** p.get('alpha', 2) / (
adstocked ** p.get('alpha', 2) + p.get('gamma', 1000) ** p.get('alpha', 2)
)
total += sat * p.get('base_roas', 3.0) * budgets[i]
return -total
constraints = [
{'type': 'eq', 'fun': lambda b: sum(b) - total_budget}
]
bounds = [
(min_channel_budget, self.channels[ch].get('max_spend', total_budget))
for ch in channels
]
x0 = [total_budget / n] * n
result = minimize(
total_negative_revenue,
x0,
method='SLSQP',
bounds=bounds,
constraints=constraints,
options={'maxiter': 1000}
)
optimal = {ch: round(float(b), 2) for ch, b in zip(channels, result.x)}
# Прогнозований Revenue
rev_estimate = -result.fun
current_rev = -total_negative_revenue(x0)
return {
'allocation': optimal,
'projected_revenue': round(float(rev_estimate), 2),
'vs_equal_split': round((rev_estimate - current_rev) / max(current_rev, 1) * 100, 1),
'optimization_converged': result.success,
}
Що таке incrementality і як ми її вимірюємо
Incrementality measurement — це geo holdout експеримент. Ми виділяємо контрольні регіони, де реклама не показується, і порівнюємо їх з тестовими. Результат — iROAS — чиста окупність без урахування органіки. Без incrementality ви ризикуєте переоцінити внесок каналів.
Приклад: для рітейлера з 20 регіонами ми виділили 5 контрольних. Після 6-тижневого експерименту iROAS для Instagram виявився 1.8, тоді як звітний ROAS був 4.2 — це означало, що 60% конверсій з Instagram були б отримані без реклами. Перерозподіл бюджету на основі iROAS дав приріст загальної виручки на 12%.
class IncrementalityMeasurement:
"""Вимірювання інкрементальності через geo experiments"""
def design_geo_holdout(self, geos: list[str],
treatment_fraction: float = 0.5) -> dict:
"""
Geo holdout experiment: частина регіонів не отримує рекламу.
Чиста міра інкрементальності без selection bias.
"""
np.random.shuffle(geos)
split = int(len(geos) * treatment_fraction)
return {
'treatment_geos': geos[:split],
'control_geos': geos[split:],
'n_treatment': split,
'n_control': len(geos) - split,
'recommendation': 'Run for minimum 4 weeks, measure revenue lift'
}
def calculate_incremental_roas(self, treatment_revenue: float,
control_revenue: float,
treatment_spend: float,
treatment_population: int,
control_population: int) -> dict:
"""True incrementality ROAS (iROAS)"""
# Нормалізуємо на чисельність населення
treatment_rev_per_capita = treatment_revenue / max(treatment_population, 1)
control_rev_per_capita = control_revenue / max(control_population, 1)
incremental_rev_per_capita = treatment_rev_per_capita - control_rev_per_capita
incremental_total_rev = incremental_rev_per_capita * treatment_population
iroas = incremental_total_rev / max(treatment_spend, 1)
return {
'incremental_revenue': round(incremental_total_rev, 2),
'iroas': round(iroas, 2),
'reported_roas': round(treatment_revenue / max(treatment_spend, 1), 2),
'incrementality_ratio': round(iroas / max(treatment_revenue / max(treatment_spend, 1), 0.01), 2),
'interpretation': 'iROAS < reported ROAS means significant organic/direct traffic'
}
Порівняння підходів до оптимізації
| Підхід | Покращення ROAS | Вимоги до даних | Терміни впровадження |
|---|---|---|---|
| Рівномірний розподіл (baseline) | — | Немає | — |
| Rule-based за historical ROAS | +10-20% | 3+ місяці даних | 2-3 тижні |
| MMM-оптимізація | +20-35% | 1+ рік даних | 6-8 тижнів |
| MMM + incrementality | +30-50% | 1+ рік + geo експерименти | 3-4 місяці |
Rule-based підхід швидкий, але втрачає до 15% ефективності порівняно з MMM через ігнорування adstock та saturation. MMM з incrementality дає максимальний приріст, але потребує більше даних та часу.
Типові метрики для моніторингу
| Метрика | Опис | Коли використовувати |
|---|---|---|
| ROAS | Виручка / витрати по каналу | Daily monitoring |
| iROAS | Інкрементальна виручка / витрати | Після holdout експериментів |
| mROAS | Граничний ROAS (віддача останньої гривні) | Для перерозподілу бюджету |
| Contribution % | Частка каналу в загальній виручці | Для розуміння структури |
Процес та етапи роботи
- Аудит даних: Збір та валідація історичних даних по витратах та виручці за 1+ рік.
- Побудова MMM: Калібрування байєсівської моделі з підбором параметрів adstock та saturation.
- Incrementality експерименти: Дизайн та проведення geo holdout тестів (4-8 тижнів).
- Оптимізація бюджету: Запуск аллокатора для знаходження оптимального розподілу.
- Впровадження та моніторинг: Налаштування дашбордів, навчання команди, супровід 1 місяць.
Кожен етап завершується артефактами: звіт по аудиту, модель з документацією, результати експерименту, новий план розподілу. Підсумкові рекомендації ґрунтуються на математично обґрунтованих розрахунках, а не на інтуїції.
Хочете перевірити, чи ефективно розподілений ваш рекламний бюджет? Замовте безкоштовний аудит — ми проаналізуємо поточну структуру та підготуємо попередній розрахунок економії за 3 робочі дні.
Чому AI-оптимізація ефективніша за ручну
Ручна оптимізація зазвичай ґрунтується на середньому ROAS за останні 30 днів. Вона не враховує, що додаткова гривня у вже насичений канал дає все менший приріст. MMM використовує криві насичення та adstock, а бюджетний аллокатор вирівнює граничні ROI — це дає математично максимальний сумарний revenue. На практиці ми бачимо приріст 30-50% без збільшення бюджету.
Оцініть свій проект — зв'яжіться з нами для безкоштовного аудиту поточного розподілу. Ми підготуємо попередній розрахунок економії за 3 робочі дні.







