Passa al contenuto principale

Pipeline sklearn & data leakage

1. Cos'Γ¨ il data leakage​

Il data leakage Γ¨ la situazione in cui informazioni del test set (o del futuro, in problemi temporali) influenzano il training. Causa metriche di validazione artificialmente buone che NON si confermano in produzione.

È il bug più sottile e dannoso del ML applicato. È anche il più frequente.

2. I tre tipi di leakage in regressione tabular​

2.1 Leakage da preprocessing globale​

Sintomo: imputazione, scaling, encoding fatti sull'INTERO dataset prima dello split.

# WRONG: leakage!
df['LotFrontage'] = df['LotFrontage'].fillna(df['LotFrontage'].median()) # mediana globale
df_scaled = StandardScaler().fit_transform(df.values) # mu/sigma globali
X_train, X_test = train_test_split(df_scaled, ...) # tardi

PerchΓ© Γ¨ leakage: la mediana e mu/sigma sono statistiche calcolate su training+test insieme. In produzione il test set non Γ¨ disponibile, quindi le statistiche sarebbero diverse β†’ predizioni diverse β†’ metriche di validazione non rappresentano la realtΓ .

Soluzione: usare sklearn.pipeline.Pipeline che esegue fit solo sul training e transform su test/inferenza usando le statistiche del training.

# RIGHT: niente leakage
pipeline = Pipeline([
('imputer', SimpleImputer(strategy='median')),
('scaler', StandardScaler()),
('model', Ridge()),
])
pipeline.fit(X_train, y_train) # statistiche da X_train
pipeline.predict(X_test) # riusa quelle statistiche

2.2 Leakage da feature derivate dal target​

Sintomo: una feature Γ¨ una funzione (anche indiretta) del target.

# WRONG: la feature usa SalePrice!
df['price_per_sqft'] = df['SalePrice'] / df['GrLivArea'] # leakage diretto
df['neighborhood_avg'] = df.groupby('Neighborhood')['SalePrice'].transform('mean') # target encoding senza CV

In Ames, il rischio principale Γ¨ il target encoding (sostituire una categoria con la media del target per quella categoria). Va sempre fatto dentro la CV, non sul dataset completo.

Regola: nessuna trasformazione che dipende da y deve essere applicata fuori dalla pipeline.

2.3 Leakage da split temporale ignorato​

Sintomo: in dati con dimensione temporale (eventi datati), uno split casuale mette nel training osservazioni successive a quelle del test set.

In Ames Housing, le case sono state vendute fra 2006 e 2010. Un modello realistico per predire prezzi futuri dovrebbe addestrarsi sul 2006-2008 e validarsi su 2009-2010, NON splitare casualmente. Per il nostro project work questo non Γ¨ richiesto (split casuale Γ¨ ammesso), ma Γ¨ un'ottima estensione.

3. La struttura della pipeline Ames​

Il design "no-leakage by construction" del nostro progetto:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
load_raw() β”‚ Trasformazioni "semantiche"β”‚
β”‚ β”‚ che NON usano statistiche β”‚
β”‚ β”‚ β†’ si possono fare prima β”‚
β”‚ fill_structural_missing β”‚ dello split. β”‚
│ (NaN→'None' / 0) │ - NaN → 'None' │
β”‚ β”‚ - rimozione outlier β”‚
β”‚ remove_grliv_area_outliers β”‚ (regola fissa di De Cock)β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β–Ό
train_test_split (stratify)
β”‚
β–Ό
╔═══════════════════════════════════════════╗
β•‘ Pipeline sklearn (fit_transform su train,β•‘
β•‘ transform su test): β•‘
β•‘ β•‘
β•‘ 1. AmesFeatureEngineer β•‘
β•‘ └─ deterministic, no statistiche β•‘
β•‘ β•‘
β•‘ 2. ColumnTransformer: β•‘
β•‘ β”œβ”€ SimpleImputer(median) sul train β•‘
β•‘ β”œβ”€ OrdinalEncoder fitted sul train β•‘
β•‘ └─ OneHotEncoder fitted sul train β•‘
β•‘ β•‘
β•‘ 3. StandardScaler (solo Ridge): β•‘
β•‘ └─ ΞΌ, Οƒ dal training β•‘
β•‘ β•‘
β•‘ 4. Modello (Ridge / RF / XGB) β•‘
β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
β”‚
β–Ό
TransformedTargetRegressor (log1p / expm1):
wrappa tutta la pipeline. Predict ritorna $.

Ogni statistica Γ¨ calcolata solo sul training. K-fold CV applica la pipeline ad ognuno dei K fold separatamente: se un fold ha statistiche diverse, va bene β€” Γ¨ proprio questa la varianza che vogliamo misurare.

4. Pre-split vs in-pipeline: quando Γ¨ OK fuori dalla pipeline​

Posso fare una trasformazione fuori dalla pipeline solo se Γ¨ deterministica e non dipende dal sample. Esempi:

OperazioneDove?PerchΓ©
Conversione tipo (str β†’ int)fuorideterministica
Rinominare colonnefuorideterministica
Mappare NaN strutturali a 'None'fuoriregola fissa, indipendente dal sample
Rimozione outlier con regola fissa (GrLivArea > 4000 e prezzo < 300k)fuoriregola fissa
Imputazione con medianaDENTROmediana dipende dal sample
ScalingDENTROmu/sigma dipendono dal sample
OneHotEncoderDENTROil vocabolario dipende dal sample

5. Cross-validation con la pipeline​

cross_val_score(pipeline, X, y, cv=5) esegue, per ognuno dei 5 fold:

  1. Split di X, y in (X_tr, y_tr) e (X_va, y_va).
  2. pipeline.fit(X_tr, y_tr) β†’ tutte le statistiche calcolate sul fold di training.
  3. pipeline.predict(X_va) β†’ riusa quelle statistiche sul fold di validation.
  4. Calcolo metrica su (y_va, y_pred).

Ogni fold Γ¨ una mini-simulazione del deployment. Le metriche CV stimano la performance attesa in produzione.

6. Errori che ho visto fare nei progetti​

  1. Standardize tutto il dataset prima del split β†’ leakage dello scaling.
  2. OneHotEncoder().fit_transform(df_completo) prima del split β†’ vocabolario contaminato dal test.
  3. Tuning iperparametri sul test set ("guardo il test, ottimizzo, riguardo, riottimizzo") β†’ test set non piΓΉ valido.
  4. Feature engineering basata su statistiche di gruppo (es. media prezzo per quartiere) calcolata sull'intero dataset.
  5. pd.cut(df['SalePrice'], bins=...) per stratificare β†’ in regressione lo stratify deve usare pd.qcut su SalePrice, ma il calcolo dei quintili deve essere esplicitamente sul y_train e poi applicato a y per il train_test_split. Nel nostro codice Γ¨ fatto correttamente.
  6. Salvare il dataframe pre-processato e poi splittarlo β†’ uno qualsiasi dei punti sopra puΓ² essere giΓ  successo.

7. Sanity check: il modello "shuffle test"​

Per assicurarsi che NON ci sia leakage, un check rapido:

y_shuffled = y.sample(frac=1, random_state=0).reset_index(drop=True)
score = cross_val_score(pipeline, X, y_shuffled, scoring='r2', cv=5).mean()
print(f"RΒ² con target casualizzato: {score:.4f}")

Atteso: R2β‰ˆ0R^2 \approx 0 (il modello non puΓ² imparare nulla da yy casuale). Se ottieni R2>0.1R^2 > 0.1, c'Γ¨ leakage.

8. Riferimenti​

  • Sklearn user guide: Pipelines and composite estimators.
  • Kapoor & Narayanan (2023), Leakage and the Reproducibility Crisis in ML-based Science, Patterns 4(9).
  • Kaufman et al. (2012), Leakage in Data Mining: Formulation, Detection, and Avoidance, ACM TKDD 6(4).