📚 背景知識(読んでから問題へ)
⚠️
データリークとは、訓練データに「本来知るはずのない未来の情報・テスト時に存在しない情報」が混入している状態のこと。Kaggleでスコアが異様に良いのに提出すると大幅ダウンする原因のほとんどがこれ。
身近な例えで理解する
| 例え | 何が問題? | 機械学習での対応 |
|---|---|---|
| 試験前に答案用紙を見てしまった | 本番では見られない情報を使っている | テスト時に得られない特徴量を学習に使う |
| 明日の株価を使って今日の取引予測 | 未来情報を使っている | 時系列で未来データを特徴量に含める |
| 模擬試験で本試験の問題を使う | 評価が実力を反映しない | CV で test データの情報を使う |
🔍 リークの2大種類
1. ターゲットリーク (Target Leakage)
目的変数と強く相関するが、実際の予測時点では取得できない特徴量を使う場合。
| 予測タスク | NG特徴量 | なぜNG? | OK特徴量 |
|---|---|---|---|
| ローン延滞予測 | total_overdue_payments |
申込時点では不明 | 年収・勤続年数 |
| 保険申請却下予測 | insurance_paid |
却下されたなら支払いなし(答え) | 申込時の健康診断結果 |
| 患者の病気予測 | prescribed_medicine |
診断後に処方される | 血液検査値・年齢 |
2. 訓練-テストリーク (Train-Test Contamination)
前処理の際にテストデータの情報が訓練データに漏れる場合。
NG例: StandardScaler
リークあり
# NG: testも含めてfit scaler = StandardScaler() X_all = pd.concat([X_train, X_test]) X_all_s = scaler.fit_transform(X_all) # ← testの統計量が混入
📌
fit は「平均と分散を記憶」する操作。test データの平均・分散が train の正規化に影響する。
OK例: 正しいScaler使用
リークなし
# OK: trainだけでfit、testはtransformのみ scaler = StandardScaler() X_train_s = scaler.fit_transform(X_train) # trainのみでfit X_test_s = scaler.transform(X_test) # transformのみ(fitしない)
⏰ タイムライン思考でリークを発見する
リーク検出の鉄則は 「この特徴量、本番環境の予測時点で持てるか?」 と問い続けること。
ローン審査の例
T=0: 申込日
✅ 年収・勤続年数・借入額 → 特徴量に使える
T+3日: 審査プロセス
✅ 信用スコア(外部から取得可能)→ 使える
T+7日: 承認/却下 ← 予測したい時点
❌ 「過去の延滞履歴(審査後判明)」→ 使えない
T+30日: 返済開始
❌ 「返済実績」→ 完全に未来情報。使えない
時系列データでのリーク例
NG: 未来集計
リークあり
# NG: 全期間の平均を特徴量に使う(未来データが混入) df['store_avg_sales'] = df.groupby('store_id')['sales'].transform('mean')
OK: 過去集計のみ
リークなし
# OK: expandingでその時点までの平均のみ使用 df['store_avg_sales'] = ( df.sort_values('date') .groupby('store_id')['sales'] .expanding() .mean() .shift(1) # 現在時点を除外 .reset_index(level=0, drop=True) )
📊 リーク有無によるスコア乖離の可視化
リークがあると、CV スコアと LB(本番)スコアで大きな乖離が生じます。
リークありの場合: CV は低すぎ(良く見える)、LB は現実的な値に戻る = 大幅ダウン
特徴量重要度でリークを検出
1つの特徴量が突出して高い重要度を持つ場合は、ターゲットリークを強く疑ってください。
重要度が 0.9+ と突出 → ほぼ確実にターゲットリーク
🎯 問題
📝
以下の5つのコードスニペット(A〜E)を見て、それぞれ「リークあり / リークなし」を判定し、理由を説明してください。リークありの場合は修正方法も述べてください。
スニペット A
from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_all = pd.concat([X_train, X_test]) X_all_scaled = scaler.fit_transform(X_all) X_train_s = X_all_scaled[:len(X_train)] X_test_s = X_all_scaled[len(X_train):]
スニペット B
# 欠損値を列の中央値で補完 for col in num_cols: median_val = X_train[col].median() X_train[col].fillna(median_val, inplace=True) X_test[col].fillna(median_val, inplace=True)
スニペット C
# groupby集計で特徴量追加 X_train['mean_price_by_neighborhood'] = ( X_train.groupby('Neighborhood')['SalePrice'].transform('mean') )
スニペット D
# 時系列データでのlag特徴量 df = df.sort_values('date') df['lag1_sales'] = df['sales'].shift(1) # 1日前の売上 train = df[df['date'] < '2024-01-01'] test = df[df['date'] >= '2024-01-01']
スニペット E
from sklearn.model_selection import KFold kf = KFold(n_splits=5, shuffle=True, random_state=42) scores = [] for train_idx, val_idx in kf.split(X): X_tr, X_val = X[train_idx], X[val_idx] y_tr, y_val = y[train_idx], y[val_idx] scaler = StandardScaler() X_tr_s = scaler.fit_transform(X_tr) X_val_s = scaler.transform(X_val) # ← ここ重要 model = Ridge() model.fit(X_tr_s, y_tr) scores.append(mean_squared_error(y_val, model.predict(X_val_s)))
💡 ヒント
ヒント1 — 方向性
各スニペットで「どのタイミングでどのデータを使っているか」に注目してください。「テスト(または検証)データの情報が訓練データに混入しているか?」を確認してください。
ヒント2 — アプローチ
- A:
fitとtransformの対象データが問題 - B: 訓練データの統計量のみを使っているかを確認
- C: グループ集計の際に
SalePrice(目的変数)を使っている点に注目 - D: 時系列処理後に正しい順序で分割しているかを確認
- E: fold内でのscalerの
fit対象を確認
ヒント3 — コード骨格
# A の正解パターン scaler = StandardScaler() X_train_s = scaler.fit_transform(X_train) # trainだけでfit X_test_s = scaler.transform(X_test) # testはtransformのみ # C は目的変数 SalePrice を X_train の groupby で直接使っているのが問題 # Target Encoding を安全に行うには OOF 方式(Day 076)が必要 # E はすでに正しい実装(fold内で独立してscalerをfit)
✅ 模範解答
A
❌ NG
B
✅ OK
C
❌ NG
D
✅ OK
E
✅ OK
A の詳細解説と修正
修正後(A)
リークなし
scaler = StandardScaler() X_train_s = scaler.fit_transform(X_train) # X_trainのみでfit(μ,σを記憶) X_test_s = scaler.transform(X_test) # transformのみ(fitしない)
C の詳細解説と修正
SalePrice は目的変数。訓練データ内でも各サンプルの SalePrice が自分自身のグループ平均に含まれるため、ターゲットリークが発生します。
修正後(C)— OOF Target Encoding
リークなし
from sklearn.model_selection import KFold import numpy as np def oof_target_encode(X_tr, y_tr, X_te, col, n_splits=5, smoothing=10): global_mean = y_tr.mean() X_tr_enc = np.zeros(len(X_tr)) kf = KFold(n_splits=n_splits, shuffle=True, random_state=42) for tr_idx, val_idx in kf.split(X_tr): tr_fold = X_tr.iloc[tr_idx] y_fold = y_tr.iloc[tr_idx] means = y_fold.groupby(tr_fold[col]).agg(['mean', 'count']) # Smoothing: 少数カテゴリはglobal_meanに引き寄せる smooth = (1 / (1 + np.exp(-(means['count'] - 1) / smoothing))) means['enc'] = global_mean * (1 - smooth) + means['mean'] * smooth X_tr_enc[val_idx] = X_tr.iloc[val_idx][col].map(means['enc']).fillna(global_mean) # testは全trainデータで作成した統計量を使う all_means = y_tr.groupby(X_tr[col]).agg(['mean', 'count']) smooth = (1 / (1 + np.exp(-(all_means['count'] - 1) / smoothing))) all_means['enc'] = global_mean * (1 - smooth) + all_means['mean'] * smooth X_te_enc = X_te[col].map(all_means['enc']).fillna(global_mean) return X_tr_enc, X_te_enc.values
🪜 Step-by-Step 解説
1
「本番時に持てるか?」を問い続ける
特徴量を追加するたびに「この値、実際に予測するとき手元にある?」を確認。タイムラインを書いて整理するのが有効。
特徴量を追加するたびに「この値、実際に予測するとき手元にある?」を確認。タイムラインを書いて整理するのが有効。
↓
2
fit/transform を厳守する
すべての前処理器(Scaler, Encoder, Imputer)は
すべての前処理器(Scaler, Encoder, Imputer)は
fit を train のみで行い、test には transform のみ適用。
↓
3
CV 内でも前処理を独立させる
CV の各 fold で前処理を再実行。sklearn Pipeline を使うと自動化できる(次 Step)。
CV の各 fold で前処理を再実行。sklearn Pipeline を使うと自動化できる(次 Step)。
↓
4
特徴量重要度で異常値を監視
1特徴量が 0.7+ など突出して高い → ターゲットリーク疑いで即調査。
1特徴量が 0.7+ など突出して高い → ターゲットリーク疑いで即調査。
↓
5
CV スコアと LB スコアの乖離を確認
CV が良すぎて LB が低い → リーク。CV と LB が近い → 健全なモデル。
CV が良すぎて LB が低い → リーク。CV と LB が近い → 健全なモデル。
🔧 sklearn Pipeline でリークを構造的に防ぐ
Pipeline を使うと、CV の各 fold で前処理が自動的に train fold だけに対して実行されます。
from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.linear_model import Ridge from sklearn.model_selection import cross_val_score import numpy as np # Pipeline = 前処理 + モデルをひとまとめにする pipe = Pipeline([ ('scaler', StandardScaler()), ('model', Ridge()) ]) # cross_val_score が fold ごとに scaler.fit を train fold だけで実行してくれる scores = cross_val_score( pipe, X_train, y_train, cv=5, scoring='neg_mean_squared_error' ) rmse_scores = np.sqrt(-scores) print(f"CV RMSE: {rmse_scores.mean():.4f} ± {rmse_scores.std():.4f}")
✨
Pipeline のメリット: リーク防止が自動化される / コードが簡潔になる /
GridSearchCV ともシームレスに連携できる。Kaggle の標準的な実装パターン。✔️ 実践的リークチェックリスト
| チェック項目 | 確認方法 | 修正方法 |
|---|---|---|
| Scaler/Encoder は train のみで fit? | fit 対象データを確認 | fit を X_train のみに変更 |
| 欠損補完の統計量は train のみ? | 補完値の計算元を確認 | X_train から median/mean を計算 |
| Target Encoding は OOF? | 実装コードのフロー確認 | Day 076 の OOF 実装を使用 |
| 時系列は未来情報を含まない? | shift の向きと日付順を確認 | shift(1) + sort_values('date') |
| 集計特徴量に目的変数が含まれない? | groupby の対象カラムを確認 | 目的変数を特徴量から除外 |
| CV 内の前処理は fold 内で完結? | CV ループのコード確認 | sklearn Pipeline を使用 |
| CV スコアと LB スコアが近い? | 提出後にスコアを比較 | 大きな乖離があればリークを調査 |
⚡ よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
| 全データで Scaler を fit する | 「全体の統計量の方が正確」と思うから | test の情報を使うのはリーク。train のみで fit が鉄則 |
| CV 前に前処理を完了させる | コードを簡潔にしたいから | 前処理は CV 内の各 fold で独立して行う。Pipeline を使う |
| 高い特徴量重要度をそのまま信じる | スコアが良いから疑わない | 重要度が突出して高い特徴量はターゲットリークを疑う |
| 「Kaggle のデータはリークなし」と思う | 公式データを信頼しすぎる | 過去に意図的なリークがあったコンペは多数存在する |
| Target Encoding はリークしない | エンコーディングと理解している | ナイーブな実装は必ずリークする(Day 076 参照) |
🚀 次のステップ
- 発展: 時系列 CV でのリーク防止(
TimeSeriesSplit・GroupKFold) - 実践: 過去の自分のコードでリークが混入していないかをチェックリストで確認
- 次回予告 (Day 078): Kaggle のコンペ構造 — データ・評価指標・提出形式の読み方