Окно атрибуции сократилось до 7 дней, отказы от согласия на cookies превысили 40%, многоканальный учёт влияния между каналами стал невозможен. В 2026 году у performance-маркетолога остаётся один надёжный путь — агрегированная эконометрическая модель, Marketing Mix Modeling. Библиотека Robyn, открытая Meta в 2021 году, впервые сделала этот процесс production-ready. Как интерпретировать кривую насыщения, что означает adstock decay, в каких диапазонах работает holdout валидация — в этой статье мы настроим Robyn на data stack BigQuery и ответим на все эти вопросы.
Что такое Robyn и чем он не является
Robyn — это библиотека на R, выпущенная командой Facebook Marketing Science как открытый исходный код. Его цель: построить регрессионную модель между еженедельными (или ежедневными) расходами по каналам плюс экзогенные макропеременные (праздники, сезонность, цены) и метрику продаж. На выходе: ROAS каждого канала, уровень насыщения, эффект задержки (adstock), оптимальное распределение бюджета.
Чем он не является: это не last-click attribution, не отслеживание customer journey на уровне пользователя. Robyn не использует персональные данные, не ждёт cookie-сигналов. Он работает с aggregate time series регрессией — не Ridge и не Lasso, а с нелинейными трансформациями, сканируемыми оптимизатором Nevergrad.
В типичном MMM процессе моделируется 36 точек данных в месячной гранулярности. Robyn работает даже с ежедневной гранулярностью — минимум рекомендуется 104 недели (2 года). Менее 52 недель — высокая дисперсия, доверительные интервалы ненадёжны.
Кривая насыщения: S-curve и Hill функция
В ядре Robyn — два преобразования насыщения: Adbudg (S-curve) и Hill. Оба кодируют предположение об убывающем предельном доходе (diminishing returns). То есть каждая дополнительная 1000 руб., потраченная на канал, даёт не столько же конверсий, сколько первая тысяча.
Формула Hill трансформации:
y = K * (x^alpha) / (S^alpha + x^alpha)
- K: максимальный ответ (asymptote)
- S: точка полусытости (при расходах S ответ достигает 50% K)
- alpha: крутизна кривой (alpha > 1 даёт S-curve, alpha < 1 даёт вогнутую)
Robyn оптимизирует параметры alpha и S для каждого канала через Nevergrad. Пробует 10000+ комбинаций, выбирает лучший fit по критерию NRMSE (normalized root mean squared error).
Практическая интерпретация:
- Если для Google Ads S = 50000 руб., это значит, что еженедельный расход в 50000 руб. достигает половины потенциального ответа.
- Если alpha = 2.5, то кривая имеет крутую S-форму — ниже 50000 доход низкий, выше 50000 растёт медленно.
- Budget optimizer использует эти кривые для ответа на вопрос: "Лучше ли увеличить Google Ads с 50000 до 60000 руб. или Facebook с 30000 до 40000?"
На практике: поиск обычно вогнутый (alpha < 1), display/video — S-curve (alpha > 1). Спрос на поиск ограничен, пул display безграничен, но внимание пользователя конечно.
Adstock Decay: моделирование отложенного эффекта
Маркетинговый расход влияет на продажи не только в день траты, но и неделями позже. TV-реклама создаёт brand recall через 3 недели, paid social действует 7 дней. Adstock кодирует эту задержку (carryover) и затухание (decay) математически.
Robyn предлагает две трансформации adstock:
- Geometric adstock: экспоненциальное затухание. Параметр theta (0–1). Theta = 0.5 означает, что 50% эффекта прошлой недели переносится на эту.
- Weibull adstock: более гибкий — отложенный пик + длинный хвост. Параметры: shape (k) и scale (lambda). Предпочтителен для TV-подобных каналов с отложенным пиком эффекта.
Формула geometric adstock:
adstocked_t = spend_t + theta * adstocked_(t-1)
Robyn оптимизирует theta (или k, lambda) для каждого канала через grid search. Пользователь задаёт диапазон в hyperparameters.json (например, theta 0–0.7), модель находит оптимальный theta.
Что делать в коде:
hyperparameters <- list(
google_ads_S = c(0.3, 3), # диапазон для theta adstock
google_ads_alphas = c(0.5, 3), # диапазон для saturation alpha
facebook_ads_S = c(0.1, 2),
facebook_ads_alphas = c(1, 5)
)
Результат: если Google Ads theta = 0.4, а Facebook = 0.2, это означает, что эффект Google дольше — четверть потраты работает ещё неделю спустя, а Facebook умирает за неделю. Budget planner это учитывает.
Блок кода: простая трансформация geometric adstock (R)
apply_geometric_adstock <- function(spend, theta) {
adstocked <- numeric(length(spend))
adstocked[1] <- spend[1]
for (t in 2:length(spend)) {
adstocked[t] <- spend[t] + theta * adstocked[t - 1]
}
return(adstocked)
}
# Пример: расходы Google Ads
google_spend <- c(10000, 15000, 12000, 8000, 20000)
theta_google <- 0.5
adstocked_google <- apply_geometric_adstock(google_spend, theta_google)
print(adstocked_google)
# [1] 10000.0 20000.0 22000.0 19000.0 29500.0
Внутри Robyn этот код оптимизирован на C++, но логика идентична.
Holdout Validation: тест надёжности модели
При улучшении fit модели существует риск overfitting. 10 каналов + 5 макропеременных + saturation и adstock параметры для каждого → 30+ переменных. На 104 точках данных это слишком много степеней свободы.
Robyn использует holdout validation: исключает последние N недель из обучения, модель учится на историческом периоде, предсказывает на holdout, вычисляет MAPE (mean absolute percentage error) относительно фактических значений.
Определение holdout в Robyn:
InputCollect <- robyn_inputs(
dt_input = df_marketing,
dep_var = "revenue",
paid_media_spends = c("google_ads", "facebook_ads", "tiktok_ads"),
window_start = "2024-01-01",
window_end = "2026-04-30",
adstock = "geometric",
prophet_vars = c("trend", "season", "holiday"),
prophet_country = "RU"
)
# Holdout: последние 8 недель
OutputModels <- robyn_run(
InputCollect = InputCollect,
iterations = 2000,
trials = 5,
ts_validation = TRUE,
ts_holdout = 8 # последние 8 недель — тестовое множество
)
Интерпретация результатов:
- NRMSE train < 0.10, NRMSE holdout < 0.15 → модель надёжна.
- NRMSE train = 0.05, holdout = 0.30 → overfitting, нужно сузить диапазон гиперпараметров.
- Decomp.RSSD (response sum of squared differences): какую долю объяснённого дохода дают каналы. 0.6+ хорошо, 0.8+ отлично.
Robyn одновременно запускает 5 trials (разные random seeds Nevergrad), каждый — 2000 итераций, показывает лучшие 10 моделей на Pareto фронте. Пользователь выбирает одну, исходя из бизнес-ограничений (например, "ROAS Google не может быть ниже 3").
BigQuery с Robyn: архитектура pipeline
Robyn работает на R, но источник данных может быть BigQuery. Типичный stack:
- BigQuery DW: таблица ежедневных расходов (spend_daily), таблица конверсий (conversions_daily), макропеременные (holidays, weather, competitor_price).
- dbt трансформация: join + агрегация. Переводит в еженедельные строки, создаёт колонны расходов по каналам.
- R скрипт (Cloud Run или Vertex AI): пакет bigrquery забирает из BigQuery, подаёт в Robyn, пишет результаты обратно в BigQuery.
- Looker Studio dashboard: визуализирует выход модели — ROAS по каналам, оптимальное распределение, графики насыщения.
Пример dbt модели (marketing_mix_weekly.sql):
WITH spend_agg AS (
SELECT
DATE_TRUNC(spend_date, WEEK) AS week_start,
SUM(CASE WHEN channel = 'google_ads' THEN spend ELSE 0 END) AS google_ads_spend,
SUM(CASE WHEN channel = 'facebook_ads' THEN spend ELSE 0 END) AS facebook_ads_spend,
SUM(CASE WHEN channel = 'tiktok_ads' THEN spend ELSE 0 END) AS tiktok_ads_spend
FROM `project.dataset.spend_daily`
WHERE spend_date BETWEEN '2024-01-01' AND '2026-04-30'
GROUP BY 1
),
revenue_agg AS (
SELECT
DATE_TRUNC(conversion_date, WEEK) AS week_start,
SUM(revenue) AS total_revenue
FROM `project.dataset.conversions_daily`
WHERE conversion_date BETWEEN '2024-01-01' AND '2026-04-30'
GROUP BY 1
)
SELECT
s.week_start,
s.google_ads_spend,
s.facebook_ads_spend,
s.tiktok_ads_spend,
r.total_revenue
FROM spend_agg s
LEFT JOIN revenue_agg r USING (week_start)
ORDER BY week_start
Эта таблица материализуется в BigQuery, R скрипт Robyn забирает её через bigrquery::bq_table_download(). Выход модели (контрибуция каждого канала по неделям) пишется обратно в BigQuery — BI инструменты читают оттуда.
Budget Optimizer: оптимальное распределение по Парето
После подгонки модели Robyn запускает второй модуль: budget allocator. Входные данные: общий бюджет (например, 500000 руб./неделю), ограничения по каналам (например, Google Ads не менее 50000 руб.). Выход: оптимальное распределение для максимизации ROAS.
Алгоритм: берёт производную кривой насыщения каждого канала (marginal ROAS), сдвигает расходы до уравнения маржинальных ROAS. Это оптимизация по множителям Лагранжа.
Пример таблицы результатов:
| Канал | Текущий расход | Оптимальный расход | Дельта | Текущий ROAS | Оптимальный ROAS | |---|---|---|---|---| | Google Ads | 200000 руб. | 180000 руб. | −20000 | 4.2 | 4.5 | | Facebook Ads | 150000 руб. | 200000 руб. | +50000 | 3.8 | 4.1 | | TikTok Ads | 100000 руб. | 120000 руб. | +20000 | 3.5 | 3.9 | | Display | 50000 руб. | 0 руб. | −50000 | 1.2 | — |
Интерпретация: Display даёт ROAS 1.2 даже далеко ниже точки насыщения — его можно убрать. Google Ads уже за точкой насыщения, сокращение бюджета на 20000 поднимет ROAS. Facebook Ads находится на пологой части кривой — увеличение бюджета эффективно.
Эта таблица предоставляется CFO, выход Robyn визуализируется в Looker. Принятие решений становится data-driven: "Дадим Facebook ещё 50000 руб. в месяц" — это уже не предположение, а output модели.
Для запуска Robyn нужны: 2 года еженедельных гранулярных данных, R окружение, подключение к BigQuery и 4–6 часов на tuning гиперпараметров. После запуска в production модель refreshится раз в месяц (добавляются 4 новые недели, holdout окно сдвигается). Параметры кривой насыщения и