Ti è mai capitato di guardare una colonna di numeri e notare un valore che stona con tutto il resto? Un 100 in una serie di 10, 12, 15.
Quel “battitore libero” è un outlier. A prima vista, la tentazione è di cancellarlo e andare avanti. Ma in un’analisi dati seria, l’istinto non basta: serve un metodo.
Questi valori anomali non sono solo curiosità statistiche. Possono distorcere completamente una media, ingannare un modello di machine learning e portare a conclusioni disastrosamente sbagliate.
In questo articolo, andiamo oltre l’intuito. Vedremo come usare la statistica e la libreria Pandas per identificare, capire e gestire questi dati in modo sistematico. Impareremo la differenza chiave tra il robusto metodo IQR e il classico Z-score, e scopriremo perché, a volte, un metodo sembra “fallire” (e perché non è affatto un fallimento, ma un’informazione preziosa).
1. Cos’è un Outlier e Perché Dovrebbe Importarci?
Un outlier è un valore che si discosta significativamente dagli altri dati in un dataset. Ma il contesto è tutto. Un outlier non è sempre “sbagliato”, a volte è l’informazione più importante.
- E-commerce: Stai analizzando i tempi di acquisto. Un outlier (es. 20 giorni) è un cliente che ha dimenticato un carrello? O un errore nel logging?
- Finanza: Analizzi i rendimenti azionari. Un outlier con Z-score altissimo potrebbe essere un crollo di mercato (un “cigno nero”) o un’opportunità di trading.
- Medicina: In un set di dati sui battiti cardiaci, un outlier potrebbe essere un errore del sensore (da rimuovere) o un evento cardiaco (da investigare immediatamente).
Capire quale metodo usare (IQR o Z-score) e come reagire (rimuovere, correggere o investigare) è fondamentale.
Statisticamente, l’identificazione avviene principalmente con due metodi:
- Metodo IQR (Interquartile Range): Robusto e non assume una distribuzione normale.
- Metodo Z-score: Basato sulla deviazione standard, ideale per dati con distribuzione normale (gaussiana).
2. Metodo 1: Calcolo Outlier con IQR (Il Metodo Robusto)
Utilizziamo il metodo IQR per definire i limiti di tolleranza di un dataset. Questo metodo si basa sui quartili ed è “robusto” perché non è influenzato dai valori estremi stessi.
Python
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
# Creiamo un DataFrame di esempio
df = pd.DataFrame({
'valori': [10, 12, 13, 15, 18, 19, 20, 21, 22, 100]
})
print(df)
Calcoliamo IQR (Interquartile Range)
L’IQR è la “scatola” che contiene il 50% centrale dei dati. È la differenza tra il terzo quartile ([math]Q_3[/math], il 75° percentile) e il primo quartile ([math]Q_1[/math], il 25° percentile).
Python
Q1 = df['valori'].quantile(0.25)
Q3 = df['valori'].quantile(0.75)
IQR = Q3 - Q1
print("Q1 (25° percentile):", Q1)
print("Q3 (75° percentile):", Q3)
print("IQR (differenza):", IQR)
Output:
Q1 (25° percentile): 13.5
Q3 (75° percentile): 20.75
IQR (differenza): 7.25
Identifichiamo gli Outlier
Gli outlier sono i valori esterni ai “baffi” (whiskers) del nostro boxplot, definiti da:
Limite Inferiore = [math]\displaystyle \begin{aligned} Q_1 – 1.5 \times IQR \end{aligned}[/math]
Limite Superiore = [math]\displaystyle \begin{aligned} Q_3 + 1.5 \times IQR \end{aligned}[/math]
Python
# Definiamo le soglie
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
print("Limite Inferiore:", lower_bound)
print("Limite Superiore:", upper_bound)
# Filtriamo i valori fuori dai limiti
outliers_iqr = df[(df['valori'] < lower_bound) | (df['valori'] > upper_bound)]
print("\nOutlier trovati (Metodo IQR):")
print(outliers_iqr)
Output:
Limite Inferiore: 2.625
Limite Superiore: 31.625
Outlier trovati (Metodo IQR):
valori
9 100
Il valore 100 è correttamente identificato come outlier in quanto [math]100 > 31.625[/math].
3. Metodo 2: Calcolo Outlier con Z-score (Il Metodo Classico)
Il metodo Z-score identifica gli outlier come punti dati che si discostano dalla media ([math]\mu[/math]) per un numero prestabilito di deviazioni standard ([math]\sigma[/math]).
Generalmente, una soglia di [math]\pm 3[/math] viene usata per dati normalmente distribuiti.

La formula è:
[math]\displaystyle \begin{aligned}
Z &= \frac{x – \mu}{\sigma}
\end{aligned}[/math]
Python
mean = df['valori'].mean()
std = df['valori'].std()
# Calcoliamo lo Z-score per ogni valore
df['z_score'] = (df['valori'] - mean) / std
print("Media:", mean)
print("Dev. Standard:", std)
print(df)
# Definiamo la soglia: |Z| > 3
outliers_z = df[abs(df['z_score']) > 3]
print("\nOutlier trovati (Metodo Z-score):")
print(outliers_z)
Output:
Media: 25.0
Dev. Standard: 26.795522...
valori z_score
0 10 -0.560874
1 12 -0.485400
2 13 -0.447663
3 15 -0.372219
4 18 -0.261331
5 19 -0.223594
6 20 -0.185857
7 21 -0.148120
8 22 -0.110383
9 100 2.799368
Outlier trovati (Metodo Z-score):
Empty DataFrame
Columns: [valori, z_score]
Index: []
Perché non abbiamo ottenuto alcun risultato?
L’analisi IQR ha detto che 100 è un outlier, ma lo Z-score dice che è “normale” (il suo punteggio è 2.8, che è minore di 3).
Questo non è un errore. È la differenza cruciale tra robustezza e sensibilità.
4. Analisi: Perché lo Z-score ha “Fallito”?
Il problema è che la media e la deviazione standard sono misure estremamente sensibili agli outlier.
Il nostro valore 100 è così grande che ha fatto due cose:
- Ha trascinato la media verso l’alto (a 25, anche se la maggior parte dei dati è sotto 22).
- Ha gonfiato la deviazione standard (a [math]\sim 26.8[/math]), allargando enormemente la definizione di “normalità”.
In pratica, l’outlier 100 ha reso la deviazione standard così grande che sé stesso è rientrato nei limiti di [math]\pm 3[/math] deviazioni.
Differenza Chiave: Deviazione Standard Campionaria vs. Popolazionale
C’è un’ulteriore sfumatura: ddof (Degrees of Freedom).
std(ddof=1)(default Pandas): Deviazione Standard Campionaria. Si usa quando i dati sono un campione della popolazione. Divide per [math]N-1[/math].std(ddof=0): Deviazione Standard Popolazionale. Si usa quando i dati sono tutta la popolazione. Divide per [math]N[/math].
Vediamo se cambia qualcosa:
Python
mean = df['valori'].mean()
std_sample = df['valori'].std(ddof=1) # Campionaria (default)
std_population = df['valori'].std(ddof=0) # Popolazionale
df['z_sample'] = (df['valori'] - mean) / std_sample
df['z_pop'] = (df['valori'] - mean) / std_population
print(df[['valori', 'z_sample', 'z_pop']])
Output:
valori z_sample z_pop
0 10 -0.560874 -0.589607
1 12 -0.485400 -0.510384
2 13 -0.447663 -0.472647
3 15 -0.372219 -0.391277
4 18 -0.261331 -0.274718
5 19 -0.223594 -0.235002
6 20 -0.185857 -0.195285
7 21 -0.148120 -0.155569
8 22 -0.110383 -0.115852
9 100 2.799368 2.947330
Come vedi, anche usando la deviazione standard popolazionale, lo Z-score di 100 è [math]\sim 2.95[/math]. È molto vicino, ma ancora sotto la soglia 3.
5. Perché questo Esercizio è Interessante?
L’esempio [10, 12, …, 22, 100] è brillante per scopi didattici perché mette in crisi un metodo (Z-score) e fa brillare l’altro (IQR).
- Dimostra la Non-Robustezza della Media/Std: Lo Z-score si basa su media e deviazione standard. L’esercizio dimostra che un singolo outlier può gonfiare queste metriche a tal punto da “nascondere” sé stesso.
- Evidenzia la Robustezza dell’IQR: Al contrario, l’IQR si basa sui quartili. Il valore 100 non ha più influenza sul 75° percentile ([math]Q_3[/math]) di quanta ne avrebbe avuto il valore 23. [math]Q_1[/math] e [math]Q_3[/math] rimangono ancorati al “corpo” principale dei dati.
Lezione Pratica:
Non esiste un solo modo “giusto”.
Se i tuoi dati sono “rumorosi” o non distribuiti normalmente, l’IQR è la tua scelta più sicura.
Se lavori con dati che sai essere distribuiti normalmente, lo Z-score è ottimo per trovare deviazioni standardizzate.
6. Rimozione degli Outlier da un DataFrame (Data Cleaning)
Ora passiamo alla pulizia vera e propria.
1. Rimozione con Metodo IQR
Questa è l’azione più comune. Manteniamo solo la porzione di dati che cade all’interno dei limiti.
Python
# Ricalcoliamo i limiti (per un codice pulito)
Q1 = df['valori'].quantile(0.25)
Q3 = df['valori'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
# Rimozione outlier (mantenendo solo i valori 'normali')
df_filtrato_iqr = df[(df['valori'] >= lower_bound) & (df['valori'] <= upper_bound)]
print("DataFrame senza outlier (IQR):")
print(df_filtrato_iqr)
Output:
DataFrame senza outlier (IQR):
valori z_score z_sample z_pop
0 10 -0.560874 -0.560874 -0.589607
1 12 -0.485400 -0.485400 -0.510384
2 13 -0.447663 -0.447663 -0.472647
3 15 -0.372219 -0.372219 -0.391277
4 18 -0.261331 -0.261331 -0.274718
5 19 -0.223594 -0.223594 -0.235002
6 20 -0.185857 -0.185857 -0.195285
7 21 -0.148120 -0.148120 -0.155569
8 22 -0.110383 -0.110383 -0.115852
Il valore 100 è stato eliminato con successo.
2. Rimozione con Metodo Z-score
Proviamo a fare lo stesso con lo Z-score, usando la soglia 3.
Python
# (La colonna 'z_score' è già calcolata)
soglia_z = 3
# Filtriamo valori con |Z| <= 3
df_filtrato_z = df[abs(df['z_score']) <= soglia_z]
print("DataFrame senza outlier (Z-score):")
print(df_filtrato_z)
Output:
DataFrame senza outlier (Z-score):
valori z_score z_sample z_pop
0 10 -0.560874 -0.560874 -0.589607
1 12 -0.485400 -0.485400 -0.510384
...
8 22 -0.110383 -0.110383 -0.115852
9 100 2.799368 2.799368 2.947330
Risultato: Il DataFrame non cambia! Come abbiamo dimostrato, lo Z-score di 100 (circa 2.8) è inferiore alla soglia 3, quindi il filtro abs(df['z_score']) <= 3 lo mantiene.
3. Funzione Generica per Più Colonne (IQR)
Nel mondo reale, avrai molte colonne. Ecco una funzione pratica per pulire un intero DataFrame usando il metodo IQR.
Python
def remove_outliers_iqr(df, columns):
"""
Rimuove gli outlier da colonne specifiche di un DataFrame usando il metodo IQR.
"""
df_clean = df.copy()
for col in columns:
Q1 = df_clean[col].quantile(0.25)
Q3 = df_clean[col].quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
# Filtraggio
df_clean = df_clean[(df_clean[col] >= lower) & (df_clean[col] <= upper)]
return df_clean
# Esempio d’uso:
df_multi = pd.DataFrame({
'altezza': [160, 162, 165, 170, 175, 300], # 300 è outlier
'peso': [50, 55, 60, 65, 70, 500] # 500 è outlier
})
df_senza_outlier = remove_outliers_iqr(df_multi, ['altezza', 'peso'])
print("\nDataFrame multi-colonna senza outlier:")
print(df_senza_outlier)
Output:
DataFrame multi-colonna senza outlier:
altezza peso
0 160 50
1 162 55
2 165 60
3 170 65
4 175 70
La riga contenente 300 e 500 è stata eliminata.
7. Visualizzazione Grafica degli Outlier
La visualizzazione è il modo più rapido per capire la distribuzione.
1. Boxplot (Metodo più Rapido)
Il boxplot è lo strumento standard: è la visualizzazione diretta del metodo IQR. Mostra i quartili (la scatola), i baffi (i limiti) e gli outlier come punti separati.
Il grafico generato in precedenza mostra chiaramente il punto isolato a 100:
Python
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
# Usiamo il DataFrame originale
df_valori = pd.DataFrame({'valori': [10, 12, 13, 15, 18, 19, 20, 21, 22, 100]})
# Creazione del Boxplot
sns.boxplot(x=df_valori['valori'])
# Aggiunta del titolo e visualizzazione
plt.title("Visualizzazione Outlier con Boxplot")
plt.show()
Analisi della Distribuzione Centrale (La “Scatola”)
La scatola blu rappresenta il 50% centrale dei tuoi dati, delimitato dal Primo Quartile (Q1) e dal Terzo Quartile (Q3).
- Mediana (linea all’interno della scatola): Si trova al centro della scatola, intorno al valore 18.5. Questo è il 50° percentile.
- Q1 (bordo sinistro della scatola): Si trova intorno a 13.5. Il 25% dei dati è inferiore a questo valore.
- Q3 (bordo destro della scatola): Si trova intorno a 20.75. Il 75% dei dati è inferiore a questo valore.
La scatola è molto compatta e si estende solo per un piccolo intervallo sull’asse x (da 13.5 a 20.75), indicando che la maggior parte dei valori è concentrata in un intervallo ristretto.
Analisi della Dispersione (I “Baffi” o Whiskers)
I baffi verticali che si estendono dalla scatola indicano la variabilità standard all’interno della distribuzione (escludendo gli outlier).
- I baffi si estendono fino al valore più piccolo e più grande che non sono considerati outlier.
- Nel nostro caso (dove Q_1 = 13.5 e Q_3 = 20.75), il Limite Superiore IQR calcolato era 31.625.
- Il baffle destro si ferma in corrispondenza del valore massimo nel dataset che è inferiore a 31.625 (ovvero 22).
Identificazione dell’Outlier
Il cerchio vuoto isolato sulla destra è l’elemento più importante del grafico:
- Posizione: È posizionato esattamente a 100 sull’asse
valori. - Significato: Questo punto rappresenta l’Outlier del dataset. Il boxplot identifica tutti i valori che cadono al di fuori dei limiti dei baffi (il Limite Superiore di 31.625 e il Limite Inferiore di 2.625).
Conclusione Visiva:
Il grafico conferma perfettamente l’analisi statistica IQR: la maggior parte dei dati è saldamente raggruppata tra 10 e 22, mentre il valore 100 è una chiara anomalia statistica che necessita di gestione o di indagine (a seconda del contesto applicativo). La notevole distanza tra la scatola e l’outlier (quasi l’80% dell’asse x) evidenzia l’impatto potenziale di questo singolo dato sull’analisi complessiva.
2. Scatter Plot con Outlier Colorati
Questo metodo richiede il calcolo esplicito dei limiti (es. con IQR) ma offre maggiore controllo.
Lo scatter plot è un ottimo strumento per visualizzare i dati in funzione della loro posizione all’interno del dataset (l’indice) e per evidenziare le anomalie trovate.
Preparazione dei Dati e Colore (hue)
Il passaggio cruciale è la creazione della colonna booleana 'outlier':
df['outlier'] = (df['valori'] < lower_bound) | (df['valori'] > upper_bound)
Questa colonna assegna:
True(rosso) ai valori che cadono al di fuori dei limiti IQR calcolati (i.e., il valore 100).False(blu) ai valori “normali” che rientrano nei limiti (i.e., tutti i valori da 10 a 22).
Struttura del Grafico
- Asse X (
x=range(len(df))): Rappresenta l’indice di riga del DataFrame (da 0 a 9). Questo serve semplicemente per distribuire i punti orizzontalmente, permettendo di vederli separati. - Asse Y (
y='valori'): Rappresenta il valore effettivo del dato.
Risultato Visivo Atteso
Il grafico mostrerà 10 punti dati:
- Nove punti blu: I punti corrispondenti agli indici 0-8 (
valorida 10 a 22) saranno raggruppati nella parte inferiore del grafico. - Un punto rosso: Il punto corrispondente all’indice 9 (
valori100) sarà isolato molto in alto sul grafico.
Questo tipo di visualizzazione serve a:
- Identificazione immediata: Vedere quanto l’outlier si discosti dalla massa.
- Verifica: Confermare che la logica di filtro (la colonna
'outlier') ha funzionato correttamente, colorando in rosso solo i dati che superano la soglia del 1.5 x IQR.
Python
# Dati di esempio
df = pd.DataFrame({'valori': [10, 12, 13, 15, 18, 19, 20, 21, 22, 100]})
# Calcoliamo i limiti
Q1 = df['valori'].quantile(0.25)
Q3 = df['valori'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
# Creiamo la colonna booleana per la visualizzazione
df['outlier'] = (df['valori'] < lower_bound) | (df['valori'] > upper_bound)
# Visualizziamo i punti
plt.figure(figsize=(8, 4))
sns.scatterplot(x=range(len(df)), y='valori', data=df, hue='outlier',
palette={False: 'blue', True: 'red'}, s=100)
plt.title("Outlier evidenziati in rosso (Metodo IQR)")
plt.xlabel("Indice")
plt.ylabel("Valori")
plt.show()

Il punto 100 apparirà in rosso, distinto da tutti gli altri punti blu.
Riepilogo Metodologico
| Metodo | Formula base | Vantaggi | Quando usarlo |
|---|---|---|---|
| IQR | [math](Q_3 – Q_1) \times 1.5[/math] | Semplice, robusto (poco sensibile agli estremi) | Metodo di default, dati non gaussiani, dati “rumorosi”. |
| Z-score | [math](x-\mu)/\sigma[/math] | Basato su media/std, facile da interpretare | Dati con distribuzione approssimativamente normale (gaussiana). |
Articoli di approfondimento
- 🔴 Pulizia dati con Python: guida pratica — da un dataset sporco all’analisi affidabile con pandas
- 🔴 Cosa sono i percentili ed i quartili e come si calcolano
- 🔴 Esercizi svolti deviazione standard: guida pratica con 6 esempi facili e difficili
- 🔴 Episodio 5 – L’incubo delle medie ingannevoli
- 🔴 Dataset di qualità: la guida completa — dalla raccolta dati alla pulizia per la tua AI
- 🔴 Lavori in corso: Data Steward — il guardiano della qualità nel regno dei dati





