Abbiamo avuto modo di esaminare, nel corso di questo percorso, strumenti per descrivere i dati, per testare ipotesi, per costruire modelli. Ma c’e’ una domanda che precede tutte le altre, e che troppo spesso viene ignorata: questi dati sono affidabili?
In qualsiasi dataset — sessioni giornaliere, click organici, tassi di conversione — possono nascondersi valori che non si comportano come gli altri. Valori che si discostano in modo anomalo dal resto della distribuzione. In statistica li chiamiamo outlier, o valori anomali.
Un punto va chiarito subito: un valore anomalo non e’ necessariamente un errore. Puo’ essere un errore di misurazione, certo (un tag di tracciamento rotto, un bot che gonfia le sessioni). Ma puo’ anche essere il segnale piu’ importante dell’intero dataset: un aggiornamento dell’algoritmo di Google, un contenuto che diventa virale, un problema tecnico che abbatte il traffico. La questione non e’ eliminare le anomalie, ma riconoscerle — e poi decidere cosa farne.
In questo articolo esaminiamo tre metodi statistici per identificare i valori anomali, dal piu’ intuitivo al piu’ formale. Per ciascuno vedremo la logica, i limiti e l’applicazione pratica con R.
Di cosa parleremo
Il dataset di lavoro
Per rendere le cose concrete, costruiamo un dataset simulato ma realistico: le sessioni giornaliere di un sito web nell’arco di un anno. I dati seguono approssimativamente una distribuzione normale con media 250 e deviazione standard 50, ma con cinque anomalie inserite intenzionalmente — tre cali drastici e due picchi.
Generiamo i dati in R:
set.seed(42)
n <- 365
sessioni <- round(rnorm(n, mean = 250, sd = 50))
sessioni[sessioni < 0] <- 0
# Inietto 5 anomalie realistiche
sessioni[45] <- 38 # giorno 45: problema tecnico
sessioni[120] <- 580 # giorno 120: articolo virale
sessioni[200] <- 22 # giorno 200: update Google
sessioni[300] <- 510 # giorno 300: menzione su social
sessioni[350] <- 15 # giorno 350: server down
Visualizziamo l’andamento con un semplice grafico temporale:
plot(1:n, sessioni, type = "l", col = "steelblue",
xlab = "Giorno", ylab = "Sessioni",
main = "Sessioni giornaliere - un anno di traffico")
abline(h = mean(sessioni), col = "red", lty = 2)
A occhio, qualche picco e qualche calo si nota. Ma dove tracciamo il confine tra variazione naturale e anomalia? Servono criteri oggettivi.
Metodo 1: lo z-score
Abbiamo incontrato lo z-score parlando della distribuzione normale. Lo z-score ci dice quante deviazioni standard un valore dista dalla media:
\(z = \frac{x – \mu}{\sigma} \\
\)
dove \(x\) e’ il valore osservato, \(\mu\) e’ la media e \(\sigma\) la deviazione standard. Un valore con z-score pari a 2 si trova a due deviazioni standard dalla media; uno con z-score pari a -3 si trova a tre deviazioni standard sotto la media.
Ricordiamo la regola empirica: in una distribuzione normale, circa il 99.7% dei dati cade entro tre deviazioni standard dalla media. Un valore con |z| > 3 e’ dunque estremamente raro — meno dello 0.3% di probabilita’ sotto ipotesi di normalita’.
Calcoliamo gli z-score per il nostro dataset e identifichiamo le anomalie:
z <- (sessioni - mean(sessioni)) / sd(sessioni)
# Soglia conservativa: |z| > 3
anomalie_z3 <- which(abs(z) > 3)
cat("Giorni anomali (|z| > 3):", anomalie_z3, "\n")
cat("Sessioni:", sessioni[anomalie_z3], "\n")
cat("Z-score:", round(z[anomalie_z3], 2), "\n")
Il risultato:
Giorni anomali (|z| > 3): 45 120 200 300 350
Sessioni: 38 580 22 510 15
Z-score: -3.75 5.92 -4.03 4.67 -4.16
Con la soglia |z| > 3, lo z-score identifica esattamente le cinque anomalie che avevamo inserito. Nessun falso positivo, nessun falso negativo — un risultato quasi perfetto.
Ma attenzione: se abbassiamo la soglia a |z| > 2, le anomalie salgono a 14. Molti di quei valori sono semplicemente dati nella coda della distribuzione, non anomalie reali. La scelta della soglia non e’ un dettaglio tecnico: e’ una decisione analitica che dipende da quanto siamo disposti a tollerare falsi allarmi.
C’e’ un limite importante in questo metodo. Lo z-score assume che i dati seguano (almeno approssimativamente) una distribuzione normale. Se la distribuzione e’ fortemente asimmetrica — e i dati di traffico web spesso lo sono, con lunghe code a destra — la media e la deviazione standard possono essere distorte proprio dagli outlier che stiamo cercando di individuare. E’ un circolo vizioso: le anomalie influenzano le statistiche che usiamo per trovarle.
Metodo 2: IQR e il metodo di Tukey
Le misure di posizione — quartili e mediana — ci offrono un approccio che non richiede ipotesi sulla forma della distribuzione. Il metodo di Tukey, dal nome del grande statistico John Tukey, usa l’intervallo interquartile (IQR) come metro di misura.
L’IQR, come abbiamo visto parlando delle misure di variabilita’, e’ la differenza tra il terzo quartile (\(Q_3\), il 75-esimo percentile) e il primo quartile (\(Q_1\), il 25-esimo percentile). Rappresenta la dispersione del 50% centrale dei dati — la parte “solida” della distribuzione, immune alle code.
La regola di Tukey e’ semplice: un valore e’ considerato anomalo se cade al di fuori dei cosiddetti cardini (in inglese fences):
\(\text{anomalia se } x < Q_1 – 1.5 \cdot IQR \quad \text{oppure} \quad x > Q_3 + 1.5 \cdot IQR \\
\)
Perche’ 1.5? Tukey non scelse questo valore a caso. Per una distribuzione normale, i cardini a 1.5 IQR corrispondono approssimativamente a 2.7 deviazioni standard dalla media — una soglia ragionevolmente conservativa che cattura circa lo 0.7% delle osservazioni nelle code. Abbastanza severa da non segnalare troppi falsi positivi, abbastanza sensibile da non lasciarsi sfuggire le anomalie importanti.
Applichiamo il metodo al nostro dataset:
Q1 <- quantile(sessioni, 0.25)
Q3 <- quantile(sessioni, 0.75)
IQR_val <- Q3 - Q1
limite_inf <- Q1 - 1.5 * IQR_val
limite_sup <- Q3 + 1.5 * IQR_val
cat("Q1:", Q1, " Q3:", Q3, " IQR:", IQR_val, "\n")
cat("Limite inferiore:", limite_inf, "\n")
cat("Limite superiore:", limite_sup, "\n")
anomalie_iqr <- which(sessioni < limite_inf | sessioni > limite_sup)
cat("Giorni anomali:", anomalie_iqr, "\n")
cat("Sessioni:", sessioni[anomalie_iqr], "\n")
Il risultato:
Q1: 215 Q3: 282 IQR: 67
Limite inferiore: 114.5
Limite superiore: 382.5
Giorni anomali: 45 59 118 120 200 300 350
Sessioni: 38 100 385 580 22 510 15
Il metodo di Tukey trova 7 anomalie: le nostre 5 iniettate piu’ due valori al confine (il giorno 59 con 100 sessioni e il giorno 118 con 385). Sono davvero anomali? 100 sessioni e’ effettivamente un valore basso per un sito con media 250, e 385 e’ alto rispetto ai quartili. La decisione, ancora una volta, spetta all’analista.
R offre un modo elegante per visualizzare le anomalie con il metodo di Tukey — il boxplot:
boxplot(sessioni, main = "Sessioni giornaliere",
ylab = "Sessioni", col = "lightblue", outline = TRUE)
# I punti oltre i baffi sono le anomalie secondo Tukey
Il grande vantaggio di questo metodo rispetto allo z-score e’ la robustezza: mediana e quartili non vengono influenzati dagli outlier. Non abbiamo bisogno di assumere che i dati siano normali. Il metodo di Tukey funziona anche con distribuzioni asimmetriche — e per chi lavora con dati web, questa non e’ una caratteristica da poco.
Il limite: il metodo non distingue tra anomalie “grandi” e “enormi”. Un valore appena fuori dal cardine e uno completamente fuori scala ricevono lo stesso trattamento — sono entrambi “anomali”, punto.
Metodo 3: il test di Grubbs
I primi due metodi si basano su regole empiriche: soglie sullo z-score, soglie sull’IQR. Ma se vogliamo un approccio formale — con un test di ipotesi vero e proprio — possiamo ricorrere al test di Grubbs.
L’idea e’ questa: prendiamo il valore piu’ estremo del dataset (quello piu’ lontano dalla media) e ci chiediamo se e’ compatibile con il resto dei dati, oppure se e’ “troppo” estremo per essere frutto del caso.
Le ipotesi sono:
- \(H_0\): non ci sono outlier nel dataset
- \(H_1\): il valore piu’ estremo e’ un outlier
La statistica del test e’:
\(G = \frac{\max |x_i – \bar{x}|}{s} \\
\)
dove \(\bar{x}\) e’ la media e \(s\) la deviazione standard. In altri termini, \(G\) e’ il massimo z-score in valore assoluto. Il valore critico si ricava dalla distribuzione t di Student con \(n-2\) gradi di liberta’.
Applichiamo il test in R usando il pacchetto outliers:
library(outliers)
risultato <- grubbs.test(sessioni)
print(risultato)
Il risultato:
Grubbs test for one outlier
data: sessioni
G = 5.9228, U = 0.9037, p-value = 2.339e-07
alternative hypothesis: highest value 580 is an outlier
Il test identifica 580 (il picco del giorno 120, il nostro “articolo virale”) come outlier, con un p-value praticamente nullo. L’evidenza e’ schiacciante: quel valore non e’ compatibile con il resto della distribuzione.
Ma va tenuto bene a mente un limite fondamentale del test di Grubbs: testa un solo outlier alla volta — il piu’ estremo. Se sospettiamo la presenza di anomalie multiple (come nel nostro caso), dobbiamo applicare il test in modo iterativo: rimuovere l’outlier identificato, ricalcolare, testare di nuovo.
Facciamolo:
dati <- sessioni
outlier_trovati <- c()
for(i in 1:5) {
g <- grubbs.test(dati)
if(g$p.value < 0.05) {
# Estraggo il valore outlier dal risultato
outlier_val <- as.numeric(gsub("[^0-9.]", "",
regmatches(g$alternative,
regexpr("[0-9.]+", g$alternative))))
outlier_trovati <- c(outlier_trovati, outlier_val)
dati <- dati[dati != outlier_val]
cat("Iterazione", i, "- Outlier:", outlier_val,
"- p-value:", format(g$p.value, digits = 3), "\n")
} else {
cat("Iterazione", i, "- Nessun outlier (p =",
round(g$p.value, 3), ")\n")
break
}
}
Questo approccio iterativo e’ efficace, ma insidioso: ogni volta che rimuoviamo un valore, cambiamo la distribuzione. La media e la deviazione standard si spostano, e cio’ che prima non era anomalo potrebbe diventarlo. E’ un procedimento da usare con cautela e consapevolezza.
Confronto tra i tre metodi
Abbiamo applicato tre metodi allo stesso dataset. Vediamo cosa ha trovato ciascuno:
| Giorno | Sessioni | Evento simulato | Z-score (|z|>3) | IQR/Tukey | Grubbs |
|---|---|---|---|---|---|
| 45 | 38 | Problema tecnico | Si | Si | Si (iter.) |
| 120 | 580 | Articolo virale | Si | Si | Si (1a iter.) |
| 200 | 22 | Update Google | Si | Si | Si (iter.) |
| 300 | 510 | Menzione social | Si | Si | Si (iter.) |
| 350 | 15 | Server down | Si | Si | Si (iter.) |
| 59 | 100 | (nessuno) | No | Si | No |
| 118 | 385 | (nessuno) | No | Si | No |
Le cinque anomalie iniettate vengono trovate da tutti e tre i metodi. Il metodo di Tukey e’ il piu’ sensibile: segnala anche due valori al confine che gli altri metodi lasciano passare. Lo z-score con soglia 3 e’ preciso ma dipende dall’ipotesi di normalita’. Grubbs e’ il piu’ formale ma richiede l’approccio iterativo per anomalie multiple.
La lezione importante e’: non esiste il metodo giusto in assoluto. Esiste il metodo giusto per quei dati e per quella domanda. Nella pratica quotidiana, un approccio sensato e’ applicare piu’ di un metodo e concentrarsi sui valori che vengono segnalati in modo concorde.
Riassumiamo in R i tre metodi a confronto:
# Creo un riepilogo per ogni giorno
riepilogo <- data.frame(
giorno = 1:n,
sessioni = sessioni,
z_score = round(z, 2),
anomalia_z = abs(z) > 3,
anomalia_iqr = sessioni < limite_inf | sessioni > limite_sup
)
# Mostro solo le righe anomale per almeno un metodo
anomale <- riepilogo[riepilogo$anomalia_z | riepilogo$anomalia_iqr, ]
print(anomale)
Prova tu
Un e-commerce ha monitorato il CTR delle proprie pagine prodotto per 30 giorni. Ecco i dati:
ctr <- c(3.2, 2.8, 3.1, 2.9, 3.0, 3.3, 2.7, 3.1, 2.8, 3.0,
0.4, 3.2, 2.9, 3.1, 2.8, 3.0, 2.9, 7.8, 3.1, 2.7,
3.0, 3.2, 2.8, 3.1, 2.9, 3.0, 2.8, 3.1, 3.0, 2.9)
Il giorno 11 e il giorno 18 sembrano sospetti. Applica i tre metodi: lo z-score con soglia |z| > 3, il metodo di Tukey e il test di Grubbs. Tutti e tre concordano? Quale dei due valori e’ piu’ chiaramente anomalo, e perche’?
Fin qui abbiamo trattato ogni osservazione come indipendente dalle altre. Abbiamo chiesto: “questo valore e’ compatibile con la distribuzione complessiva?” Ma i dati di traffico web hanno una struttura temporale: trend, stagionalita’, cicli settimanali. Un calo del 30% a dicembre potrebbe essere perfettamente normale per un sito B2B, mentre lo stesso calo a settembre sarebbe allarmante.
Per distinguere un’anomalia reale dalla semplice stagionalita’ servono strumenti diversi — la decomposizione delle serie storiche in trend, componente stagionale e residuo. Sara’ l’argomento di un prossimo articolo.
Per approfondire
Per chi volesse approfondire il tema dei valori anomali e del ragionamento statistico sui dati inattesi, L’arte della statistica di David Spiegelhalter e’ una lettura che affronta il problema con chiarezza e numerosi esempi dal mondo reale. Si trova qui.
Per una trattazione piu’ formale dei test per outlier (Grubbs, Rosner, Dixon), il manuale Statistica di Newbold, Carlson e Thorne offre la copertura completa con esercizi. Si trova qui.