Як 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 робочі дні.







