Il 21 gennaio 2015 Optimizely — una delle piattaforme di A/B testing più usate al mondo — accese per tutti i suoi clienti un motore statistico completamente nuovo, il New Stats Engine.
Non era un capriccio tecnico: il vecchio motore, costruito attorno a un classico t-test a orizzonte fisso (Fixed Horizon) e sviluppato con statistici di Stanford, aveva un difetto che riguardava chiunque guardasse i risultati di un test prima della fine. E noi i risultati di un test li guardiamo sempre prima della fine.
Il problema lo avevano misurato loro stessi, simulando dei test A/A — due varianti identiche, dove per costruzione nessuna è migliore dell’altra, quindi qualunque “vincitore” dichiarato è un falso allarme.
Stando ai dati pubblicati da Optimizely, su test da 5.000 visitatori chi controllava i numeri dopo ogni visitatore vedeva il 57% dei test A/A dichiarare un falso vincitore almeno una volta; controllando ogni 500 visitatori il numero scendeva al 26%, ogni 1.000 al 20%. Numeri da brivido per uno strumento che dovrebbe servire a decidere con rigore. La riscrittura — inferenza sequenziale più controllo del false discovery rate, quella che chiamano always-valid — serviva proprio a riportare l’errore, come scrivevano loro, “da oltre il 30% al 5%”.
È lo stesso inganno che abbiamo incontrato chiudendo l’articolo sulla regressione verso la media: lì selezionavamo le pagine messe peggio — un istante estremo nello spazio dei dati — e ci facevamo ingannare dal loro rientro. Qui selezioniamo un istante estremo nel tempo: ci fermiamo appena il test ci dà ragione. Il meccanismo è cugino, il rischio identico.
Chi gestisce un A/B test lo conosce bene: il test è in corso, i dati arrivano giorno dopo giorno, e la tentazione di sbirciare la dashboard è irresistibile.
Il peeking — letteralmente “sbirciare” — non è il semplice atto di guardare: è guardare riservandosi di fermare il test nel momento in cui il risultato diventa significativo. È quel “ottimo, la variante B ha superato la soglia, chiudiamo qui e dichiariamo il vincitore” detto a metà raccolta dati.
Il punto delicato è che ogni sguardo accompagnato dalla possibilità di fermarsi è un test statistico in più.
Un singolo test con soglia al 5% accetta, per definizione, un 5% di probabilità di gridare al vincitore quando in realtà non c’è alcuna differenza. Ma se quello stesso test lo ripetiamo venti volte lungo la raccolta, e ci basta che una sola di quelle venti volte superi la soglia per fermarci e cantare vittoria, allora le probabilità di inciampare in un falso positivo non sono più il 5%: si accumulano a ogni sguardo.
Non è la solita molteplicità di chi confronta dieci varianti insieme. Qui la molteplicità è nascosta nel tempo: una sola variante, guardata tante volte. È la stessa logica per cui se si lancia una moneta una volta sola un risultato strano è raro, ma se ci si concede di guardare dopo ogni lancio e di fermarsi al primo momento favorevole, prima o poi quel momento arriva — e lo si scambia per un segnale.
Le parole convincono fino a un certo punto; i numeri molto di più. Simulo in R un A/A test, cioè due varianti con esattamente lo stesso tasso di conversione vero (il 10%): qualunque differenza che emerge è rumore, e qualunque “vittoria” dichiarata è un falso positivo per costruzione.
Preparo il terreno fissando il seme del generatore casuale (così i numeri sono riproducibili), la funzione che calcola il p-value del confronto tra due proporzioni, e la funzione che simula un singolo esperimento e dice se a un certo punto ha dichiarato un (falso) vincitore:
set.seed(2025)
p_vero <- 0.10 # stesso conversion rate per A e B (H0 vera)
n_arm <- 2000 # visitatori per variante a fine test
n_sim <- 4000 # numero di esperimenti simulati
alpha <- 0.05
sguardi <- 20 # quante volte "sbirciamo" durante la raccolta
look_at <- round(seq(n_arm / sguardi, n_arm, length.out = sguardi))
# p-value z-test due proporzioni, due code
pval_ab <- function(xa, na, xb, nb) {
pp <- (xa + xb) / (na + nb)
se <- sqrt(pp * (1 - pp) * (1 / na + 1 / nb))
2 * pnorm(-abs((xa / na - xb / nb) / se))
}
# un esperimento A/A: TRUE se dichiara un (falso) vincitore
esperimento <- function(soglia, guarda) {
a <- cumsum(rbinom(n_arm, 1, p_vero))
b <- cumsum(rbinom(n_arm, 1, p_vero))
for (k in guarda) {
p <- pval_ab(a[k], k, b[k], k)
if (!is.na(p) && p < soglia) return(TRUE)
}
FALSE
} Cominciamo dal comportamento corretto: un solo test, alla fine, sui 2.000 visitatori per variante. Lo eseguo 4.000 volte e conto quante dichiarano un vincitore:
# orizzonte fisso: un solo test, alla fine
fisso <- mean(replicate(n_sim, esperimento(alpha, n_arm)))
cat(sprintf("Orizzonte fisso: %.1f%% falsi positivi\n", 100 * fisso))
# Orizzonte fisso: 5.0% falsi positivi Esce 5,0%: esattamente il livello che avevamo dichiarato con la soglia al 5%. Il test, usato come si deve, mantiene la promessa.
Ora cambio una cosa sola: invece di guardare una volta alla fine, guardo venti volte durante la raccolta e mi fermo al primo momento in cui il p-value scende sotto 0,05. Aggiungo gli sguardi intermedi e rieseguo:
# peeking: test a ogni sguardo, stop al primo significativo
peek <- mean(replicate(n_sim, esperimento(alpha, look_at)))
cat(sprintf("Peeking (%d sguardi): %.1f%% falsi positivi\n", sguardi, 100 * peek))
# Peeking (20 sguardi): 24.3% falsi positivi Da 5,0% a 24,3%.
Gli stessi dati, lo stesso test, la stessa soglia: l’unica cosa cambiata è quando abbiamo deciso di guardare, e il tasso di falsi positivi è quasi quintuplicato. Quasi un test A/A su quattro, in cui le due varianti sono identiche per costruzione, ci convince di aver trovato un vincitore. Il 24,3% della nostra simulazione e il 30% riportato da Optimizely raccontano la stessa storia con dati diversi: sbirciare non è un peccato veniale, è il modo più efficace per ingannarsi da soli.
La cura più semplice è anche la più antipatica: decidere prima quanti dati raccogliere, e poi avere la disciplina di aspettare fino alla fine senza fermarsi in anticipo, qualunque cosa dica la dashboard nel frattempo.
È quello che la simulazione ci ha appena mostrato: con un solo test alla fine, il falso positivo resta inchiodato al 5% promesso. Nessuna magia, solo l’aver eliminato gli sguardi opportunistici.
“Quanti dati” non è una cifra a caso: dipende da quanto è piccola la differenza che vogliamo essere in grado di cogliere e da quanta certezza pretendiamo. È il calcolo della dimensione campionaria, che si fa prima di lanciare il test con il nostro calcolatore di significatività e che poggia sui concetti di effect size e power analysis.
Fissato quel numero, l’orizzonte fisso è la strada più sicura: nessuna correzione statistica da applicare, nessuna soglia da ritoccare. Si paga però un prezzo in pazienza — bisogna resistere alla curiosità per giorni o settimane — e questo, nella realtà operativa, è esattamente ciò che quasi nessuno riesce a fare.
E se monitorare in corsa fosse davvero necessario — perché un test che sta andando malissimo va fermato, perché gli stakeholder vogliono aggiornamenti?
Allora la strada non è guardare di nascosto con la soglia di sempre, ma guardare apertamente con una soglia più severa. L’idea è semplice: se a ogni sguardo alziamo l’asticella, rendendo più difficile gridare al vincitore in ciascuna occasione, possiamo fare in modo che l’errore complessivo — sommato su tutti gli sguardi — resti il 5% che volevamo. Calibro in R la soglia per-sguardo, provando valori via via più stringenti sugli stessi venti sguardi di prima:
# soglia per-sguardo più severa che riporta l'errore complessivo ~5%
for (sg in c(0.05, 0.02, 0.01, 0.005)) {
fp <- mean(replicate(n_sim, esperimento(sg, look_at)))
cat(sprintf(" soglia %.3f -> %.1f%% complessivo\n", sg, 100 * fp))
}
# soglia 0.050 -> 25.1% complessivo
# soglia 0.020 -> 11.7% complessivo
# soglia 0.010 -> 6.6% complessivo
# soglia 0.005 -> 3.3% complessivo Come si vede, la soglia abituale dello 0,05 produce un 25,1% di errore complessivo (di nuovo il disastro del peeking), ma man mano che la rendiamo più severa l’errore rientra: intorno allo 0,01 — una soglia cinque volte più stringente di quella standard — l’errore complessivo torna vicino al 5% nominale. È il prezzo da pagare per il diritto di sbirciare: a ogni singolo sguardo si pretende molta più evidenza, in cambio della libertà di guardare spesso.
Quella appena mostrata è una versione artigianale e a soglia costante dell’idea. I confini “da manuale” — più raffinati, con soglie che cambiano nel corso del test, come quelli di Pocock o O’Brien-Fleming — si ottengono in R con il pacchetto gsDesign, e i tool commerciali come Optimizely usano una variante always-valid (la cosiddetta mSPRT) della stessa idea di fondo.
Cambia la matematica fine, non il principio: per guardare spesso senza barare bisogna chiedere, a ogni sguardo, più evidenza di quanta ne chiederebbe un test singolo.
Un avvertimento: un risultato visto durante il test, da solo, non prova niente: conta quando si è deciso di guardarlo.
Lo stesso p-value sotto 0,05 significa cose diverse a seconda che sia l’unico test a orizzonte fisso o il primo dei venti in cui ci si è riservati di fermarsi. Senza dichiarare in anticipo come e quando si guarderanno i dati, qualunque “vincitore” emerso in corsa è sospetto.
Per sentire il meccanismo da vicino, si parte dallo script e si cambia un solo parametro: il numero di sguardi.
Si prova a passare da un monitoraggio settimanale (pochi sguardi) a uno giornaliero (molti sguardi) e si riesegue la simulazione del peeking. Cosa aspettarsi: più di frequente si sbircia, più sale il tasso di falsi positivi — la frequenza degli sguardi è la benzina del problema. Poi si rifà la calibrazione della soglia con quel nuovo numero di sguardi e si verifica che, scegliendo una soglia abbastanza severa, l’errore complessivo torna comunque sotto controllo. È la dimostrazione, fatta in prima persona, che il peeking non è una maledizione: è solo un conto che va pagato.
C’è un’ultima trappola di questa famiglia, forse la più subdola, perché non si nasconde nei nostri dati ma in quelli che ci raccontano gli altri. Quando leggiamo il case study di un’agenzia — “abbiamo aumentato le conversioni del 300% con questa tattica” — stiamo guardando un sopravvissuto: i mille tentativi identici che sono falliti non li racconta nessuno. È il survivorship bias, il motivo per cui i case study mentono anche quando dicono il vero, ed è il prossimo passo del nostro viaggio tra i tranelli dei dati di marketing.
Su peeking, arresto anticipato e test sequenziali il riferimento — in inglese — resta Trustworthy Online Controlled Experiments di Ron Kohavi, Diane Tang e Ya Xu: scritto da chi ha guidato le piattaforme di sperimentazione di Microsoft, Google e LinkedIn, dedica pagine esplicite a tutti i modi in cui un A/B test in corsa può ingannarci, e a come difendersi. È il libro che tira fuori dal cassetto chiunque debba prendere sul serio gli esperimenti online.
Nell'aeronautica militare israeliana, racconta Daniel Kahneman, gli istruttori erano convinti di una cosa: lodare un…
Chi guarda i dati di un sito lo fa di continuo, spesso senza nemmeno accorgersene,…
Abbiamo chiuso l'articolo sul calcolatore di significatività con una promessa. Dicevamo che il p-value risponde…
Il nostro A/B test è arrivato alla fine: la variante B mostra un tasso di…
C'è una domanda che torna, puntuale, ogni volta che pubblico un articolo di questo percorso:…
È l'ultimo giorno del mese. Stiamo preparando il report SEO per il cliente principale. Apriamo…