Introduzione al Machine Learning con scikit-learn
Se lavori con i dati, prima o poi ti ritrovi a fare i conti col machine learning. È inevitabile. Dalla classificazione di email spam alla previsione dei prezzi immobiliari, gli algoritmi di apprendimento automatico sono ovunque — e francamente, sarebbe un peccato non sfruttarli.
In questo panorama, scikit-learn resta la libreria di riferimento per il machine learning classico in Python. Interfaccia pulita, documentazione eccellente, un ecosistema sterminato di strumenti. Difficile chiedere di più.
Con la versione 1.8 (dicembre 2025), le cose si sono fatte ancora più interessanti: supporto nativo per l'Array API che abilita l'accelerazione GPU tramite PyTorch e CuPy, miglioramenti al Metadata Routing, compatibilità col free-threaded CPython (finalmente senza GIL!) e ottimizzazioni per modelli lineari e operazioni sparse.
In questa guida ti accompagno attraverso l'intero flusso di lavoro: preparazione dei dati, pipeline, validazione incrociata, selezione e ottimizzazione degli iperparametri. Se hai già un po' di familiarità con pandas e la visualizzazione dei dati, sei nel posto giusto.
Preparazione dell'Ambiente e Installazione
Prima di tutto, assicuriamoci di avere tutto il necessario. Scikit-learn richiede Python 3.9 o superiore e si installa in un attimo:
pip install scikit-learn==1.8.0
pip install pandas numpy matplotlib seaborn
Verifichiamo che tutto funzioni:
import sklearn
print(sklearn.__version__) # 1.8.0
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris, fetch_california_housing
print("Tutte le librerie sono pronte!")
Vuoi provare il supporto GPU sperimentale della 1.8? Ti serve PyTorch o CuPy, più questa riga per abilitare l'Array API:
import sklearn
sklearn.set_config(array_api_dispatch=True)
Concetti Fondamentali: Supervisionato vs Non Supervisionato
Scikit-learn supporta due paradigmi principali:
- Apprendimento supervisionato: il modello impara da dati etichettati (feature + target). Qui rientrano la classificazione (target categorico) e la regressione (target numerico).
- Apprendimento non supervisionato: il modello scopre pattern nascosti senza etichette. Pensa al clustering, alla riduzione della dimensionalità e al rilevamento delle anomalie.
Una cosa che adoro di scikit-learn è la coerenza dell'interfaccia. Tutti gli estimator seguono lo stesso schema:
fit(X, y)— addestra il modellopredict(X)— genera previsioniscore(X, y)— valuta le prestazioni
Questo significa che cambiare algoritmo è spesso questione di modificare una sola riga. Sul serio.
Il Primo Modello: Classificazione con Random Forest
Ok, basta teoria. Partiamo con un esempio pratico usando il classicissimo dataset Iris e un Random Forest:
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report, accuracy_score
# Caricamento del dataset
iris = load_iris()
X, y = iris.data, iris.target
print(f"Dimensioni del dataset: {X.shape}")
print(f"Classi: {iris.target_names}")
# Suddivisione in training e test set
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
# Addestramento del modello
rf = RandomForestClassifier(n_estimators=100, random_state=42)
rf.fit(X_train, y_train)
# Valutazione
y_pred = rf.predict(X_test)
print(f"\nAccuratezza: {accuracy_score(y_test, y_pred):.4f}")
print("\nReport di classificazione:")
print(classification_report(y_test, y_pred, target_names=iris.target_names))
Nota quel stratify=y nella suddivisione: garantisce che ogni classe sia rappresentata proporzionalmente nel training e nel test set. È una di quelle piccole accortezze che fanno la differenza, soprattutto con dataset sbilanciati.
Regressione: Previsione dei Prezzi Immobiliari
Ora proviamo un problema di regressione col dataset California Housing. Qui usiamo un Gradient Boosting Regressor, che personalmente considero uno dei cavalli di battaglia più affidabili per dati tabulari:
from sklearn.datasets import fetch_california_housing
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.metrics import mean_squared_error, r2_score
import numpy as np
# Caricamento dei dati
housing = fetch_california_housing()
X, y = housing.data, housing.target
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# Addestramento di un Gradient Boosting Regressor
gbr = GradientBoostingRegressor(
n_estimators=200,
max_depth=4,
learning_rate=0.1,
random_state=42
)
gbr.fit(X_train, y_train)
# Previsioni e valutazione
y_pred = gbr.predict(X_test)
rmse = np.sqrt(mean_squared_error(y_test, y_pred))
r2 = r2_score(y_test, y_pred)
print(f"RMSE: {rmse:.4f}")
print(f"R² Score: {r2:.4f}")
Il Gradient Boosting costruisce alberi decisionali in sequenza, dove ogni nuovo albero corregge gli errori dei precedenti. Non a caso è spesso protagonista nelle competizioni Kaggle.
Pipeline di scikit-learn: Il Vero Game Changer
Ecco, se c'è una cosa che dovresti portarti a casa da questa guida, sono le Pipeline. Sono probabilmente lo strumento più importante di scikit-learn, e ti spiego perché.
Le pipeline concatenano trasformazioni e modello in un unico oggetto. Ma il vantaggio principale è che prevengono il data leakage — uno degli errori più subdoli e comuni nel machine learning.
Cos'è il Data Leakage?
Il data leakage si verifica quando informazioni dal test set "filtrano" nel training. Un esempio classico: normalizzi l'intero dataset prima di suddividerlo, e così la media e la deviazione standard includono anche dati di test. Il risultato? Una valutazione troppo ottimistica che poi crolla in produzione.
Costruire una Pipeline Corretta
from sklearn.pipeline import Pipeline, make_pipeline
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer
from sklearn.compose import ColumnTransformer
from sklearn.ensemble import RandomForestClassifier
import pandas as pd
import numpy as np
# Creiamo un dataset di esempio con feature miste
np.random.seed(42)
n = 500
df = pd.DataFrame({
"eta": np.random.randint(18, 70, n),
"reddito": np.random.normal(35000, 15000, n),
"citta": np.random.choice(["Milano", "Roma", "Napoli", "Torino"], n),
"istruzione": np.random.choice(["Liceo", "Laurea", "Master", "Dottorato"], n),
"acquisto": np.random.randint(0, 2, n)
})
# Separiamo feature e target
X = df.drop("acquisto", axis=1)
y = df["acquisto"]
# Definiamo le colonne numeriche e categoriche
col_numeriche = ["eta", "reddito"]
col_categoriche = ["citta", "istruzione"]
# Pipeline per feature numeriche
pipeline_numerica = Pipeline([
("imputer", SimpleImputer(strategy="median")),
("scaler", StandardScaler())
])
# Pipeline per feature categoriche
pipeline_categorica = Pipeline([
("imputer", SimpleImputer(strategy="most_frequent")),
("encoder", OneHotEncoder(handle_unknown="ignore", sparse_output=False))
])
# Combinazione con ColumnTransformer
preprocessore = ColumnTransformer([
("num", pipeline_numerica, col_numeriche),
("cat", pipeline_categorica, col_categoriche)
])
# Pipeline completa: preprocessing + modello
pipeline_completa = Pipeline([
("preprocessore", preprocessore),
("classificatore", RandomForestClassifier(n_estimators=100, random_state=42))
])
# Addestramento e valutazione
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
pipeline_completa.fit(X_train, y_train)
score = pipeline_completa.score(X_test, y_test)
print(f"Accuratezza della pipeline: {score:.4f}")
Questa pipeline gestisce tutto in automatico: imputazione dei valori mancanti, normalizzazione delle feature numeriche, codifica one-hot delle categoriche. E soprattutto, ogni trasformazione viene applicata separatamente su training e test set. Zero data leakage.
Validazione Incrociata: Valutare i Modelli Sul Serio
Una singola suddivisione training/test può dare risultati instabili, soprattutto con dataset piccoli. La validazione incrociata ti dà una stima molto più robusta.
K-Fold Cross-Validation
Il concetto è semplice: suddividi il dataset in K parti (fold), addestri K volte usando K-1 fold per il training e il rimanente per la validazione. In pratica:
from sklearn.model_selection import cross_val_score, cross_validate
# Cross-validation con la pipeline completa
scores = cross_val_score(
pipeline_completa, X, y, cv=5, scoring="accuracy"
)
print(f"Accuratezza per fold: {scores}")
print(f"Accuratezza media: {scores.mean():.4f} (+/- {scores.std():.4f})")
# Per ottenere metriche più dettagliate
risultati = cross_validate(
pipeline_completa, X, y, cv=5,
scoring=["accuracy", "precision_macro", "recall_macro", "f1_macro"],
return_train_score=True
)
print(f"\nAccuratezza test: {risultati['test_accuracy'].mean():.4f}")
print(f"Precisione test: {risultati['test_precision_macro'].mean():.4f}")
print(f"Recall test: {risultati['test_recall_macro'].mean():.4f}")
print(f"F1-Score test: {risultati['test_f1_macro'].mean():.4f}")
Stratified K-Fold per Classificazione
Per la classificazione, meglio usare StratifiedKFold. Garantisce che ogni fold mantenga la stessa proporzione di classi del dataset originale:
from sklearn.model_selection import StratifiedKFold
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(
pipeline_completa, X, y, cv=skf, scoring="f1_macro"
)
print(f"F1-Score medio (Stratified): {scores.mean():.4f}")
Selezione e Ottimizzazione degli Iperparametri
Gli iperparametri sono quei parametri che imposti prima dell'addestramento e che possono cambiare radicalmente le prestazioni del modello. La buona notizia? Scikit-learn ha strumenti potenti per trovare la combinazione giusta.
GridSearchCV: Ricerca Esaustiva
GridSearchCV testa ogni combinazione possibile. È l'approccio più semplice (e più lento), ma su spazi piccoli funziona benissimo:
from sklearn.model_selection import GridSearchCV
# Definiamo la griglia degli iperparametri
# I nomi seguono la convenzione: nome_step__nome_parametro
param_grid = {
"classificatore__n_estimators": [50, 100, 200],
"classificatore__max_depth": [3, 5, 10, None],
"classificatore__min_samples_split": [2, 5, 10]
}
# Configurazione della ricerca
grid_search = GridSearchCV(
pipeline_completa,
param_grid,
cv=5,
scoring="f1_macro",
n_jobs=-1, # Usa tutti i core disponibili
verbose=1,
return_train_score=True
)
grid_search.fit(X_train, y_train)
print(f"Migliori iperparametri: {grid_search.best_params_}")
print(f"Miglior F1-Score (CV): {grid_search.best_score_:.4f}")
# Valutazione sul test set
score_test = grid_search.score(X_test, y_test)
print(f"Accuratezza sul test set: {score_test:.4f}")
RandomizedSearchCV: Quando lo Spazio è Grande
Se lo spazio degli iperparametri è vasto, RandomizedSearchCV è la scelta furba: campiona combinazioni casuali da distribuzioni che specifichi tu, ed è decisamente più veloce:
from sklearn.model_selection import RandomizedSearchCV
from scipy.stats import randint, uniform
# Distribuzioni per il campionamento casuale
param_distributions = {
"classificatore__n_estimators": randint(50, 500),
"classificatore__max_depth": [3, 5, 7, 10, 15, None],
"classificatore__min_samples_split": randint(2, 20),
"classificatore__min_samples_leaf": randint(1, 10),
"classificatore__max_features": ["sqrt", "log2", None]
}
random_search = RandomizedSearchCV(
pipeline_completa,
param_distributions,
n_iter=50, # Numero di combinazioni da testare
cv=5,
scoring="f1_macro",
n_jobs=-1,
random_state=42,
verbose=1
)
random_search.fit(X_train, y_train)
print(f"Migliori iperparametri: {random_search.best_params_}")
print(f"Miglior F1-Score (CV): {random_search.best_score_:.4f}")
HalvingGridSearchCV: Velocità Estrema
Dalla versione 1.8, HalvingGridSearchCV è pienamente supportato. L'idea è geniale: valuta tutte le combinazioni su un sottoinsieme ridotto dei dati, poi elimina progressivamente le peggiori, allocando sempre più risorse alle migliori:
from sklearn.experimental import enable_halving_search_cv
from sklearn.model_selection import HalvingGridSearchCV
halving_search = HalvingGridSearchCV(
pipeline_completa,
param_grid,
cv=5,
scoring="f1_macro",
factor=3, # Fattore di riduzione ad ogni iterazione
random_state=42,
n_jobs=-1
)
halving_search.fit(X_train, y_train)
print(f"Migliori iperparametri: {halving_search.best_params_}")
print(f"Miglior F1-Score: {halving_search.best_score_:.4f}")
Confronto Sistematico degli Algoritmi
Ecco un passaggio che molti saltano (e non dovrebbero): confrontare diversi algoritmi in modo sistematico prima di scegliere. Ho preparato un framework che uso regolarmente nei miei progetti:
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC
from sklearn.ensemble import (
RandomForestClassifier,
GradientBoostingClassifier,
AdaBoostClassifier
)
from sklearn.neighbors import KNeighborsClassifier
from sklearn.tree import DecisionTreeClassifier
import pandas as pd
# Dizionario dei modelli da confrontare
modelli = {
"Logistic Regression": make_pipeline(
preprocessore, LogisticRegression(max_iter=1000, random_state=42)
),
"SVM (RBF)": make_pipeline(
preprocessore, SVC(kernel="rbf", random_state=42)
),
"Random Forest": make_pipeline(
preprocessore, RandomForestClassifier(n_estimators=100, random_state=42)
),
"Gradient Boosting": make_pipeline(
preprocessore, GradientBoostingClassifier(n_estimators=100, random_state=42)
),
"KNN": make_pipeline(
preprocessore, KNeighborsClassifier(n_neighbors=5)
),
"Decision Tree": make_pipeline(
preprocessore, DecisionTreeClassifier(random_state=42)
),
"AdaBoost": make_pipeline(
preprocessore, AdaBoostClassifier(n_estimators=100, random_state=42)
)
}
# Confronto con cross-validation
risultati_confronto = {}
for nome, modello in modelli.items():
scores = cross_val_score(modello, X, y, cv=5, scoring="f1_macro")
risultati_confronto[nome] = {
"F1 Medio": scores.mean(),
"Dev. Std.": scores.std()
}
print(f"{nome:25s} | F1: {scores.mean():.4f} (+/- {scores.std():.4f})")
# Creazione DataFrame dei risultati
df_risultati = pd.DataFrame(risultati_confronto).T.sort_values(
"F1 Medio", ascending=False
)
print("\nClassifica dei modelli:")
print(df_risultati)
Un consiglio: non guardare solo la media. La deviazione standard conta eccome. Un modello con F1 medio alto ma molto variabile potrebbe rivelarsi meno affidabile di uno leggermente peggiore ma più stabile.
Feature Importance e Interpretabilità dei Modelli
Capire perché un modello fa certe previsioni non è un optional. È fondamentale sia per la fiducia nel modello sia (sempre più spesso) per requisiti normativi.
Importanza delle Feature con Random Forest
import matplotlib.pyplot as plt
# Addestramento del modello
pipeline_completa.fit(X_train, y_train)
# Estrazione dell'importanza delle feature
rf_model = pipeline_completa.named_steps["classificatore"]
# Recupero dei nomi delle feature dopo il preprocessing
feature_names_num = col_numeriche
feature_names_cat = (pipeline_completa
.named_steps["preprocessore"]
.named_transformers_["cat"]
.named_steps["encoder"]
.get_feature_names_out(col_categoriche)
.tolist())
feature_names = feature_names_num + feature_names_cat
# Visualizzazione
importanze = rf_model.feature_importances_
idx_ordinati = np.argsort(importanze)
plt.figure(figsize=(10, 6))
plt.barh(range(len(importanze)), importanze[idx_ordinati])
plt.yticks(range(len(importanze)),
[feature_names[i] for i in idx_ordinati])
plt.xlabel("Importanza")
plt.title("Importanza delle Feature — Random Forest")
plt.tight_layout()
plt.savefig("feature_importance.png", dpi=150)
plt.show()
Permutation Importance: Un Approccio Più Robusto
A dire il vero, l'importanza basata sulle impurità ha i suoi limiti (tende a favorire feature con alta cardinalità). La Permutation Importance è un'alternativa più affidabile:
from sklearn.inspection import permutation_importance
# Calcolo della permutation importance sul test set
result = permutation_importance(
pipeline_completa, X_test, y_test,
n_repeats=10,
random_state=42,
scoring="accuracy"
)
# Visualizzazione ordinata
idx_ordinati = result.importances_mean.argsort()
plt.figure(figsize=(10, 6))
plt.boxplot(
result.importances[idx_ordinati].T,
vert=False,
labels=[X.columns[i] for i in idx_ordinati]
)
plt.xlabel("Diminuzione dell'accuratezza")
plt.title("Permutation Importance")
plt.tight_layout()
plt.savefig("permutation_importance.png", dpi=150)
plt.show()
La Permutation Importance misura l'impatto reale di ciascuna feature sulle prestazioni, rendendola particolarmente utile quando hai feature correlate tra loro.
Validazione Incrociata Annidata: La Best Practice per Eccellenza
Questo è un argomento che merita attenzione. Quando combini la selezione degli iperparametri con la valutazione del modello usando la stessa cross-validation, rischi di ottenere stime troppo ottimistiche.
La soluzione? La validazione incrociata annidata.
from sklearn.model_selection import cross_val_score, GridSearchCV, StratifiedKFold
# Loop esterno: valutazione delle prestazioni
cv_esterno = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
# Loop interno: selezione degli iperparametri
cv_interno = StratifiedKFold(n_splits=3, shuffle=True, random_state=42)
param_grid = {
"classificatore__n_estimators": [50, 100, 200],
"classificatore__max_depth": [3, 5, 10],
}
# GridSearch interno
grid_search_interno = GridSearchCV(
pipeline_completa,
param_grid,
cv=cv_interno,
scoring="f1_macro",
n_jobs=-1
)
# Cross-validation esterna
scores_annidati = cross_val_score(
grid_search_interno, X, y,
cv=cv_esterno,
scoring="f1_macro"
)
print(f"F1-Score (Nested CV): {scores_annidati.mean():.4f} (+/- {scores_annidati.std():.4f})")
print(f"Scores per fold: {scores_annidati}")
Sì, è più lento. Ma la stima che ottieni è imparziale, perché il tuning degli iperparametri è completamente isolato dalla valutazione finale. In progetti seri, è la strada da percorrere.
Matrici di Confusione e Curve ROC
Le metriche aggregate (accuratezza, F1) vanno bene per avere un'idea generale, ma per capire davvero come si comporta il tuo classificatore servono strumenti più dettagliati.
from sklearn.metrics import (
confusion_matrix, ConfusionMatrixDisplay,
roc_curve, auc, RocCurveDisplay
)
from sklearn.preprocessing import label_binarize
from sklearn.multiclass import OneVsRestClassifier
# Esempio con il dataset Iris (multiclasse)
iris = load_iris()
X_iris, y_iris = iris.data, iris.target
X_tr, X_te, y_tr, y_te = train_test_split(
X_iris, y_iris, test_size=0.3, random_state=42, stratify=y_iris
)
# Addestramento
clf = RandomForestClassifier(n_estimators=100, random_state=42)
clf.fit(X_tr, y_tr)
y_pred = clf.predict(X_te)
# Matrice di confusione
fig, ax = plt.subplots(figsize=(8, 6))
ConfusionMatrixDisplay.from_predictions(
y_te, y_pred,
display_labels=iris.target_names,
cmap="Blues",
ax=ax
)
ax.set_title("Matrice di Confusione — Random Forest su Iris")
plt.tight_layout()
plt.savefig("confusion_matrix.png", dpi=150)
plt.show()
# Curva ROC multiclasse (One-vs-Rest)
y_bin = label_binarize(y_te, classes=[0, 1, 2])
y_score = clf.predict_proba(X_te)
fig, ax = plt.subplots(figsize=(8, 6))
for i, nome_classe in enumerate(iris.target_names):
fpr, tpr, _ = roc_curve(y_bin[:, i], y_score[:, i])
roc_auc = auc(fpr, tpr)
ax.plot(fpr, tpr, label=f"{nome_classe} (AUC = {roc_auc:.3f})")
ax.plot([0, 1], [0, 1], "k--", label="Casuale")
ax.set_xlabel("Tasso Falsi Positivi")
ax.set_ylabel("Tasso Veri Positivi")
ax.set_title("Curva ROC — Classificazione Multiclasse")
ax.legend()
plt.tight_layout()
plt.savefig("roc_curve.png", dpi=150)
plt.show()
Novità di scikit-learn 1.8: GPU e Array API
Questa è la parte che mi entusiasma di più. Con la versione 1.8, scikit-learn fa un passo importante verso il calcolo su GPU grazie al supporto per l'Array API. In pratica, puoi passare tensor PyTorch o array CuPy direttamente agli estimator:
import sklearn
import torch
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import RidgeCV
# Abilitazione dell'Array API
sklearn.set_config(array_api_dispatch=True)
# Creazione dei dati su GPU (se disponibile)
device = "cuda" if torch.cuda.is_available() else "cpu"
X_torch = torch.randn(10000, 50, device=device)
y_torch = torch.randn(10000, device=device)
# StandardScaler e RidgeCV ora funzionano con tensor PyTorch
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X_torch)
ridge = RidgeCV(alphas=[0.1, 1.0, 10.0])
ridge.fit(X_scaled, y_torch)
print(f"Alpha ottimale: {ridge.alpha_}")
print(f"R² Score: {ridge.score(X_scaled, y_torch):.4f}")
print(f"Dispositivo: {device}")
Al momento, gli estimator compatibili includono StandardScaler, PolynomialFeatures, RidgeCV, RidgeClassifierCV, GaussianMixture e CalibratedClassifierCV. Anche diverse metriche come confusion_matrix e roc_curve supportano l'Array API. La lista crescerà nelle prossime versioni.
Serializzazione e Deploy dei Modelli
Ok, il modello funziona. E adesso? Bisogna salvarlo per poterlo usare in produzione.
import joblib
import pickle
# Metodo 1: joblib (raccomandato per scikit-learn)
joblib.dump(pipeline_completa, "modello_pipeline.joblib")
modello_caricato = joblib.load("modello_pipeline.joblib")
# Verifica che il modello caricato funzioni correttamente
score_originale = pipeline_completa.score(X_test, y_test)
score_caricato = modello_caricato.score(X_test, y_test)
print(f"Score originale: {score_originale:.4f}")
print(f"Score caricato: {score_caricato:.4f}")
assert score_originale == score_caricato, "I modelli non corrispondono!"
# Metodo 2: pickle (alternativa standard Python)
with open("modello_pipeline.pkl", "wb") as f:
pickle.dump(pipeline_completa, f)
with open("modello_pipeline.pkl", "rb") as f:
modello_pkl = pickle.load(f)
Attenzione alla sicurezza: non caricare mai modelli serializzati da fonti non fidate. I file pickle possono contenere codice arbitrario che viene eseguito al caricamento. Per ambienti di produzione, vale la pena considerare il formato ONNX come alternativa più sicura e portabile.
Workflow Completo: Dalla Preparazione al Deploy
Mettiamo tutto insieme in un esempio end-to-end. Questo è più o meno il flusso che seguo nella maggior parte dei progetti:
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split, cross_val_score, GridSearchCV
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.metrics import classification_report
import joblib
# === FASE 1: Caricamento e Analisi Esplorativa ===
# (In un progetto reale, usereste i vostri dati)
from sklearn.datasets import make_classification
X, y = make_classification(
n_samples=2000, n_features=10, n_informative=6,
n_redundant=2, n_classes=2, random_state=42
)
df = pd.DataFrame(X, columns=[f"feature_{i}" for i in range(10)])
df["target"] = y
print(f"Shape: {df.shape}")
print(f"Distribuzione target:\n{df['target'].value_counts(normalize=True)}")
# === FASE 2: Suddivisione dei Dati ===
X = df.drop("target", axis=1)
y = df["target"]
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
# === FASE 3: Costruzione della Pipeline ===
col_numeriche = X.columns.tolist()
pipeline = Pipeline([
("preprocessore", ColumnTransformer([
("num", Pipeline([
("imputer", SimpleImputer(strategy="median")),
("scaler", StandardScaler())
]), col_numeriche)
])),
("modello", GradientBoostingClassifier(random_state=42))
])
# === FASE 4: Validazione Incrociata ===
scores = cross_val_score(pipeline, X_train, y_train, cv=5, scoring="f1")
print(f"\nF1-Score (CV): {scores.mean():.4f} (+/- {scores.std():.4f})")
# === FASE 5: Ottimizzazione degli Iperparametri ===
param_grid = {
"modello__n_estimators": [100, 200],
"modello__max_depth": [3, 5],
"modello__learning_rate": [0.05, 0.1]
}
grid = GridSearchCV(pipeline, param_grid, cv=5, scoring="f1", n_jobs=-1)
grid.fit(X_train, y_train)
print(f"\nMigliori parametri: {grid.best_params_}")
print(f"Miglior F1 (CV): {grid.best_score_:.4f}")
# === FASE 6: Valutazione Finale ===
y_pred = grid.predict(X_test)
print(f"\nReport sul Test Set:")
print(classification_report(y_test, y_pred))
# === FASE 7: Salvataggio del Modello ===
joblib.dump(grid.best_estimator_, "modello_finale.joblib")
print("Modello salvato con successo!")
Tabella Riepilogativa degli Strumenti di Selezione
Per orientarti rapidamente nella scelta, ecco un riepilogo:
| Metodo | Strategia | Velocità | Ideale per |
|---|---|---|---|
GridSearchCV |
Ricerca esaustiva | Lenta | Spazi piccoli, pochi parametri |
RandomizedSearchCV |
Campionamento casuale | Rapida | Spazi ampi, esplorazione iniziale |
HalvingGridSearchCV |
Eliminazione progressiva | Molto rapida | Spazi medio-grandi |
HalvingRandomSearchCV |
Halving + casuale | La più rapida | Spazi molto grandi |
Conclusioni e Prossimi Passi
Abbiamo coperto parecchio terreno in questa guida: dal primo modello alla validazione incrociata annidata, dalla selezione degli iperparametri al deploy. Ecco i punti che mi preme sottolineare:
- Usate sempre le Pipeline — sul serio, prevengono il data leakage e rendono il codice molto più pulito
- La validazione incrociata è irrinunciabile per stime affidabili delle prestazioni
- Confrontate sempre più algoritmi: il primo che provate raramente è il migliore
- La validazione annidata è la strada giusta quando fate tuning e valutazione insieme
- L'interpretabilità conta, a volte più delle prestazioni pure
- scikit-learn 1.8 apre la porta al calcolo su GPU con l'Array API
Il prossimo passo naturale? Esplorare il deep learning con framework come TensorFlow e PyTorch. Le basi solide acquisite con scikit-learn vi torneranno utilissime — garantito.