Marketing Mix Modeling (MMM) è tornato prepotentemente verso la fine del 2020 con il collasso dell'attribution basata su cookie. Ma il passaggio dai paper accademici all'ambiente production è un livello completamente diverso. Robyn, open-sourced da Meta nel 2021, ancora questo passaggio alla disciplina dell'ingegneria: curve di saturazione, adstock decay e holdout validation — concetti statistici trasformati in strumenti concreti per spostare il codice R da notebook verso pipeline operative. In questo articolo mostriamo come configurare in production i tre meccanismi che costituiscono il cuore di Robyn — il decadimento temporale dell'effetto pubblicitario, il raggiungimento della saturazione nella relazione spesa-ritorno e il processo di holdout che testa il potere predittivo del modello.

Adstock Decay: Distribuzione dell'Effetto Pubblicitario nel Tempo

Uno spot TV trasmesso non genera vendite il giorno stesso, ma produce effetto per una settimana. Un annuncio di ricerca a pagamento può convertire nel secondo in cui viene cliccato, ma il brand recall triggerizza conversioni ancora 3 giorni dopo. Il termine adstock cattura matematicamente questo ritardo temporale. In Robyn esistono due tipologie di adstock: geometric e Weibull. Geometric implementa il decadimento esponenziale semplice; ogni giorno l'effetto del giorno precedente viene moltiplicato per il parametro theta. Weibull è più flessibile — controlla indipendentemente la fase di salita e discesa della curva di effetto.

Nel setup pratico, calibri i parametri di adstock in base al tipo di canale. La ricerca a pagamento di solito richiede theta=0.3 (decadimento rapido), TV theta=0.7 (coda lunga), display intorno a theta=0.5. Questi valori non sono arbitrari — li trovi tramite grid search su holdout set di periodi precedenti. Nella funzione robyn_inputs() di Robyn configuri l'argomento adstock canale per canale:

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)
  )
)

Qui specifichi l'intervallo c(min, max); l'algoritmo di ottimizzazione Nevergrad scandaglia questo spazio per trovare il miglior valore di theta. Se usi Weibull invece di geometric, aggiungi anche i parametri di shape e scale. Il vantaggio di Weibull è una migliore capacità di fit per canali a "picco ritardato" come display — effetto basso i primi 2 giorni, zenit nei giorni 3-5.

Una configurazione scorretta di adstock fa sì che il modello distribuisca male la contribuzione dei canali. Se modelli TV con geometric theta=0.1, attribuisci l'effetto solo al giorno di trasmissione e perdi il traffico organico delle settimane successive. Al contrario, assegnando a ricerca a pagamento theta=0.9, attribuisci le vendite di oggi ai click di una settimana fa — illogico. Per questo motivo il setup di adstock deve essere calibrato alle caratteristiche del canale e vincolato dalla domain knowledge.

Curva di Saturazione: Relazione Spesa-Ritorno che Raggiunge il Saturation Point

La regressione lineare assume che ogni euro di spesa generi lo stesso ritorno. In realtà i primi 10 mila euro producono ROAS 8, a 100 mila scende a 3, a 1 milione cade sotto 1 — il ritorno marginale segue una curva decrescente. La saturazione è la trasformazione che modella questa curva. Il tipo di saturazione più comune in Robyn è Hill equation (Michaelis-Menten):

y = Vmax * (x^S) / (K^S + x^S)

Dove Vmax è l'effetto massimo, K è il livello di spesa che raggiunge il mezzo della saturazione (punto di inflessione), S è la pendenza della curva (shape). Un K basso significa saturazione rapida del canale, un K alto significa saturazione tardiva. Quando S>1 la curva assume forma S — inizio lento, mezzo rapido, fine lento.

In Robyn definisci i parametri Hill canale per canale:

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 corrisponde al parametro S di Hill, gammas al parametro K (notazione di Robyn). L'ottimizzazione scandaglia questi intervalli per il miglior fit. Ma non lasciare la ricerca completamente cieca — se già spendi l'80% del budget TV, la saturazione deve essere >90%, altrimenti il modello produce ROAS marginali irrealistici.

La configurazione della saturazione impatta direttamente sulla strategia di allocazione del budget. Se il modello calibra bene la curva di saturazione, puoi calcolare il ROAS marginale per ogni canale e re-allocare il budget. La funzione robyn_allocator() di Robyn fa esattamente questo — tenendo fisso il budget totale, quale canale preleviamo e quale canale finanziamo per massimizzare le vendite? Ma questa raccomandazione è valida solo se i parametri di saturazione sono corretti. Un valore K sbagliato significa milioni di euro in allocazioni errate.

Holdout Validation: Test del Potere Predittivo del Modello

Il rischio maggiore di MMM è l'overfitting — il modello memorizza i dati storici senza generalizzare al futuro. Per evitare ciò occorre un holdout validation su serie temporali. Nella configurazione di Robyn, metti da parte le ultime 4-8 settimane come set di holdout, il modello si allena sui dati rimanenti e poi fa previsioni nel periodo holdout. Se NRMSE (Normalized Root Mean Square Error) e MAPE (Mean Absolute Percentage Error) sono bassi, il modello generalizza.

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 mette da parte le ultime 4 settimane come holdout. Il modello si allena senza vederle, poi fa previsioni. L'output di Robyn mostra l'NRMSE di holdout per ogni modello — sotto il 10% è buono, sopra il 20% è sospetto. Ma non decidere basandoti su una sola metrica; verifica se il periodo holdout contiene anomalie (campagne, giorni festivi). Se Black Friday ricade nella settimana di holdout, il modello underestima perché il suo pattern di demand normale non contiene picchi di questo tipo.

Dopo la validazione di holdout è pratica comune re-allenare il modello su tutti i dati — il modello finale usa tutti gli osservazioni, ma gli iper-parametri sono scelti in base ai risultati di holdout. Questo ciclo "train-validate-finalize". In Robyn lo fai 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 aggiorna il modello con i nuovi dati delle ultime 4 settimane ma mantiene fissi i parametri di saturazione e adstock (la calibrazione è preservata). Questo è il fondamento di una pipeline che gira continuamente in production — ogni settimana aggiungi una riga, il modello si ri-allena, la dashboard si aggiorna.

Portare la Pipeline di Robyn in Production

Robyn non è uno script R isolato da eseguire occasionalmente, ma uno strumento che deve integrarsi in una pipeline operativa di data. L'architettura tipica: tabella spese marketing in BigQuery + tabella conversioni da GA4 + tabella revenue da CRM → aggregazione settimanale con dbt → trigger di uno script R Robyn in un DAG Cloud Composer (Airflow) → risultato in JSON su una dashboard Looker Studio. Questo stack opera all'interno dell'architettura dati first-party.

Il primo passo è standardizzare lo schema dei dati. Robyn si aspetta un input dt_input con questa struttura: DATE (settimanale), revenue, tv_spend, search_spend, facebook_impressions e altri — una colonna per canale. La distinzione organic/paid deve essere esplicita altrimenti il modello non riesce a fare attribution. Le settimane mancanti devono essere imputate (zero o interpolazione), gli outlier devono essere flaggati. Un modello dbt di esempio:

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

Questa tabella viene esportata da BigQuery come CSV e alimenta lo script Robyn, oppure lo script accede direttamente ai dati con il pacchetto R bigrquery. La seconda opzione è preferibile — garantisce data freshness.

Nel DAG di Airflow, il passo Robyn si presenta così:

from airflow.operators.bash import BashOperator

run_robyn = BashOperator(
    task_id='run_robyn_mmm',
    bash_command='Rscript /path/to/robyn_model.R ',
    dag=dag
)

All'interno dello script, salvi l'oggetto modello con robyn_save() in formato RDS e lo carichi su GCS. Nelle settimane successive, carica il modello e fai robyn_refresh() — non un training da zero ogni volta, ma un update incrementale. Il tempo di compute scende da 2 ore a 15 minuti.

Le metriche di holdout vengono salvate in JSON, scritte in BigQuery e rappresentate come grafici di trend in Looker Studio. Se l'NRMSE salta improvvisamente (da 8% a 18%) genera un alert — il modello si è degradato e richiede ri-calibrazione. Senza questo monitoraggio, MMM fallisce silenziosamente; l'allocazione budget sbagliata passa inosservata per 3 mesi.

Collegare l'Output del Modello al Meccanismo Decisionale

L'output di Robyn non è un pie chart di contribuzione per canale, ma una tabella di ROAS marginale. Il ritorno dell'ultimo euro speso su ogni canale. Con questo dati, esegui un budget optimizer: se il ROAS marginale di TV è 2 e quello di ricerca è 5, shift verso ricerca. Ma questa ottimizzazione meccanica può scontrarsi con la strategia brand — se TV è allocato per brand awareness, guardarvi solo lo short-term ROAS è fuorviante.

Perciò gli output di MMM non devono essere usati come strumento decisionale isolato, ma sintetizzati all'interno di un livello di analisi dati insieme ad altri segnali: brand lift study, incrementality test, customer lifetime value. Se Robyn dice contribuzione al 30% ma un test di geo-lift trova il 15%, occorre riconciliare — c'è un errore nelle assunzioni del modello (per esempio l'adstock decay è set troppo alto).

In production, MMM si aggiorna settimanalmente ma le decisioni di budget avvengono mensili o trimestrali. Il modello gira ogni settimana, le metriche entrano in un trend dashboard, ma decidi basandoti sulla media di 4 settimane. Fare shift milionari ogni settimana in base a una run singola introduce volatilità. L'holdout validation è anche su 4 settimane, quindi il ciclo di budget review deve essere allineato con la finestra di holdout.

Infine, MMM non rimpiazza l'attribution incrementale — la complementa. GA4 last-click è per tattica a breve termine, MMM per strategia di lungo periodo. Quando presenti al C-level su due dashboard diversi la domanda "quale è corretta?" è inevitabile. La risposta: entrambi sono corretti nel loro contesto; GA4 mostra il customer journey, MMM mostra l'incrementality aggregata. Per la decisione di budget, calcoli una media ponderata dei due (per esempio 60% MMM, 40% GA4). Questa formula di blending si calibra sulla cultura dell'organizzazione e il livello di data maturity.


Marketing Mix Modeling non è più un esercizio accademico, ma un modulo integrato di una pipeline operativa di dati. Robyn abilita questo passaggio perché trasforma concetti statistici — adstock, saturazione, holdout — in componenti parametrizzabili, versionabili, automatizzabili. Tuttavia eseguire uno script Robyn una volta e scaricare un PDF non è sufficiente — occorrono il ciclo di refresh settimanale, il monitoraggio di holdout e il loop di budget allocator. Implementare tutto questo nello stack BigQuery + dbt + Airflow è l'ideale; così gli output di MMM alimentano un motore decisionale real-time e quando la performance di un canale cambia, l'allocazione si aggiusta automaticamente. Ora hai Robyn in mano; il passo successivo è portarlo dal notebook isolato verso la pipeline operazionale.