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à.
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