📚 背景知識(読んでから問題へ)
📊
クロスバリデーション(CV)は、モデルの汎化性能を測る最重要テクニックです。Kaggle では「CVスコアが上がったのに Public LB が下がった」という乖離を防ぐために、CV設計が肝になります。
🔀 CVの種類と使い分け
K-Fold
データをK分割して順番にバリデーション
使いどころ: 回帰問題のデフォルト
Stratified K-Fold
各Foldで目的変数の比率を揃えてから分割
使いどころ: 分類・クラス不均衡データ
Group K-Fold
同グループが同じFoldに入るよう分割
使いどころ: 患者ID・ユーザーID
Time Series Split
時系列の順序を守ってFold分割
使いどころ: 株価・センサー・需要予測
Repeated K-Fold
K-Foldを複数回繰り返す(乱数変更)
使いどころ: 小規模データで分散を下げる
⚖️ Stratified K-Fold が重要な理由
Titanic のような二値分類では、ランダムにFold分割すると各Foldの生存率にバラつきが生じます。Stratified K-Fold を使うと、各Foldで「生存率 ≈ 全体の生存率(38%)」が保証されます。
K-Fold(揃えない)vs Stratified K-Fold(揃える)— 各Foldの生存率
K-Fold(ランダム分割)— 生存率がバラつく
Fold 1
Train
Train
Train
Train
Val
生存率 0.28 ↓
Fold 2
Train
Train
Train
Val
Train
生存率 0.43 ↑
Fold 3
Train
Train
Val
Train
Train
生存率 0.31 ↓
Stratified K-Fold(層化分割)— 全Foldで生存率が揃う
Fold 1
Train
Train
Train
Train
Val
生存率 0.384
Fold 2
Train
Train
Train
Val
Train
生存率 0.384
Fold 3
Train
Train
Val
Train
Train
生存率 0.383
各Foldの生存率が揃う → CVスコアの標準偏差(std)が小さくなり、安定した評価が得られる
📌
Titanicの生存率参考値: 全体 38.4% / 女性 74.2% / 男性 18.9%
クラスによって生存率が大きく異なるため、Stratified 分割が特に有効です。
クラスによって生存率が大きく異なるため、Stratified 分割が特に有効です。
🎯 問題
📝
前提コード: 以下のセットアップが済んでいるとして問題を解いてください。
import pandas as np import numpy as np from sklearn.model_selection import KFold, StratifiedKFold, cross_val_score from sklearn.ensemble import RandomForestClassifier from sklearn.preprocessing import LabelEncoder train = pd.read_csv('train.csv') train['Sex_enc'] = LabelEncoder().fit_transform(train['Sex']) train['Age'] = train['Age'].fillna(train['Age'].median()) train['Embarked'] = train['Embarked'].fillna('S') train['Embarked_enc'] = LabelEncoder().fit_transform(train['Embarked']) features = ['Pclass', 'Sex_enc', 'Age', 'SibSp', 'Parch', 'Fare', 'Embarked_enc'] X = train[features] y = train['Survived'] model = RandomForestClassifier(n_estimators=100, random_state=42)
問題1: K-Fold vs Stratified K-Fold を比較する
上記コードに続けて、KFold(n_splits=5) と StratifiedKFold(n_splits=5) の両方で cross_val_score を実行し、各Foldのスコアと平均・標準偏差を出力するコードを書いてください。
問題2: データリークを修正する
以下のコードは間違った CV の使い方です。何が問題で、どう修正すべきか説明してください。
間違ったCVの例
データリーク あり
# 全データで特徴量を作成してからCVする(NG) train['Age_mean'] = train.groupby('Pclass')['Age'].transform('mean') train['Fare_mean'] = train.groupby('Pclass')['Fare'].transform('mean') X_leak = train[features + ['Age_mean', 'Fare_mean']] skf = StratifiedKFold(n_splits=5) scores = cross_val_score(model, X_leak, y, cv=skf, scoring='accuracy') print(scores.mean())
問題3: 状況別CV選択
以下の各状況で使うべきCVの種類と理由を答えてください。
| 状況 | 使うべきCV | 理由 |
|---|---|---|
| 患者データ(同一患者の複数測定値あり) | ? | ? |
| 株価の翌日予測 | ? | ? |
| 猫・犬・鳥の3クラス分類(各クラス均等) | ? | ? |
| Titanicの生存予測(クラス不均衡あり) | ? | ? |
💡 ヒント
ヒント1 — 方向性
cross_val_score の cv パラメータに KFold オブジェクトや StratifiedKFold オブジェクトを渡すと、自動的に各Foldでスコアを計算してくれます。
ヒント2 — アプローチ
- 問題2:
groupby().transform()を全データで計算してから CV に渡している。各Foldのバリデーションデータの情報がトレーニング計算に混入している(データリーク) - 問題3: 「同一IDが複数行に存在するか」→ Group K-Fold、「時系列か」→ Time Series Split、「クラス比率が偏っているか」→ Stratified K-Fold
ヒント3 — コード骨格
# 問題1の骨格 kf = KFold(n_splits=5, shuffle=True, random_state=42) skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) for name, cv in [('KFold', kf), ('StratifiedKFold', skf)]: scores = cross_val_score(model, X, y, cv=cv, scoring='accuracy') print(f"{name}: {scores} | mean={scores.mean():.4f} std={scores.std():.4f}")
✅ 解答1: K-Fold vs Stratified K-Fold の比較
比較コード(完全版)
実行可能
from sklearn.model_selection import KFold, StratifiedKFold, cross_val_score kf = KFold(n_splits=5, shuffle=True, random_state=42) skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) for name, cv in [('KFold ', kf), ('StratifiedKFold', skf)]: scores = cross_val_score(model, X, y, cv=cv, scoring='accuracy') print(f"{name}: {scores.round(4)} | mean={scores.mean():.4f} std={scores.std():.4f}") # 出力例: # KFold : [0.8156 0.7989 0.8156 0.8268 0.8034] | mean=0.8121 std=0.0099 # StratifiedKFold: [0.8156 0.8101 0.8101 0.8268 0.8034] | mean=0.8132 std=0.0080 # ─── 各Foldのバリデーション生存率を確認 ─── print("\n--- 各FoldのVal生存率(Stratified)---") for fold, (train_idx, val_idx) in enumerate(skf.split(X, y), 1): fold_rate = y.iloc[val_idx].mean() print(f"Fold {fold}: 生存率 = {fold_rate:.3f}") print(f"全体の生存率: {y.mean():.3f}")
CVスコアの標準偏差(std)比較 — 低いほど安定
std が小さい = 安定したモデル = 汎化性能の推定が信頼できる
💡
ポイント: Stratified K-Fold は mean が高いだけでなく、std が小さいのが重要です。std が小さいほどCVスコアを信頼してモデル改善の判断ができます。
✅ 解答2: データリークの問題点と修正
⚠️
問題点: データリーク(Information Leakage)
transform('mean') を全データで実行すると、バリデーション対象のデータが「自分自身の平均計算」に参加してしまいます。これは「テスト中に答えを見ている」状態です。
NG: 全データでtransform(リークあり)
データリーク
# バリデーションデータの情報が計算に混入している train['Age_mean'] = train.groupby('Pclass')['Age'].transform('mean') # ↑ valデータの年齢も使って平均を計算している → リーク!
OK: 手動CVループでFold内のみ計算(リークなし)
正しい実装
import numpy as np def add_group_features(X_tr, X_val, train_fold_df, val_fold_df): """trainFoldのみで集計を計算してvalFoldに適用""" for col in ['Pclass']: # trainFoldのみで平均を計算 means = train_fold_df.groupby(col)['Age'].mean() X_tr[f'Age_mean_by_{col}'] = train_fold_df[col].map(means) X_val[f'Age_mean_by_{col}'] = val_fold_df[col].map(means) # valにないグループはtrainの全体平均で補完 X_val[f'Age_mean_by_{col}'] = X_val[f'Age_mean_by_{col}'].fillna( train_fold_df['Age'].mean() ) return X_tr, X_val skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) scores = [] for train_idx, val_idx in skf.split(X, y): X_tr = X.iloc[train_idx].copy() X_val_fold = X.iloc[val_idx].copy() y_tr = y.iloc[train_idx] y_val_fold = y.iloc[val_idx] # trainFoldのみで集計特徴量を計算 tr_df = train.iloc[train_idx] val_df = train.iloc[val_idx] X_tr, X_val_fold = add_group_features(X_tr, X_val_fold, tr_df, val_df) model.fit(X_tr, y_tr) scores.append(model.score(X_val_fold, y_val_fold)) print(f"リーク修正後: {np.mean(scores):.4f} ± {np.std(scores):.4f}")
✅ 解答3: 状況別CV選択の解答
| 状況 | 使うべきCV | 理由 |
|---|---|---|
| 患者データ(同一患者の複数測定値あり) | Group K-Fold | 同一患者がtrainとvalに分かれると過学習を見逃す |
| 株価の翌日予測 | Time Series Split | 未来データを過去データの学習に使ってはいけない |
| 猫・犬・鳥の3クラス分類(各クラス均等) | K-Fold | クラス均等なので層化不要(通常K-Foldで十分) |
| Titanicの生存予測(クラス不均衡あり) | Stratified K-Fold | クラス比率を各Foldで揃えてスコアの分散を下げる |
🔢 Step-by-Step 解説
Step 1: CVの基本フロー(5-Fold の例)
全データ 891件 を5Fold分割するイメージ
Fold 1: [Val 178件] [Tr 713件] → score1
Fold 2: [Tr 178件] [Val 178件] [Tr 535件] → score2
Fold 3: [Tr 356件] [Val 178件] [Tr 357件] → score3
Fold 4: [Tr 534件] [Val 178件] [Tr 179件] → score4
Fold 5: [Tr 712件] [Val 179件] → score5
CVスコア = mean([score1, score2, score3, score4, score5])
Step 2: Stratified の「揃える」を実感する
from sklearn.model_selection import StratifiedKFold, KFold import numpy as np # 極端に不均衡なデータで差を確認 y_imbalanced = np.array([1]*10 + [0]*90) # 陽性10% / 陰性90% kf = KFold(n_splits=5, shuffle=True, random_state=42) skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) print("── KFold(揃えない)──") for i, (_, val_idx) in enumerate(kf.split(y_imbalanced), 1): print(f"Fold {i}: 陽性率 = {y_imbalanced[val_idx].mean():.2f}") print("\n── Stratified KFold(揃える)──") for i, (_, val_idx) in enumerate(skf.split(y_imbalanced, y_imbalanced), 1): print(f"Fold {i}: 陽性率 = {y_imbalanced[val_idx].mean():.2f}") # KFold の例: Fold 1: 0.05 Fold 2: 0.15 ... バラつく # Stratified の例: Fold 1: 0.10 Fold 2: 0.10 ... 全Foldで10%に揃う
Step 3: CVスコアをモデル改善に使う
# CVスコアの解釈 # mean が高い → 全体的に良いモデル # std が低い → 安定したモデル(過学習リスクが低い) # Kaggle での実用ルール: # mean - std をモデルの「保守的な実力値」として参考にする if improved_cv > baseline_cv + 0.001: print("→ 改善あり。本番提出へ") elif improved_cv < baseline_cv - 0.001: print("→ 悪化。変更を元に戻す") else: print("→ 誤差範囲内。std を確認して判断")
📉 CVとPublic LBの乖離を防ぐルール
CV設計のベストプラクティス(Kaggle実践版)
1
分類問題はデフォルトで Stratified K-Fold
sklearn の
sklearn の
cross_val_score は分類問題で自動的に Stratified を使う(n_splits=5指定時)
↓
2
n_splits は5 or 10(データ量に応じて)
小規模データ(〜1000件)は10 Fold、大規模データは5 Fold で計算コスト削減
小規模データ(〜1000件)は10 Fold、大規模データは5 Fold で計算コスト削減
↓
3
shuffle=True, random_state を固定
再現性の確保と、データ順序への依存を防ぐ。異なるseedで複数回試すとさらに安定
再現性の確保と、データ順序への依存を防ぐ。異なるseedで複数回試すとさらに安定
↓
4
CVが改善 → LBが悪化 → データリークを疑う
この乖離が起きたらCV設計を見直す。Private LB はCVに近い傾向があるため、CVを信頼する
この乖離が起きたらCV設計を見直す。Private LB はCVに近い傾向があるため、CVを信頼する
Titanic でよく使う評価指標の意味(accuracy vs AUC)
Titanic のコンペ評価指標はAccuracy。AUCはモデル開発中の参考指標として使うと不均衡データでより安定した比較ができます。
⚠️ よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
cross_val_score の cv=5 は常にK-Fold |
デフォルトを確認しない | 分類問題では自動的に Stratified K-Fold になる |
| CVスコアが高い = Public LBも高い | CVを過信する | CV-LB乖離時はデータリークや分布ズレが原因。Private LBはCVに近い |
| shuffle=False のまま使う | デフォルトがFalse | データが順番に並んでいると偏りが生じる。必ずshuffle=Trueにする |
| std が高くても mean が高ければOK | 安定性を無視する | std が高いモデルは本番で大きく外れるリスクがある。mean - stdで判断 |
🚀 次のステップ
- 発展: Group K-Fold と Group Stratified K-Fold の実装(患者データ・ユーザーデータへの応用)
- 次回予告: Day 081 — 特徴量重要度の再確認 — Permutation Importance vs SHAP の使い分け
📝 自己評価(解いた後に記入)
理解度
自分の回答・気づき・メモ