  {"id":3846,"date":"2026-06-23T10:40:33","date_gmt":"2026-06-23T09:40:33","guid":{"rendered":"https:\/\/www.gironi.it\/blog\/?p=3846"},"modified":"2026-06-23T10:40:34","modified_gmt":"2026-06-23T09:40:34","slug":"peeking-problem","status":"publish","type":"post","link":"https:\/\/www.gironi.it\/blog\/peeking-problem\/","title":{"rendered":"Il peeking problem: perch\u00e9 sbirciare l&#8217;A\/B test gonfia i falsi positivi"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Il 21 gennaio 2015 Optimizely \u2014 una delle piattaforme di A\/B testing pi\u00f9 usate al mondo \u2014 accese per tutti i suoi clienti un motore statistico completamente nuovo, il <em>New Stats Engine<\/em>. <br>Non era un capriccio tecnico: il vecchio motore, costruito attorno a un classico t-test a orizzonte fisso (<em>Fixed Horizon<\/em>) 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 <em>sempre<\/em> prima della fine.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Il problema lo avevano misurato loro stessi, simulando dei test A\/A \u2014 due varianti identiche, dove per costruzione nessuna \u00e8 migliore dell&#8217;altra, quindi qualunque &#8220;vincitore&#8221; dichiarato \u00e8 un falso allarme. <br>Stando ai dati pubblicati da Optimizely, su test da 5.000 visitatori chi controllava i numeri dopo <em>ogni<\/em> visitatore vedeva il <strong>57% dei test A\/A dichiarare un falso vincitore almeno una volta<\/strong>; 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 \u2014 inferenza sequenziale pi\u00f9 controllo del false discovery rate, quella che chiamano always-valid \u2014 serviva proprio a riportare l&#8217;errore, come scrivevano loro, &#8220;da oltre il 30% al 5%&#8221;.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u00c8 lo stesso inganno che abbiamo incontrato chiudendo l&#8217;articolo sulla <a href=\"https:\/\/www.gironi.it\/blog\/regressione-verso-la-media\/\">regressione verso la media<\/a>: l\u00ec selezionavamo le pagine messe peggio \u2014 un istante estremo nello <em>spazio<\/em> dei dati \u2014 e ci facevamo ingannare dal loro rientro. Qui selezioniamo un istante estremo nel <em>tempo<\/em>: ci fermiamo appena il test ci d\u00e0 ragione. Il meccanismo \u00e8 cugino, il rischio identico.<\/p>\n\n\n\n<!--more-->\n\n\n\n<h2 class=\"wp-block-heading\">Cos&#8217;\u00e8 il peeking<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Chi gestisce un <a href=\"https:\/\/www.gironi.it\/blog\/ab-testing\/\">A\/B test<\/a> lo conosce bene: il test \u00e8 in corso, i dati arrivano giorno dopo giorno, e la tentazione di sbirciare la dashboard \u00e8 irresistibile. <br>Il <em>peeking<\/em> \u2014 letteralmente &#8220;sbirciare&#8221; \u2014 non \u00e8 il semplice atto di guardare: \u00e8 guardare <em>riservandosi di fermare il test<\/em> nel momento in cui il risultato diventa significativo. \u00c8 quel &#8220;ottimo, la variante B ha superato la soglia, chiudiamo qui e dichiariamo il vincitore&#8221; detto a met\u00e0 raccolta dati.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Il punto delicato \u00e8 che ogni sguardo accompagnato dalla possibilit\u00e0 di fermarsi <strong>\u00e8 un test statistico in pi\u00f9<\/strong>. <br>Un singolo test con soglia al 5% accetta, per definizione, un 5% di probabilit\u00e0 di gridare al vincitore quando in realt\u00e0 non c&#8217;\u00e8 alcuna differenza. Ma se quello stesso test lo ripetiamo venti volte lungo la raccolta, e ci basta che <em>una sola<\/em> di quelle venti volte superi la soglia per fermarci e cantare vittoria, allora le probabilit\u00e0 di inciampare in un falso positivo non sono pi\u00f9 il 5%: si <strong>accumulano<\/strong> a ogni sguardo.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Non \u00e8 la solita molteplicit\u00e0 di chi confronta dieci varianti insieme. Qui la molteplicit\u00e0 \u00e8 nascosta nel <em>tempo<\/em>: una sola variante, guardata tante volte. \u00c8 la stessa logica per cui se si lancia una moneta una volta sola un risultato strano \u00e8 raro, ma se ci si concede di guardare dopo ogni lancio e di fermarsi al primo momento favorevole, prima o poi quel momento arriva \u2014 e lo si scambia per un segnale.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Quanto costa sbirciare: una simulazione<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Le parole convincono fino a un certo punto; i numeri molto di pi\u00f9. Simulo in R un A\/A test, cio\u00e8 due varianti con <strong>esattamente lo stesso<\/strong> tasso di conversione vero (il 10%): qualunque differenza che emerge \u00e8 rumore, e qualunque &#8220;vittoria&#8221; dichiarata \u00e8 un falso positivo per costruzione. <br>Preparo il terreno fissando il seme del generatore casuale (cos\u00ec 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:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>set.seed(2025)\n\np_vero  &lt;- 0.10    # stesso conversion rate per A e B (H0 vera)\nn_arm   &lt;- 2000    # visitatori per variante a fine test\nn_sim   &lt;- 4000    # numero di esperimenti simulati\nalpha   &lt;- 0.05\nsguardi &lt;- 20      # quante volte \"sbirciamo\" durante la raccolta\nlook_at &lt;- round(seq(n_arm \/ sguardi, n_arm, length.out = sguardi))\n\n# p-value z-test due proporzioni, due code\npval_ab &lt;- function(xa, na, xb, nb) {\n  pp &lt;- (xa + xb) \/ (na + nb)\n  se &lt;- sqrt(pp * (1 - pp) * (1 \/ na + 1 \/ nb))\n  2 * pnorm(-abs((xa \/ na - xb \/ nb) \/ se))\n}\n\n# un esperimento A\/A: TRUE se dichiara un (falso) vincitore\nesperimento &lt;- function(soglia, guarda) {\n  a &lt;- cumsum(rbinom(n_arm, 1, p_vero))\n  b &lt;- cumsum(rbinom(n_arm, 1, p_vero))\n  for (k in guarda) {\n    p &lt;- pval_ab(a[k], k, b[k], k)\n    if (!is.na(p) &amp;&amp; p &lt; soglia) return(TRUE)\n  }\n  FALSE\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">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:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># orizzonte fisso: un solo test, alla fine\nfisso &lt;- mean(replicate(n_sim, esperimento(alpha, n_arm)))\ncat(sprintf(\"Orizzonte fisso: %.1f%% falsi positivi\\n\", 100 * fisso))\n# Orizzonte fisso: 5.0% falsi positivi<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Esce <strong>5,0%<\/strong>: esattamente il livello che avevamo dichiarato con la soglia al 5%. Il test, usato come si deve, mantiene la promessa. <br>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:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># peeking: test a ogni sguardo, stop al primo significativo\npeek &lt;- mean(replicate(n_sim, esperimento(alpha, look_at)))\ncat(sprintf(\"Peeking (%d sguardi): %.1f%% falsi positivi\\n\", sguardi, 100 * peek))\n# Peeking (20 sguardi): 24.3% falsi positivi<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Da <strong>5,0%<\/strong> a <strong>24,3%<\/strong>. <br><strong>Gli stessi dati, lo stesso test, la stessa soglia: l&#8217;unica cosa cambiata \u00e8 quando abbiamo deciso di guardare, e il tasso di falsi positivi \u00e8 quasi quintuplicato.<\/strong> 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 \u00e8 un peccato veniale, \u00e8 il modo pi\u00f9 efficace per ingannarsi da soli.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Soluzione 1: l&#8217;orizzonte fisso<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">La cura pi\u00f9 semplice \u00e8 anche la pi\u00f9 antipatica: decidere <em>prima<\/em> quanti dati raccogliere, e poi avere la disciplina di aspettare fino alla fine senza fermarsi in anticipo, qualunque cosa dica la dashboard nel frattempo. <br>\u00c8 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&#8217;aver eliminato gli sguardi opportunistici.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&#8220;Quanti dati&#8221; non \u00e8 una cifra a caso: dipende da quanto \u00e8 piccola la differenza che vogliamo essere in grado di cogliere e da quanta certezza pretendiamo. \u00c8 il calcolo della dimensione campionaria, che si fa prima di lanciare il test con il nostro <a href=\"https:\/\/www.gironi.it\/blog\/calcolatore-significativita-ab-test\/\">calcolatore di significativit\u00e0<\/a> e che poggia sui concetti di <a href=\"https:\/\/www.gironi.it\/blog\/effect-size-e-power-analysis\/\">effect size e power analysis<\/a>. <br>Fissato quel numero, l&#8217;orizzonte fisso \u00e8 la strada pi\u00f9 sicura: nessuna correzione statistica da applicare, nessuna soglia da ritoccare. Si paga per\u00f2 un prezzo in pazienza \u2014 bisogna resistere alla curiosit\u00e0 per giorni o settimane \u2014 e questo, nella realt\u00e0 operativa, \u00e8 esattamente ci\u00f2 che quasi nessuno riesce a fare.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Soluzione 2: guardare senza barare<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">E se monitorare in corsa fosse davvero necessario \u2014 perch\u00e9 un test che sta andando malissimo va fermato, perch\u00e9 gli stakeholder vogliono aggiornamenti? <br>Allora la strada non \u00e8 guardare di nascosto con la soglia di sempre, ma guardare <em>apertamente<\/em> con una soglia pi\u00f9 severa. L&#8217;idea \u00e8 semplice: se a ogni sguardo alziamo l&#8217;asticella, rendendo pi\u00f9 difficile gridare al vincitore in ciascuna occasione, possiamo fare in modo che l&#8217;errore <strong>complessivo<\/strong> \u2014 sommato su tutti gli sguardi \u2014 resti il 5% che volevamo. Calibro in R la soglia per-sguardo, provando valori via via pi\u00f9 stringenti sugli stessi venti sguardi di prima:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># soglia per-sguardo pi\u00f9 severa che riporta l'errore complessivo ~5%\nfor (sg in c(0.05, 0.02, 0.01, 0.005)) {\n  fp &lt;- mean(replicate(n_sim, esperimento(sg, look_at)))\n  cat(sprintf(\"  soglia %.3f -&gt; %.1f%% complessivo\\n\", sg, 100 * fp))\n}\n#   soglia 0.050 -&gt; 25.1% complessivo\n#   soglia 0.020 -&gt; 11.7% complessivo\n#   soglia 0.010 -&gt;  6.6% complessivo\n#   soglia 0.005 -&gt;  3.3% complessivo<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">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\u00f9 severa l&#8217;errore rientra: <strong>intorno allo 0,01 \u2014 una soglia cinque volte pi\u00f9 stringente di quella standard \u2014 l&#8217;errore complessivo torna vicino al 5% nominale.<\/strong> \u00c8 il prezzo da pagare per il diritto di sbirciare: a ogni singolo sguardo si pretende molta pi\u00f9 evidenza, in cambio della libert\u00e0 di guardare spesso.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Quella appena mostrata \u00e8 una versione artigianale e a soglia costante dell&#8217;idea. I confini &#8220;da manuale&#8221; \u2014 pi\u00f9 raffinati, con soglie che cambiano nel corso del test, come quelli di Pocock o O&#8217;Brien-Fleming \u2014 si ottengono in R con il pacchetto <code>gsDesign<\/code>, e i tool commerciali come Optimizely usano una variante <em>always-valid<\/em> (la cosiddetta mSPRT) della stessa idea di fondo. <br>Cambia la matematica fine, non il principio: per guardare spesso senza barare bisogna chiedere, a ogni sguardo, pi\u00f9 evidenza di quanta ne chiederebbe un test singolo.<\/p>\n\n\n\n<p class=\"has-light-gray-background-color has-background wp-block-paragraph\">Un avvertimento: <strong>un risultato visto durante il test, da solo, non prova niente: conta quando si \u00e8 deciso di guardarlo.<\/strong> <br>Lo stesso p-value sotto 0,05 significa cose diverse a seconda che sia l&#8217;unico test a orizzonte fisso o il primo dei venti in cui ci si \u00e8 riservati di fermarsi. Senza dichiarare in anticipo come e quando si guarderanno i dati, qualunque &#8220;vincitore&#8221; emerso in corsa \u00e8 sospetto.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Prova tu<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Per sentire il meccanismo da vicino, si parte dallo script e si cambia un solo parametro: il numero di <code>sguardi<\/code>. <br>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\u00f9 di frequente si sbircia, pi\u00f9 sale il tasso di falsi positivi \u2014 la frequenza degli sguardi \u00e8 la benzina del problema. Poi si rif\u00e0 la calibrazione della soglia con quel nuovo numero di sguardi e si verifica che, scegliendo una soglia abbastanza severa, l&#8217;errore complessivo torna comunque sotto controllo. \u00c8 la dimostrazione, fatta in prima persona, che il peeking non \u00e8 una maledizione: \u00e8 solo un conto che va pagato.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\">C&#8217;\u00e8 un&#8217;ultima trappola di questa famiglia, forse la pi\u00f9 subdola, perch\u00e9 non si nasconde nei nostri dati ma in quelli che ci raccontano gli altri. Quando leggiamo il case study di un&#8217;agenzia \u2014 &#8220;abbiamo aumentato le conversioni del 300% con questa tattica&#8221; \u2014 stiamo guardando un sopravvissuto: i mille tentativi identici che sono falliti non li racconta nessuno. \u00c8 il <em>survivorship bias<\/em>, il motivo per cui i case study mentono anche quando dicono il vero, ed \u00e8 il prossimo passo del nostro viaggio tra i tranelli dei dati di marketing.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">Per approfondire<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Su peeking, arresto anticipato e test sequenziali il riferimento \u2014 in inglese \u2014 resta <a href=\"https:\/\/www.amazon.it\/dp\/1108724264?tag=consulenzeinf-21\" rel=\"nofollow sponsored noopener\" target=\"_blank\"><em>Trustworthy Online Controlled Experiments<\/em><\/a> 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\u00f2 ingannarci, e a come difendersi. \u00c8 il libro che tira fuori dal cassetto chiunque debba prendere sul serio gli esperimenti online.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Il 21 gennaio 2015 Optimizely \u2014 una delle piattaforme di A\/B testing pi\u00f9 usate al mondo \u2014 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 &hellip; <a href=\"https:\/\/www.gironi.it\/blog\/peeking-problem\/\" class=\"more-link\">Leggi tutto<span class=\"screen-reader-text\"> &#8220;Il peeking problem: perch\u00e9 sbirciare l&#8217;A\/B test gonfia i falsi positivi&#8221;<\/span><\/a><\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_uag_custom_page_level_css":"","footnotes":""},"categories":[629],"tags":[],"class_list":["post-3846","post","type-post","status-publish","format-standard","hentry","category-statistica-it"],"lang":"it","translations":{"it":3846,"en":3847},"uagb_featured_image_src":{"full":false,"thumbnail":false,"medium":false,"medium_large":false,"large":false,"1536x1536":false,"2048x2048":false,"post-thumbnail":false},"uagb_author_info":{"display_name":"Paolo Gironi","author_link":"https:\/\/www.gironi.it\/blog\/author\/autore-articoli\/"},"uagb_comment_info":0,"uagb_excerpt":"Il 21 gennaio 2015 Optimizely \u2014 una delle piattaforme di A\/B testing pi\u00f9 usate al mondo \u2014 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&hellip;","_links":{"self":[{"href":"https:\/\/www.gironi.it\/blog\/wp-json\/wp\/v2\/posts\/3846","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.gironi.it\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.gironi.it\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.gironi.it\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/www.gironi.it\/blog\/wp-json\/wp\/v2\/comments?post=3846"}],"version-history":[{"count":1,"href":"https:\/\/www.gironi.it\/blog\/wp-json\/wp\/v2\/posts\/3846\/revisions"}],"predecessor-version":[{"id":3853,"href":"https:\/\/www.gironi.it\/blog\/wp-json\/wp\/v2\/posts\/3846\/revisions\/3853"}],"wp:attachment":[{"href":"https:\/\/www.gironi.it\/blog\/wp-json\/wp\/v2\/media?parent=3846"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.gironi.it\/blog\/wp-json\/wp\/v2\/categories?post=3846"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.gironi.it\/blog\/wp-json\/wp\/v2\/tags?post=3846"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}