Passa al contenuto principale

Split temporali e prevenzione del data leakage

"In serie temporali, il futuro non Γ¨ un campione casuale del passato."

1. Il problema​

Il PW01 ha una dimensione esplicitamente temporale: gli eventi sono datati e l'obiettivo Γ¨ predire l'appartenenza futura ai cluster. Questo cambia tutto rispetto a un classificatore tradizionale:

  • Non possiamo usare train_test_split(events, ..., random_state=42) β€” mescolerebbe passato e futuro.
  • Non possiamo usare KFold(shuffle=True) β€” stessa ragione.
  • La label non Γ¨ una colonna data: la dobbiamo costruire noi a partire dagli eventi futuri rispetto allo snapshot.

2. Definizione formale di data leakage temporale​

Leakage temporale: l'utilizzo, in fase di training del modello, di informazioni che al tempo di osservazione tt NON sarebbero state disponibili.

In formula: una feature Ο•\phi usata per predire al tempo tt ha leakage se esistono eventi ee con timestamp(e)β‰₯t\text{timestamp}(e) \geq t tali che Ο•\phi dipende da ee.

Esempi nel PW:

CosaLeakage?PerchΓ©
recency_days(u, t) = t βˆ’ last_purchase(u, before t)NOusa solo eventi < t
recency_days(u, t) = t βˆ’ last_purchase(u, all events)SÌusa eventi futuri
monetary(u, t) = sum(price, purchases before t)NOcorretto
n_recent_interactions = events in [t-14, t)NOfinestra chiusa a destra esclusiva
future_cluster(u, t) = cluster(features(u, t+H))È la LABELnon è leakage: è il target

La regola operativa: filtrare gli eventi con timestamp < as_of PRIMA di calcolare qualsiasi feature. Nel PW questo Γ¨ imposto da filter_events_until in data.py:

def filter_events_until(events, as_of, inclusive=False):
op = events["timestamp"] <= as_of if inclusive else events["timestamp"] < as_of
return events.loc[op].copy()

3. La struttura "due snapshot" del PW​

Il PW richiede di valutare il classificatore su periodi realmente futuri. La soluzione adottata in pipeline.py::_pick_snapshots:

t_max - 2H t_max - H t_max
β”‚ β”‚ β”‚
─────────────━━━━━━●━━━━━━━━━━━━●─────────────● asse temporale
β”‚ β”‚ β”‚
β”‚ β”‚ β”‚
as_of_train as_of_test horizon end
(1) (2) (3)
β”‚ β”‚ β”‚
└─── H β”€β”€β”€β”€β”€β”€β”˜ β”‚
└─── H β”€β”€β”€β”€β”€β”€β”˜

Concretamente con H=30H = 30 giorni:

  1. as_of_train = t_max βˆ’ 2H: snapshot a cui osserviamo le feature di training del classificatore.
  2. as_of_test = t_max βˆ’ H: snapshot a cui osserviamo le feature di test.
  3. La label di ogni utente Γ¨ il cluster a cui apparterrΓ  dopo HH giorni:
    • Per il train: cluster a as_of_train + H = t_max βˆ’ H (entro il dataset).
    • Per il test: cluster a as_of_test + H = t_max (limite ultimo del dataset).

Vantaggi di questa struttura:

  • Train e test sono temporalmente disgiunti: nessuna sovrapposizione informativa.
  • Le feature sono point-in-time rispetto al rispettivo snapshot.
  • Le label sono calcolate applicando il modello di clustering (fittato sul training!) alle feature future.

Il flusso anti-leakage critico Γ¨ in labeling.py::make_future_user_clusters:

def make_future_user_clusters(ecom, as_of, horizon_days, cluster_model, scaler, ...):
future_t = as_of + pd.Timedelta(days=horizon_days)
events_future = filter_events_until(ecom.events, future_t)
feats_future = compute_user_features(events_future, future_t, ...)
X = scaler.transform(feats_future[cols].to_numpy()) # scaler dal TRAINING
labels = cluster_model.predict(X) # K-Means dal TRAINING
return pd.Series(labels, index=feats_future.index, ...)
Sottiglia: il cluster_model Γ¨ fittato sul training

Le label future del test set vengono ottenute applicando il K-Means fittato su as_of_train alle feature di as_of_test + H. Questo Γ¨ essenziale: se rifittassimo K-Means sui dati di test, l'identitΓ  dei cluster cambierebbe (label = 0 nel training potrebbe essere label = 2 nel test) e il classificatore non avrebbe nulla da imparare.

4. Strategie di split temporale (panoramica)​

4.1 Hold-out cronologico​

Il piΓΉ semplice: scegli una data tβˆ—t^*, tutto prima Γ¨ train, tutto dopo Γ¨ test.

events: ──────●───────●───●─────●──────●───●──●────●─────
↑
t*
[────── train ────][────── test ──────]

Pro: semplicissimo, riproducibile. Contro: una sola stima di performance β†’ varianza alta della metrica.

È quello che facciamo nel PW, con la variante "due snapshot" descritta sopra.

4.2 Walk-forward (rolling)​

Per stimare la performance media nel tempo:

fold 1: [── train ──][test]
fold 2: [─── train ────][test]
fold 3: [──── train ─────][test]
...

Si addestra il modello a KK tempi diversi, si misura la performance media. È quello che useremmo se il PW richiedesse una stima robusta del classificatore (es. monitoraggio in produzione).

4.3 Time-series K-fold (sklearn TimeSeriesSplit)​

Variante di walk-forward in cui i fold di train crescono progressivamente:

from sklearn.model_selection import TimeSeriesSplit

tscv = TimeSeriesSplit(n_splits=5)
for fold, (train_idx, test_idx) in enumerate(tscv.split(X_temporal)):
# train_idx βŠ‚ {0, 1, ..., t}
# test_idx βŠ‚ {t+1, ..., T}
...

Da NON usare con shuffle=True. Da usare quando il dataset Γ¨ ordinato per tempo e non c'Γ¨ una struttura "snapshot" come la nostra.

5. Errori comuni di leakage nel contesto del PW​

5.1 Calcolare RFM su eventi non filtrati​

# WRONG
rfm_train = compute_rfm(ecom.events, as_of=as_of_train) # usa anche eventi futuri!
rfm_test = compute_rfm(ecom.events, as_of=as_of_test)

Anche se compute_rfm ha il parametro as_of, non filtra automaticamente gli eventi: si fida che chi chiama abbia giΓ  filtrato. Il pattern corretto (usato nella pipeline) Γ¨:

# RIGHT
events_train_history = filter_events_until(ecom.events, as_of_train)
rfm_train = compute_rfm(events_train_history, as_of=as_of_train)

5.2 Standardizzare con statistiche globali​

# WRONG: scaler vede train+test
scaler = StandardScaler().fit(np.vstack([X_train, X_test]))

Il test set "dona" la sua media e std al training. Le metriche risultano gonfiate.

# RIGHT
scaler = StandardScaler().fit(X_train)
X_test_s = scaler.transform(X_test) # riusa mu/sigma del train

5.3 Generare label future con K-Means rifittato​

# WRONG: rifittare KMeans sui dati di test cambia l'identitΓ  dei cluster
kmeans_test = KMeans(n_clusters=K, random_state=42).fit(X_test)
y_test = kmeans_test.predict(X_test)

L'unico modo corretto: riusare il modello di clustering fittato sul training (vedi sezione 3).

5.4 Tunare iperparametri sul test set​

Anche con uno split temporale corretto, se modifichi il modello dopo aver visto le metriche di test, il test set diventa implicitamente parte del training. Usa una finestra di validation intermedia (es. as_of_val = t_max βˆ’ 1.5H) per il tuning, riserva il test per la valutazione finale.

6. Sanity check: lo "shuffle test"​

Per verificare empiricamente l'assenza di leakage:

# Mescola le label di training (rompe la relazione X β†’ y)
y_train_shuffled = pd.Series(y_train.values).sample(frac=1, random_state=0).values

clf = fit_random_forest(X_train_clf, y_train_shuffled)
acc_shuffled = (clf.predict(X_test_clf) == y_test_clf).mean()

print(f"Accuracy con label shuffled: {acc_shuffled:.3f}")
# Atteso: β‰ˆ 1/n_classes (random). Se > 1.5/n_classes β†’ c'Γ¨ leakage residuo.

Se ottieni accuracy molto sopra il random, una feature sta "trapelando" il target β€” tipicamente una feature derivata dalle label senza accorgersene.

7. Cosa NON Γ¨ leakage (non confondere)​

  • Avere le features di test in formato comparabile con quelle di train: ovvio, deve essere cosΓ¬, basta non usare statistiche del test.
  • Ordinare gli eventi per timestamp: ovvio, Γ¨ un pre-processing semantico.
  • Conoscere la struttura del problema (es. che esiste una colonna purchase): la conoscenza del dominio non Γ¨ leakage.

Il leakage Γ¨ specificamente l'uso di valori (statistiche aggregate, label, eventi futuri) che non sarebbero noti al tempo tt.

8. Riferimenti​

  • Kaufman, Rosset, Perlich, Stitelman (2012), Leakage in Data Mining: Formulation, Detection, and Avoidance, ACM TKDD 6(4) β€” paper di riferimento, taxonomy del leakage.
  • Kapoor, A. & Narayanan, A. (2023), Leakage and the Reproducibility Crisis in ML-based Science, Patterns 4(9).
  • Bergmeir, C. & BenΓ­tez, J. M. (2012), On the use of cross-validation for time series predictor evaluation, Information Sciences 191.
  • Sklearn user guide: TimeSeriesSplit.
  • Hyndman, R. J. & Athanasopoulos, G., Forecasting: Principles and Practice β€” capitolo 5 sul time series cross-validation.