Train/test split, scaling e outlier: preparare i dati senza barare

Un modello che azzecca il 99% sul tuo notebook e crolla in produzione ha quasi sempre lo stesso problema: hai valutato sui dati con cui ha studiato, o hai fatto trapelare informazioni dal test set. Vediamo come dividere, scalare e ripulire i dati nel modo giusto, con scikit-learn.

Ti è mai capitato? Alleni un modello, l’accuratezza segna 99%, esulti — e poi sui dati nuovi il modello sbaglia una volta su tre. Non è sfortuna: quasi sempre hai valutato il modello sugli stessi dati con cui ha imparato. È come interrogare uno studente esattamente sulle domande che ha studiato a memoria la sera prima: il 10 in pagella non ti dice nulla su quanto ha capito davvero. In questo tutorial sistemiamo le tre operazioni che vengono prima di qualsiasi modello serio: dividere i dati, portarli sulla stessa scala, gestire i valori anomali. Sono dieci righe di codice, ma sbagliarle invalida tutto quello che viene dopo.

Overfitting e underfitting, il compromesso bias-varianza, train/validation/test e la cross-validation: nel libro, con i conti.

Leggi «Overfitting, bias-varianza e validazione» nel libro →

Perché dividere: il test set è l’esame vero

Il machine learning ha un solo obiettivo: funzionare su dati mai visti. Se non ricordi la differenza tra feature e label, ti basta sapere che le feature sono le colonne della tabella che descrivono ogni esempio, e la label è la risposta che vogliamo prevedere. Per sapere se il modello generalizza, dobbiamo nascondergli una parte dei dati e usarla solo alla fine, come esame.

Prepara l’ambiente (Python 3.12, un ambiente virtuale con uv o venv, e VS Code o PyCharm come editor):

# nel terminale
uv venv .venv && source .venv/bin/activate
uv pip install scikit-learn numpy
# in alternativa: python3.12 -m venv .venv + pip install

Usiamo il dataset dei vini incluso in scikit-learn: 178 vini descritti da 13 misure chimiche, da classificare in 3 cultivar. Nessun download, funziona offline.

from sklearn.datasets import load_wine
from sklearn.model_selection import train_test_split

X, y = load_wine(return_X_y=True)

# 80% per addestrare, 20% tenuto da parte per l'esame finale
X_train, X_test, y_train, y_test = train_test_split(
    X, y,
    test_size=0.2,      # quota riservata al test
    random_state=42,    # semilla fissa: risultati riproducibili
    stratify=y,         # stesse proporzioni di classi nei due insiemi
)

print(X_train.shape, X_test.shape)  # (142, 13) (36, 13)

Due parametri meritano una riga a testa. random_state fissa il mescolamento casuale: senza, ogni esecuzione produce una divisione diversa e i tuoi esperimenti non sono confrontabili. stratify=y garantisce che le tre classi compaiano nelle stesse proporzioni in train e test: è come dividere un mazzo di carte assicurandosi che ogni metà abbia la stessa quota di assi — senza, con dataset piccoli, rischi un test set dove una classe quasi non esiste.

Il test set è l’esame vero: se lo hai già visto durante lo studio, il voto non misura niente.

Scaling: mettere le feature sulla stessa unità di misura

Nel dataset dei vini, la prolina va da 278 a 1680, mentre l’acido malico sta tra 0.7 e 5.8. Per un algoritmo basato sulle distanze — come il k-nearest neighbors, che classifica un punto guardando i suoi vicini più prossimi — la prolina domina qualsiasi calcolo solo perché ha numeri più grandi. È come stimare quanto due città sono «vicine» sommando la distanza in millimetri sulla mappa e la differenza di altitudine in chilometri: le unità incoerenti rendono il confronto privo di senso.

Lo StandardScaler risolve: sottrae la media e divide per la deviazione standard, così ogni feature ha media 0 e deviazione 1. Ma attenzione al punto più importante di tutto il tutorial: media e deviazione si imparano solo dal train set.

from sklearn.preprocessing import StandardScaler
from sklearn.neighbors import KNeighborsClassifier

scaler = StandardScaler()

# fit: impara media e deviazione SOLO dai dati di train
X_train_s = scaler.fit_transform(X_train)
# transform: applica al test le statistiche imparate dal train
X_test_s = scaler.transform(X_test)   # MAI fit_transform qui!

# confronto: kNN con e senza scaling
knn_raw = KNeighborsClassifier().fit(X_train, y_train)
knn_std = KNeighborsClassifier().fit(X_train_s, y_train)

print(f"Accuratezza senza scaling: {knn_raw.score(X_test, y_test):.2f}")   # ~0.81
print(f"Accuratezza con scaling:   {knn_std.score(X_test_s, y_test):.2f}") # ~0.97

Da 81% a 97% cambiando solo la scala: nessun modello nuovo, nessun dato in più. E perché mai fit solo sul train? Se calcoli la media anche sui dati di test, stai facendo trapelare informazione dal futuro: si chiama data leakage. È come tarare la bilancia usando anche i pacchi che dovrai pesare all’esame di certificazione — la misura finale sembra migliore di quanto sarà nella realtà.

Dividere e scalare senza barare Dataset completo (100%) Train (80%) Test (20%) fit StandardScaler media, deviazione transform transform Train scalato (media 0, dev 1) Test scalato (stesse statistiche)

Il dataset viene diviso prima di tutto; lo scaler impara media e deviazione solo dal train (freccia piena) e le applica a entrambi gli insiemi (frecce transform). Il test non contribuisce mai alle statistiche.

Outlier: quando un valore rovina la media

Un outlier è un valore anomalo, lontanissimo dagli altri. Se non ricordi perché è un problema, pensa allo stipendio medio in un bar: entra un miliardario e la «media» dice che siete tutti ricchi, ma la vita di nessuno è cambiata. La media (e quindi lo StandardScaler, che la usa) è sensibile agli estremi; la mediana no.

Un criterio classico per individuarli è la regola dell’IQR: tutto ciò che cade oltre 1.5 volte lo scarto interquartile dai quartili è sospetto.

import numpy as np
from sklearn.preprocessing import RobustScaler

col = X_train[:, 4]  # feature "magnesio"
q1, q3 = np.percentile(col, [25, 75])
iqr = q3 - q1                      # scarto interquartile
basso, alto = q1 - 1.5 * iqr, q3 + 1.5 * iqr
outlier = (col < basso) | (col > alto)
print(f"Outlier trovati: {outlier.sum()} su {len(col)}")

# alternativa robusta: scala con mediana e IQR invece di media e deviazione
rob = RobustScaler()
X_train_r = rob.fit_transform(X_train)
X_test_r = rob.transform(X_test)

Che cosa farne? Dipende da perché esistono. Se sono errori di misura (un’età di 220 anni), correggili o eliminali — ma solo dal train, e documentando la scelta. Se sono valori reali ma estremi (il cliente che spende dieci volte gli altri), spesso sono proprio l’informazione interessante: in quel caso non cancellarli, usa strumenti robusti come RobustScaler o modelli meno sensibili. Cancellare gli outlier «perché rovinano l’accuratezza» è un modo elegante di mentire a te stesso.

Quando tutto questo NON serve

Onestà prima di tutto: lo scaling non è un rito obbligatorio. Gli alberi di decisione e le foreste casuali (Random Forest, gradient boosting) dividono i dati con soglie del tipo «prolina > 750?», e una soglia non cambia se moltiplichi la colonna per mille: per loro lo scaling è irrilevante. Serve invece — ed è quasi obbligatorio — per kNN, SVM, regressione logistica con regolarizzazione, reti neurali e qualsiasi metodo basato su distanze o gradienti. Il train/test split, invece, non è mai negoziabile: qualunque sia il modello, senza un test set pulito non hai una misura, hai una speranza.

Ultima avvertenza: quando inizierai a provare tanti modelli e iperparametri, il test set «si consuma» — ogni volta che lo guardi per decidere qualcosa, un po’ di informazione trapela. La soluzione si chiama validation set e cross-validation, e la vedremo nell’articolo sulle Pipeline: per ora ti basta la regola d’oro, il test set si apre una volta sola, alla fine.

Prova tu

Carica il dataset load_breast_cancer di scikit-learn e ripeti l’esperimento: dividi con stratify, confronta un KNeighborsClassifier su dati grezzi, standardizzati con StandardScaler e scalati con RobustScaler. Poi prova a fare apposta la cosa sbagliata — fit_transform su tutto il dataset prima dello split — e osserva di quanto la valutazione diventa ottimista. Vedere il leakage con i tuoi occhi vale più di dieci articoli.

Per approfondire: il capitolo 2 di Hands-On Machine Learning with Scikit-Learn, Keras & TensorFlow di Aurélien Géron (3ª edizione) e la pagina Common pitfalls and recommended practices della documentazione ufficiale di scikit-learn, che dedica al data leakage una sezione intera.

I commenti sono riservati agli iscritti.

Accedi per commentare