Machine Learning: Addestramento e Test in Python
Dividi i dati in set di addestramento e test in Python con scikit-learn. Tratta test_size, random_state, stratify e metriche di valutazione.
La divisione train/test è il passaggio più fondamentale nella costruzione di un modello di machine learning. L'idea è semplice: tieni una parte dei tuoi dati nascosta al modello durante l'addestramento, poi misura quanto bene il modello si comporta su quella porzione nascosta. Senza questa separazione, non hai un modo onesto per sapere se il tuo modello ha davvero appreso uno schema o ha semplicemente memorizzato gli esempi di addestramento.
Questo capitolo spiega come funziona train_test_split di scikit-learn, cosa fa ogni parametro e come valutare il modello risultante — sia per problemi di regressione che di classificazione.
Perché Separare i Dati di Addestramento da Quelli di Test?
Un modello che si addestra e si valuta sugli stessi dati sembrerà molto più accurato di quanto non sia in realtà. Questo fenomeno si chiama data leakage o overfitting: il modello ha memorizzato gli esempi di addestramento anziché apprendere uno schema generalizzabile.
Immagina uno studente che studia 100 domande di pratica e poi sostiene un esame usando quelle stesse 100 domande. Potrebbe ottenere 100% — ma quel punteggio non dice nulla sulla sua effettiva comprensione della materia.
La divisione train/test previene questo problema:
- Addestrando il modello su una porzione dei dati in modo che possa apprendere gli schemi.
- Testando il modello su una porzione separata e non vista per misurare le prestazioni nel mondo reale.
Una divisione tipica è 80% addestramento / 20% test, anche se il rapporto giusto dipende dalla quantità di dati disponibili.
La Funzione train_test_split
scikit-learn fornisce train_test_split nel suo modulo model_selection. Mescola casualmente il dataset e lo divide in due parti:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)I quattro valori restituiti sono sempre in quest'ordine: feature di addestramento, feature di test, etichette di addestramento, etichette di test.
Parametri Principali
| Parametro | Tipo | Descrizione |
|---|---|---|
test_size | float o int | Frazione (0–1) o conteggio assoluto dei campioni di test. Il valore predefinito è 0.25. |
train_size | float o int | Complemento di test_size. Di solito si imposta uno o l'altro, non entrambi. |
random_state | int | Seme per il generatore di numeri casuali. Usa qualsiasi intero per rendere la divisione riproducibile. |
stratify | array-like | Passa y qui per preservare le proporzioni delle classi in entrambe le divisioni. Essenziale per dataset sbilanciati. |
shuffle | bool | Se mescolare prima di dividere. Il valore predefinito è True. Imposta False per dati di serie temporali. |
Effetto di test_size
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.datasets import load_iris
X, y = load_iris(return_X_y=True) # 150 samples
for ts in [0.1, 0.2, 0.3]:
X_tr, X_te, _, _ = train_test_split(X, y, test_size=ts, random_state=42)
print(f"test_size={ts}: train={len(X_tr)}, test={len(X_te)}")Output:
test_size=0.1: train=135, test=15
test_size=0.2: train=120, test=30
test_size=0.3: train=105, test=45random_state e Riproducibilità
Senza un random_state, la divisione è diversa ogni volta che si esegue lo script. Impostalo su qualsiasi intero per ottenere risultati riproducibili:
import numpy as np
from sklearn.model_selection import train_test_split
X = np.arange(10).reshape(-1, 1)
y = np.arange(10)
_, X_te1, _, _ = train_test_split(X, y, test_size=0.3, random_state=42)
_, X_te2, _, _ = train_test_split(X, y, test_size=0.3, random_state=42)
print("Same random_state → same split:", list(X_te1.ravel()) == list(X_te2.ravel()))
# TrueLa scelta dell'intero (42, 0, 1, ecc.) non conta — l'importante è usare sempre lo stesso valore.
Esempio di Regressione: Previsione dei Prezzi delle Case
L'esempio seguente genera un dataset sintetico di dimensioni e prezzi delle case, addestra un modello di regressione lineare e lo valuta sul set di test tenuto da parte.
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error, r2_score
# Generate synthetic data: house size (sq ft) → price
np.random.seed(42)
n = 200
sqft = np.random.randint(500, 3500, n).astype(float)
price = 150 * sqft + np.random.randn(n) * 20000
X = sqft.reshape(-1, 1)
y = price
# Split
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
print("Training samples:", len(X_train)) # 160
print("Testing samples:", len(X_test)) # 40
# Train
model = LinearRegression()
model.fit(X_train, y_train)
# Evaluate on the test set
y_pred = model.predict(X_test)
mse = mean_squared_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)
print(f"Mean Squared Error: {mse:,.0f}")
print(f"R² Score: {r2:.4f}")
print(f"Coefficient: {model.coef_[0]:.2f}")
print(f"Intercept: {model.intercept_:.2f}")Output:
Training samples: 160
Testing samples: 40
Mean Squared Error: 489,271,263
R² Score: 0.9651
Coefficient: 147.55
Intercept: 5237.02Interpretazione delle metriche:
- MSE (Mean Squared Error) è la differenza quadratica media tra le previsioni e i valori reali. Più basso è meglio, ma la scala dipende dalla variabile target (qui, in dollari).
- R² va da 0 a 1. Un valore di 0.965 significa che il modello spiega circa il 96,5% della varianza nei prezzi delle case — un ottimo adattamento su questo semplice dataset.
Per ulteriori informazioni sulla regressione lineare, consulta Regressione Lineare in Python.
Esempio di Classificazione: Specie di Fiori Iris
Per un'attività di classificazione, l'accuratezza e un report di classificazione sono più informativi dell'MSE.
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.datasets import load_iris
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, classification_report
X, y = load_iris(return_X_y=True)
# stratify=y ensures each class is proportionally represented in both splits
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
print("Train class distribution:", np.bincount(y_train)) # [40 40 40]
print("Test class distribution: ", np.bincount(y_test)) # [10 10 10]
model = LogisticRegression(max_iter=200)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
print("\nAccuracy:", round(accuracy_score(y_test, y_pred), 4))
print()
print(classification_report(y_test, y_pred,
target_names=["setosa", "versicolor", "virginica"]))Output:
Train class distribution: [40 40 40]
Test class distribution: [10 10 10]
Accuracy: 0.9667
precision recall f1-score support
setosa 1.00 1.00 1.00 10
versicolor 1.00 0.90 0.95 10
virginica 0.91 1.00 0.95 10
accuracy 0.97 30
macro avg 0.97 0.97 0.97 30
weighted avg 0.97 0.97 0.97 30Perché stratify=y è importante: Senza di esso, una divisione casuale di un dataset sbilanciato potrebbe inserire la maggior parte dei campioni di una classe rara nell'addestramento, non lasciandone nessuno nel set di test. stratify=y garantisce che ogni classe appaia nelle stesse proporzioni in entrambe le divisioni.
Per ulteriori informazioni sulla classificazione, consulta Regressione Logistica in Python e Matrice di Confusione in Python.
Errori Comuni
Pre-elaborazione Prima o Dopo la Divisione?
Dividi sempre prima di addestrare qualsiasi passaggio di pre-elaborazione (scaling, codifica, imputazione). Se scali tutti i tuoi dati e poi dividi, il set di test ha influenzato lo scaler — una forma di data leakage.
L'ordine corretto:
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train) # fit ONLY on training data
X_test_scaled = scaler.transform(X_test) # transform test with same parametersConsulta Feature Scaling in Python per una guida completa.
Mescolamento e Dati di Serie Temporali
train_test_split mescola i dati per impostazione predefinita. Per i dati di serie temporali questo è sbagliato — si addestrerebbe su dati futuri per prevedere il passato. Imposta shuffle=False e assicurati che i tuoi dati siano ordinati cronologicamente prima di dividere:
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, shuffle=False
)Quando una Singola Divisione Non Basta
Una singola divisione 80/20 fornisce una stima delle prestazioni del modello che dipende da quali campioni sono finiti nel set di test. La cross-validation ripete la divisione più volte e fa la media dei punteggi, ottenendo una stima molto più stabile — specialmente su dataset piccoli.
Scegliere una Metrica di Valutazione
La metrica giusta dipende dal tipo di problema:
| Problema | Metriche Comuni |
|---|---|
| Regressione | MSE, RMSE, MAE, R² |
| Classificazione binaria | Accuratezza, Precisione, Recall, F1, AUC-ROC |
| Classificazione multi-classe | Accuratezza, F1 macro/pesato |
Per la classificazione binaria, consulta Curva AUC-ROC in Python e Matrice di Confusione in Python. Per la regolazione degli iperparametri dopo aver ottenuto una divisione funzionante, consulta Grid Search in Python.
Riepilogo
- Dividi i tuoi dati prima di qualsiasi pre-elaborazione per evitare il data leakage.
- Usa
test_size=0.2come valore predefinito ragionevole; adatta in base alla dimensione del dataset. - Imposta
random_statesu qualsiasi intero per ottenere divisioni riproducibili. - Usa
stratify=yper attività di classificazione, specialmente su dati sbilanciati. - Imposta
shuffle=Falseper dati di serie temporali. - Una singola divisione train/test è una base di riferimento rapida; usa la cross-validation per stime delle prestazioni più affidabili.