Day 080 — CV設計戦略 — Stratified K-Fold で Titanic を正確に評価する

2026-06-30 水 / Phase 3 理論 CV / StratifiedKFold / データリーク / Titanic

📚 背景知識(読んでから問題へ)

📊
クロスバリデーション(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 分割が特に有効です。

🎯 問題

📝
前提コード: 以下のセットアップが済んでいるとして問題を解いてください。
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_scorecv パラメータに 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)比較 — 低いほど安定
K-Fold(揃えない)
std = 0.0099
0.0099 (大きい)
Stratified K-Fold(揃える)
std = 0.0080
0.0080 (小さい)
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}")
データリークの仕組み — なぜ「答えを見ている」のか NG: 全データで transform [Train 713件 + Val 178件] → mean計算 ↓ Valの情報が漏れている CVスコアが過度に楽観的になる OK: Fold内のみで計算 [Train 713件のみ] → mean計算 → Val に適用 ↓ Valは計算に参加しない 本番と同じ条件でスコアを計算できる

解答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 の cross_val_score は分類問題で自動的に Stratified を使う(n_splits=5指定時)
2
n_splits は5 or 10(データ量に応じて)
小規模データ(〜1000件)は10 Fold、大規模データは5 Fold で計算コスト削減
3
shuffle=True, random_state を固定
再現性の確保と、データ順序への依存を防ぐ。異なるseedで複数回試すとさらに安定
4
CVが改善 → LBが悪化 → データリークを疑う
この乖離が起きたらCV設計を見直す。Private LB はCVに近い傾向があるため、CVを信頼する
Titanic でよく使う評価指標の意味(accuracy vs AUC)
Accuracy(正答率)
0.82 = 100件中82件正解
使いやすい
AUC-ROC(順位の正確さ)
0.86 = 生存/死亡の順位付けの精度
クラス不均衡に強い
Titanic のコンペ評価指標はAccuracy。AUCはモデル開発中の参考指標として使うと不均衡データでより安定した比較ができます。

⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
cross_val_scorecv=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 の使い分け

📝 自己評価(解いた後に記入)

理解度
自分の回答・気づき・メモ