Атрибуция ломается последние три года. iOS 14.5, Consent Mode v2, отказ от сторонних cookies — всё это оставляет маркетолога с одним вопросом: какой канал действительно работает? Marketing Mix Modeling (MMM) — это статистический ответ, который разрывает зависимость от cookies и пикселей, работая с агрегированными данными на уровне портфеля. Фреймворк Robyn от Meta превращает MMM из академического упражнения в production-ready pipeline. Эта статья даёт конкретные шаги для настройки Robyn с нуля, интерпретации кривых насыщения, калибровки параметров адстока и валидации модели через holdout-тестирование.

Что такое MMM и почему это критично сейчас

Marketing Mix Modeling объясняет связь между расходами на медиа и продажами или конверсиями через регрессионную статистику. Он не требует данных на уровне пользователей — работает с агрегированными метриками: недельные или суточные расходы, показы, продажи. Модель рассчитывает маржинальный вклад каждого канала (incrementality) и показывает, когда канал входит в насыщение.

Классическая last-click атрибуция основана на пиксельном отслеживании — весь кредит за конверсию получает последний нажатый канал. MMM наблюдает все каналы в одном временном окне и изолирует корреляцию. Например, если между ТВ-рекламой и продажами есть трёхнедельная задержка (carryover effect), модель захватывает её через параметр «адсток». Кривая насыщения показывает убывающую отдачу: первые 100.000 рублей расходов приносят 50 конверсий, а следующие 100.000 — только 20.

Robyn — это R-пакет Meta, обученный на собственных данных кампаний. Он включает байесовскую гребневую регрессию, многоцелевой эволюционный алгоритм (MOEA) для настройки гиперпараметров и оптимизацию через Nevergrad. После подготовки данных 50 строк кода R производят модель.

Подготовка данных: из BigQuery в Robyn

Robyn ожидает на входе один CSV/data.frame. Каждая строка — это временной период (неделя или день), каждый столбец — расходы канала, показы или продажи. Пропущенные данные не допускаются — если ячейка пуста, нужна imputation. Минимальная схема:

datetv_spendfb_spendgoogle_spendrevenuecontrol_var
2024-01-01500001200080001200000.8
2024-01-08550001300090001350000.9

Запрос для извлечения данных из BigQuery с еженедельной агрегацией:

SELECT
  DATE_TRUNC(event_date, WEEK) AS date,
  SUM(IF(channel = 'tv', spend, 0)) AS tv_spend,
  SUM(IF(channel = 'facebook', spend, 0)) AS fb_spend,
  SUM(IF(channel = 'google', spend, 0)) AS google_spend,
  SUM(revenue) AS revenue,
  AVG(seasonality_index) AS control_var
FROM `project.dataset.marketing_events`
WHERE event_date BETWEEN '2022-01-01' AND '2024-12-31'
GROUP BY 1
ORDER BY 1

Контрольная переменная (тренд, сезонность, макроэкономический индекс) не обязательна, но повышает объяснительную силу модели. Например, в розничной торговле январь — месяц скидок, поэтому добавьте dummy-переменную. Robyn включает эти переменные как «органическую» базовую линию в регрессию.

Для загрузки данных в R используйте пакет bigrquery:

library(bigrquery)
bq_auth(path = "service-account-key.json")
sql <- "SELECT date, tv_spend, fb_spend, google_spend, revenue FROM ..."
df <- bq_project_query("your-project-id", sql) %>% bq_table_download()

Функция robyn_inputs() валидирует схему для совместимости с Robyn. Столбец даты должен быть класса Date, метрики — numeric.

Конфигурация модели Robyn: адсток и насыщение

Ядро Robyn — функции robyn_inputs() и robyn_run(). Первый шаг — определить входные параметры модели:

library(Robyn)

InputCollect <- robyn_inputs(
  dt_input = df,
  date_var = "date",
  dep_var = "revenue",
  dep_var_type = "revenue",
  prophet_vars = c("trend", "season", "holiday"),
  prophet_country = "RU",
  paid_media_spends = c("tv_spend", "fb_spend", "google_spend"),
  paid_media_vars = c("tv_spend", "fb_spend", "google_spend"),
  context_vars = c("control_var"),
  adstock = "geometric",
  window_start = "2022-01-01",
  window_end = "2024-10-31"
)

Выбор типа адстока:

  • geometric: Наиболее распространённый. Постоянная скорость затухания (например, каждую неделю остаётся 80%). Подходит для ТВ и display.
  • weibull: Асимметричное затухание — быстрое падение вначале, затем замедление. Логично для видео и кампаний инфлюэнсеров.

Формула геометрического адстока:

transformed_value[t] = spend[t] + theta * transformed_value[t-1]

theta — это скорость затухания (0-1). Robyn автоматически оптимизирует этот параметр, но вы можете установить диапазон вручную:

hyperparameters <- list(
  tv_spend_alphas = c(0.5, 3),       # коэффициент кривой насыщения
  tv_spend_gammas = c(0.3, 1),       # точка перегиба насыщения
  tv_spend_thetas = c(0, 0.5),       # скорость затухания адстока
  fb_spend_alphas = c(0.5, 3),
  fb_spend_gammas = c(0.3, 1),
  fb_spend_thetas = c(0, 0.3)
)

InputCollect <- robyn_inputs(
  InputCollect = InputCollect,
  hyperparameters = hyperparameters
)

Параметры насыщения:

  • alpha: форма кривой. Высокий alpha → позднее насыщение.
  • gamma: точка перегиба — 0.5 означает излом в середине.

Насыщение моделируется через уравнение Хилла:

response = spend^alpha / (gamma^alpha + spend^alpha)

Robyn оптимизирует эти параметры через эволюционный алгоритм. Он создаёт 2000 моделей и выбирает лучшие на основе Pareto frontier (баланс между R² и NRMSE).

Запуск модели и интерпретация результатов

Запуск модели Robyn:

OutputModels <- robyn_run(
  InputCollect = InputCollect,
  iterations = 2000,
  trials = 5,
  cores = 8
)

Выход — список, где каждая итерация представляет разный набор гиперпараметров. Robyn автоматически выбирает 3 лучшие модели (Pareto-оптимальные). Результаты:

OutputModels$resultHypParam    # параметры для всех моделей
OutputModels$xDecompAgg        # декомпозиция по каналам
OutputModels$resultCalibration # оценка валидации holdout

Пример таблицы декомпозиции:

channeltotal_spendtotal_responseroimean_response
tv_spend240000018000000.7515000
fb_spend6000007200001.206000
google_spend4000005600001.404667

Интерпретация ROI: Facebook 1.20 — каждый рубль расходов приносит 1.20 рублей дохода. ТВ 0.75 — это не отрицательный ROI, а 0.75 рублей инкрементального вклада сверх базовой линии. Robyn измеряет incrementality, а не last-click кредит.

Обнаружение насыщения: Robyn строит график кривой насыщения:

robyn_onepagers(InputCollect, OutputModels, select_model = "2_100_3")

На графике видна точка, где кривая начинает выравниваться. Например, если расходы ТВ превышают 80.000 рублей, маржинальный доход падает на 50% — это критический сигнал для оптимизации бюджета.

Валидация holdout и надёжность модели

Чтобы использовать MMM-модель в production, разделите исторические данные: training set (например, январь 2022 — октябрь 2024) + holdout set (ноябрь-декабрь 2024). Модель обучается на training set и тестируется на holdout. Если MAPE (средняя абсолютная ошибка в процентах) ниже 10%, модель надёжна.

Robyn автоматически выполняет валидацию holdout:

InputCollect <- robyn_inputs(
  InputCollect = InputCollect,
  window_start = "2022-01-01",
  window_end = "2024-10-31",
  rollingWindowStartWhich = 52,  # последние 52 недели — holdout
  rollingWindowEndWhich = 4
)

Результаты в таблице resultCalibration:

model_idnrmse_trainnrmse_valdecomp.rssd
2_100_30.080.120.05

NRMSE (нормализованная среднеквадратичная ошибка): Ниже — лучше. 0.12 приемлемо (production-ready — ниже 0.15). decomp.rssd: Согласованность декомпозиции между training и validation. 0.05 → 5% отклонение → стабильная модель.

Если валидация holdout не удалась, возможны две проблемы: (1) Недостаточно данных — нужно минимум 2 года еженедельных данных. (2) Пропущены переменные — добавьте сезонность, расходы конкурентов, изменения цены.

Связь выходов Robyn с механизмом принятия решений

Экспортируйте таблицу декомпозиции в CSV:

write.csv(OutputModels$xDecompAgg, "robyn_output.csv")

Загрузите в BigQuery:

LOAD DATA OVERWRITE `project.dataset.mmm_results`
FROM FILES (
  format = 'CSV',
  uris = ['gs://bucket/robyn_output.csv']
);

Эта таблица подключается к dashboard'ам (Looker, Tableau) или оптимизаторам бюджета. Например, dbt-модель рассчитает оптимальные расходы:

WITH saturation AS (
  SELECT
    channel,
    total_spend,
    roi,
    total_spend / NULLIF(roi, 0) AS optimal_spend
  FROM `project.dataset.mmm_results`
)
SELECT * FROM saturation WHERE roi > 1.0 ORDER BY roi DESC;

Этот запрос ранжирует каналы с ROI > 1 — это приоритеты для увеличения бюджета. Robyn также имеет встроенную функцию оптимизатора бюджета:

AllocatorCollect <- robyn_allocator(
  InputCollect = InputCollect,
  OutputCollect = OutputModels,
  select_model = "2_100_3",
  scenario = "max_response",
  channel_constr_low = c(0.7, 0.7, 0.7),
  channel_constr_up = c(1.5, 1.5, 1.5)
)

Выход — рекомендуемый новый бюджет для каждого канала. Ограничения удерживают изменения в диапазоне 70-150% текущих расходов (для снижения операционного риска).

First-Party Data & Measurement Architecture критична для Robyn — качество данных, скармливаемых модели, прямо влияет на надёжность. Без server-side event tracking, разрешения identity и интеграции consent mode агрегирование вводит bias.

Распространённые ловушки и их смягчение

Мультиколлинеарность: Если два канала всегда работают одновременно (например, ТВ + Facebook запускаются вместе), модель не может раз