Marketing Mix Modeling regresó a finales de los 2020 con el colapso de la atribución basada en cookies. Pero pasar de artículos académicos a un entorno de producción es un nivel completamente diferente. Robyn, que Meta liberó como código abierto en 2021, vincula esta transición a la disciplina de ingeniería: ofrece herramientas concretas para trasladar conceptos estadísticos como curvas de saturación, decay de adstock y validación holdout desde scripts R hacia pipelines operacionales. En este artículo mostramos cómo configurar en un setup de producción los tres mecanismos que forman el núcleo de Robyn: cómo disminuye el impacto publicitario en el tiempo, cómo se satura la relación gasto-retorno y cómo el proceso holdout prueba el poder predictivo del modelo.
Adstock Decay: Distribuyendo el Impacto Publicitario en el Tiempo
Un spot de TV emitido ayer no genera ventas hoy; su efecto se distribuye a lo largo de la semana. Un anuncio de búsqueda puede convertirse en el segundo del clic, pero el recuerdo de marca dispara conversiones 3 días después. El término adstock modela matemáticamente este desfase temporal. En Robyn hay dos tipos: geométrico y Weibull. El geométrico aplica un decaimiento exponencial simple; cada día el efecto del anterior se multiplica por el parámetro theta. Weibull es más flexible: controla de forma independiente la curva de ascenso y descenso del impacto.
En un setup práctico, ajustas los parámetros de adstock según el tipo de canal. Búsqueda de pago suele usar theta=0.3 (decaimiento rápido), TV theta=0.7 (cola larga), display alrededor de theta=0.5. Estos valores no son arbitrarios—se encuentran mediante búsqueda de hiperparámetros en el conjunto holdout del período anterior. En la función robyn_inputs() de Robyn estableces el argumento adstock para cada canal:
InputCollect <- robyn_inputs(
dt_input = dt_simulated_weekly,
adstock = "geometric",
adstock_params = list(
tv_s = c(0.3, 0.8),
search_clicks_p = c(0.0, 0.3),
facebook_i = c(0.0, 0.5)
)
)
Aquí especificas el rango c(min, max); el algoritmo de optimización Nevergrad explora este rango para encontrar el mejor valor de theta. Si usas Weibull en lugar de geométrico, también se añaden parámetros de forma y escala. La ventaja de Weibull es que proporciona mejor ajuste en canales como display que tienen "picos tardíos"—donde el impacto es bajo en los primeros 2 días, llega al máximo entre días 3-5.
Si configuras adstock incorrectamente, el modelo distribuye mal la contribución de los canales. Por ejemplo, si modeleas TV con theta=0.1 geométrico, el impacto se asigna solo al día de emisión y pierdes el tráfico orgánico de la semana siguiente. Al revés, si usas theta=0.9 en búsqueda de pago, atribuyes venta de hoy a clics de hace una semana—sin sentido. Por eso la configuración de adstock debe alinearse con las características del canal y estar restringida por el conocimiento del dominio.
Curva de Saturación: Cómo se Satura la Relación Gasto-Retorno
La regresión lineal asume que cada euro gastado genera el mismo retorno. En realidad, los primeros 10 mil euros dan ROAS de 8, a los 100 mil el ROAS cae a 3, a 1 millón cae por debajo de 1—retornos marginales decrecientes. La saturación es la transformación que modela esta curva. En Robyn, el tipo de saturación más común es la ecuación de Hill (Michaelis-Menten):
y = Vmax * (x^S) / (K^S + x^S)
Donde Vmax es el impacto máximo, K es el nivel de gasto donde se alcanza saturación media (punto de inflexión), S es la pendiente de la curva (forma). Si K es bajo, el canal se satura rápido; si es alto, se satura tarde. Cuando S>1 la curva toma forma S—inicio lento, medio rápido, final lento.
En Robyn configuras los parámetros de Hill también por canal:
hyperparameters <- list(
tv_s_alphas = c(0.5, 3),
tv_s_gammas = c(0.3, 1),
search_clicks_p_alphas = c(0.5, 3),
search_clicks_p_gammas = c(0.3, 1)
)
alphas corresponde al parámetro S de Hill, gammas a K (notación de Robyn). La optimización busca el mejor ajuste dentro de estos rangos. Pero no dejes la búsqueda al azar—si ya gastas el 80% de tu presupuesto de TV, la saturación debe estar por encima del 90%, sino el modelo produce ROAS marginales poco realistas.
La configuración de saturación impacta directamente tu estrategia de asignación de presupuesto. Si el modelo dibuja la curva de saturación correctamente, puedes calcular el ROAS marginal de cada canal y reasignar presupuesto. La función robyn_allocator() de Robyn hace esto—con presupuesto total fijo, ¿de qué canal restar y a cuál sumar para maximizar ventas? Pero esta recomendación solo es válida si los parámetros de saturación son correctos. Un valor incorrecto de K significa decisiones equivocadas por millones.
Validación Holdout: Probando el Poder Predictivo del Modelo
El mayor riesgo de MMM es el overfitting—el modelo memoriza datos históricos sin predecir el futuro. Para evitarlo se necesita validación holdout en series temporales. En la configuración de Robyn, reservas las últimas 4-8 semanas como conjunto holdout, el modelo se entrena con el resto de datos y hace predicciones en el período holdout. Si NRMSE (Error Cuadrado Medio Raíz Normalizado) y MAPE (Error Porcentual Absoluto Medio) son bajos, el modelo generaliza.
InputCollect <- robyn_inputs(
dt_input = dt_simulated_weekly,
window_start = "2022-01-01",
window_end = "2023-10-31",
rollingWindowStartWhich = 1,
rollingWindowEndWhich = 52,
rollingWindowLength = 4
)
rollingWindowLength = 4 reserva las últimas 4 semanas como holdout. El modelo se entrena sin ver esas 4 semanas, luego genera predicciones. En la salida de Robyn ves el NRMSE holdout para cada modelo—por debajo de 10% es bueno, por encima de 20% es sospechoso. Pero no decidas sobre una sola métrica; verifica si hay anomalías en el período holdout (campañas, días festivos). Por ejemplo, si la semana de Black Friday cae en holdout, el modelo subestima porque ese patrón de demanda pico no existe en el histórico.
Después del holdout, es práctica común re-entrenar el modelo—ajusta el modelo final con todos los datos pero selecciona hiperparámetros basándote en resultados holdout. Este ciclo "entrenar-validar-finalizar". En Robyn lo haces con robyn_refresh():
Robyn1 <- robyn_run(InputCollect = InputCollect, plot_folder = OutputCollect$plot_folder)
OutputCollect <- robyn_outputs(Robyn1, select_model = "1_100_3")
RobynRefresh <- robyn_refresh(Robyn1, dt_input = dt_simulated_weekly, refresh_steps = 4)
refresh_steps = 4 actualiza el modelo con 4 semanas de datos nuevos pero mantiene los parámetros de saturación/adstock fijos (la calibración se preserva). Este es el fundamento de un pipeline que se ejecuta continuamente en producción—cada semana añade una fila, el modelo se re-ajusta, el dashboard se actualiza.
Trasladando el Pipeline de Robyn a Producción
Robyn no es un script R aislado, es una herramienta que debe integrarse en un pipeline de datos de producción. La arquitectura típica: tabla de gastos de marketing en BigQuery + tabla de conversiones de GA4 + tabla de ingresos de CRM → agregación semanal con dbt → trigger script R de Robyn en Cloud Composer (Airflow) → resultado JSON en dashboard de Looker Studio. Este stack funciona dentro de una arquitectura de datos first-party.
El primer paso es estandarizar el esquema de datos. Robyn espera una tabla dt_input: DATE (semanal), revenue, tv_spend, search_spend, facebook_impressions y similares. Cada canal como columna separada; sin separación paid/organic el modelo no puede hacer atribución. Las semanas faltantes se deben interpolar, los outliers se deben marcar. Ejemplo de modelo dbt:
with base as (
select
date_trunc(event_date, week) as week_start,
sum(case when source = 'google/cpc' then cost else 0 end) as search_spend,
sum(case when source = 'facebook' then cost else 0 end) as facebook_spend,
count(distinct case when event_name = 'purchase' then user_pseudo_id end) as conversions
from `project.analytics_123456789.events_*`
where _table_suffix between '20220101' and '20231231'
group by 1
)
select * from base
order by week_start
Esta tabla se exporta de BigQuery como CSV y se alimenta al script Robyn, o mejor aún, se extrae directamente con el paquete R bigrquery. La segunda opción es preferible—garantiza freshness de datos.
El paso de Robyn en el DAG de Airflow:
from airflow.operators.bash import BashOperator
run_robyn = BashOperator(
task_id='run_robyn_mmm',
bash_command='Rscript /path/to/robyn_model.R ',
dag=dag
)
Dentro del script guardas el objeto del modelo con robyn_save() en formato RDS y lo subes a GCS. Las semanas siguientes lo cargas con robyn_refresh(). Así cada semana no entrenas desde cero sino que actualizas incrementalmente—el tiempo de cómputo baja de 2 horas a 15 minutos.
Las métricas holdout se guardan como JSON, se escriben en BigQuery, y tienes un gráfico de tendencia en Looker Studio. Si NRMSE salta de repente (de 8% a 18%), dispara una alerta—el modelo se degradó, necesita re-calibración. Sin este monitoreo, MMM falla silenciosamente; una mala asignación de presupuesto pasa desapercibida durante 3 meses.
Conectando la Salida del Modelo al Mecanismo de Decisión
La salida de Robyn no es un gráfico de contribución por canal, es una tabla de ROAS marginal. El retorno de cada euro adicional gastado en cada canal. Con esto ejecutas un optimizador de presupuesto: si el ROAS marginal de TV es 2 y el de búsqueda es 5, debes desplazar presupuesto hacia búsqueda. Pero esta optimización mecánica puede chocar con la estrategia de marca—si TV se ejecuta para conciencia de marca, mirar solo su ROAS a corto plazo es engañoso.
Por eso los resultados de MMM no deben ser una herramienta de decisión aislada, sino sintetizados con otras señales en la capa de análisis de datos: estudio de brand lift, test de incrementalidad, valor de vida útil del cliente. Si Robyn dice contribución del 30% pero un geo-lift test encuentra 15%, necesitas reconciliar—hay un error en los supuestos del modelo (por ejemplo, decay de adstock configurado demasiado alto).
En producción, MMM se actualiza semanalmente pero las decisiones de presupuesto se toman mensuales o trimestrales. El modelo corre cada semana, las métricas entran en tendencia, pero observas el promedio de 4 semanas. Cambios basados en una sola semana crean volatilidad. La validación holdout también es de 4 semanas, así que el ciclo de revisión de presupuesto debe alinearse con la ventana holdout.
Por último, MMM no reemplaza la atribución incremental—la complementa. Datos de GA4 last-click para tácticas a corto plazo, MMM para estrategia a largo plazo. Cuando presentas ambos al nivel C, viene la pregunta "¿cuál es correcto?". La respuesta: ambos son correctos en su contexto; GA4 muestra el journey del usuario, MMM muestra incrementalidad agregada. Para decisiones de presupuesto se toma un promedio ponderado (por ejemplo, 60% MMM, 40% GA4). Ajustas esta fórmula de mezcla según la cultura corporativa y el nivel de madurez en datos.
Marketing Mix Modeling ya no es un ejercicio académico sino un módulo integral del pipeline de datos de producción. Robyn hace posible esta transición porque convierte conceptos estadísticos como adstock, saturación y holdout en componentes parametrizables, versionables y automatizables. Pero ejecutar un script Robyn una vez y obtener un PDF no es suficiente—necesitas el ciclo semanal de refresh, monitoreo holdout y bucle del allocator de presupuesto. Implementar esto en un stack BigQuery + dbt + Airflow es ideal; así las salidas de MMM alimentan el motor de decisión en tiempo real y la asignación se ajusta automáticamente cuando el performance de canales cambia. Ahora tienes Robyn en las manos; el siguiente paso es trasladarlo de un notebook aislado a un pipeline operacional.