📚 背景知識(読んでから問題へ)
🎯
Day 058 — Day 051 で K-Fold の基礎を学びました。今日は「場面ごとに CV の種類を使い分ける」上位技術を習得します。特に不均衡データ・小データ・時系列での適切な選択がKaggle上位への近道です。
なぜ CV の種類を使い分けるのか
| 問題 | 普通の KFold の欠点 | 解決策 |
|---|---|---|
| 不均衡データ(陽性 5%) | 一部の Fold に陽性が 0件〜2件しか入らない | StratifiedKFold で比率を維持 |
| 小データ(〜200件) | Fold ごとのスコアのばらつき(分散)が大きい | RepeatedKFold で 50回平均化 |
| 超小データ(〜100件) | 5-Fold でも学習データが少なすぎる | LOOCV で最大限のデータを学習に活用 |
| 時系列データ | 未来データが学習に混入(情報リーク) | TimeSeriesSplit で過去→未来の順序を保持 |
🗂️ CV 種類と使い分け
KFold
sklearn:
KFold用途: 回帰・均衡分類
データ量: 中〜大
特徴: シンプル・高速
StratifiedKFold ★
sklearn:
StratifiedKFold用途: 不均衡分類
データ量: 中〜大
特徴: クラス比率を各 Fold で維持
RepeatedStratifiedKFold ★
sklearn:
RepeatedStratifiedKFold用途: 小データ×不均衡
データ量: 小〜中
特徴: 分散を大幅に低減
LeaveOneOut
sklearn:
LeaveOneOut用途: 超小データ
データ量: ≤ 500件
特徴: 計算コスト O(N)
TimeSeriesSplit
sklearn:
TimeSeriesSplit用途: 時系列データ
データ量: 任意
特徴: 過去→未来の順序を維持
📊 Stratified の効果(陽性 10% の場合)
各 Fold の陽性割合(理想値: 10.0%)。KFold は偏り、StratifiedKFold は均一。
📉 繰り返し CV による分散安定化
スコア数が増えるほど std(ばらつき)が小さくなる(√n に反比例)
std の理論値: std ∝ 1/√n(n = スコア数)。5→50 で約 3.2倍安定化
🗂️ データスキーマ(シミュレーションデータ)
| パラメータ | 値 | 説明 |
|---|---|---|
n_samples | 500 | 総サンプル数(小データ想定で LOOCV 比較も可能) |
n_features | 15 | 特徴量数 |
n_informative | 8 | 実際に有用な特徴量数 |
weights | [0.9, 0.1] | 陰性90% / 陽性10%(不均衡) |
random_state | 42 | 再現性確保 |
クラス分布(陽性 10% / 陰性 90%)
📝 問題
import numpy as np
import pandas as pd
from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.model_selection import (
KFold, StratifiedKFold,
RepeatedStratifiedKFold, LeaveOneOut,
cross_val_score, cross_validate,
)
import warnings
warnings.filterwarnings('ignore')
# 不均衡データの生成(陽性 10% / 陰性 90%)
X, y = make_classification(
n_samples=500,
n_features=15,
n_informative=8,
weights=[0.9, 0.1],
random_state=42,
)
pipeline = Pipeline([
('scaler', StandardScaler()),
('model', LogisticRegression(max_iter=1000, random_state=42))
])
問1 — KFold vs StratifiedKFold の比較
KFold(n_splits=5, shuffle=True, random_state=42)で 5-fold CV を実行し、各フォールドの陽性割合を出力せよStratifiedKFold(n_splits=5, shuffle=True, random_state=42)でも同様に実行し、陽性割合を比較せよ- どちらのほうが陽性割合が安定しているか観察せよ
問2 — cross_validate の活用
cross_validate を使って 1回の呼び出しで以下を同時取得せよ:
StratifiedKFold(n_splits=5)を使用- 取得するスコア:
roc_aucとaverage_precision(AP) - 各スコアの平均・標準偏差を出力せよ
- 学習時間(
fit_time)の平均も合わせて出力せよ
問3 — RepeatedStratifiedKFold で分散を安定化
RepeatedStratifiedKFold(n_splits=5, n_repeats=10, random_state=42)で AP を評価せよ- 通常の
StratifiedKFold(n_splits=5)の結果と分散(std)を比較せよ - 繰り返し CV のほうが std が小さくなることを確認せよ
問4 — LeaveOneOut CV(LOOCV)
LeaveOneOut()を使って AP スコアを求めよ(※直接は計算不可のため代替手法を考えよ)- LOOCV で AP が計算できない理由を説明せよ
- 代替として
roc_aucスコアを LOOCV で計算し、StratifiedKFold の結果と比較せよ
問5 — 考察(CV 手法の選択)
以下のシナリオで最適な CV 手法を選び、理由を述べよ:
- 陽性 5%・サンプル数 10,000 件のKaggle不均衡分類コンペ
- 陽性 50%・サンプル数 80 件の医療研究データ
- 陽性 15%・サンプル数 300 件・評価指標が AP
💡 ヒント
ヒント1(方向性)
cross_val_scoreは単一スコアのみ。複数スコアが必要ならcross_validate(scoring=[...], ...)- LOOCV で AP が計算できない理由: テストデータが 1件なので Precision/Recall の曲線が描けない(2値しかない)
- 各フォールドの陽性割合は
for train_idx, test_idx in cv.split(X, y): y[test_idx].mean()で取得
ヒント2(アプローチ)
# 各フォールドの陽性割合を確認する方法
kf = KFold(n_splits=5, shuffle=True, random_state=42)
for fold, (train_idx, test_idx) in enumerate(kf.split(X, y)):
pos_ratio = y[test_idx].mean()
print(f"Fold {fold+1}: 陽性割合 = {pos_ratio:.3f}")
# cross_validate の使い方
results = cross_validate(
pipeline, X, y,
cv=StratifiedKFold(n_splits=5, shuffle=True, random_state=42),
scoring=['roc_auc', 'average_precision'],
return_train_score=False,
)
ヒント3(コード骨格)
# RepeatedStratifiedKFold
rskf = RepeatedStratifiedKFold(n_splits=5, n_repeats=10, random_state=42)
repeated_scores = cross_val_score(pipeline, X, y, cv=rskf, scoring='average_precision')
print(f"Repeated CV: {repeated_scores.mean():.4f} ± {repeated_scores.std():.4f} (n={len(repeated_scores)})")
# LOOCV では roc_auc を使う(AP の代替)
loo = LeaveOneOut()
loo_scores = cross_val_score(pipeline, X, y, cv=loo, scoring='roc_auc')
print(f"LOOCV ROC-AUC: {loo_scores.mean():.4f}")
✅ 模範解答
import numpy as np
import pandas as pd
from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.model_selection import (
KFold, StratifiedKFold,
RepeatedStratifiedKFold, LeaveOneOut,
cross_val_score, cross_validate,
)
import warnings
warnings.filterwarnings('ignore')
# データ生成
X, y = make_classification(
n_samples=500, n_features=15, n_informative=8,
weights=[0.9, 0.1], random_state=42,
)
pipeline = Pipeline([
('scaler', StandardScaler()),
('model', LogisticRegression(max_iter=1000, random_state=42))
])
# ── 問1: KFold vs StratifiedKFold の陽性割合比較 ──
print("=" * 55)
print("問1: KFold vs StratifiedKFold の陽性割合")
print("=" * 55)
for name, cv in [
("KFold", KFold(n_splits=5, shuffle=True, random_state=42)),
("StratifiedKFold", StratifiedKFold(n_splits=5, shuffle=True, random_state=42))
]:
ratios = []
for fold, (train_idx, test_idx) in enumerate(cv.split(X, y)):
r = y[test_idx].mean()
ratios.append(r)
print(f" [{name}] Fold {fold+1}: 陽性割合 = {r:.3f}")
print(f" >> 平均: {np.mean(ratios):.3f}, std: {np.std(ratios):.4f}\n")
# ── 問2: cross_validate で複数スコア取得 ──
print("=" * 55)
print("問2: cross_validate — ROC-AUC & AP の同時取得")
print("=" * 55)
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
results = cross_validate(
pipeline, X, y,
cv=skf,
scoring=['roc_auc', 'average_precision'],
return_train_score=False,
)
print(f"ROC-AUC: {results['test_roc_auc'].mean():.4f} ± {results['test_roc_auc'].std():.4f}")
print(f"AP: {results['test_average_precision'].mean():.4f} ± {results['test_average_precision'].std():.4f}")
print(f"学習時間: {results['fit_time'].mean()*1000:.2f} ms/fold")
# ── 問3: RepeatedStratifiedKFold で分散を安定化 ──
print("\n" + "=" * 55)
print("問3: RepeatedStratifiedKFold vs 通常 StratifiedKFold")
print("=" * 55)
normal_scores = cross_val_score(
pipeline, X, y,
cv=StratifiedKFold(n_splits=5, shuffle=True, random_state=42),
scoring='average_precision'
)
print(f"StratifiedKFold: {normal_scores.mean():.4f} ± {normal_scores.std():.4f} (n={len(normal_scores)})")
rskf = RepeatedStratifiedKFold(n_splits=5, n_repeats=10, random_state=42)
repeated_scores = cross_val_score(
pipeline, X, y,
cv=rskf,
scoring='average_precision'
)
print(f"RepeatedStratifiedKFold: {repeated_scores.mean():.4f} ± {repeated_scores.std():.4f} (n={len(repeated_scores)})")
print(f">> 分散改善率: std が {normal_scores.std()/repeated_scores.std():.1f}倍 安定化")
# ── 問4: LOOCV ──
print("\n" + "=" * 55)
print("問4: LeaveOneOut CV (LOOCV)")
print("=" * 55)
print("AP は LOOCV で計算不可: テストデータが1件のため Precision-Recall 曲線が定義できない")
print("→ 代替: ROC-AUC(predict_proba の結果が確率値のため1件でも計算可能)")
loo = LeaveOneOut()
loo_scores = cross_val_score(pipeline, X, y, cv=loo, scoring='roc_auc')
print(f"\nLOOCV ROC-AUC: {loo_scores.mean():.4f} ± {loo_scores.std():.4f} (n={len(loo_scores)})")
print(f"StratifiedKFold ROC-AUC: {results['test_roc_auc'].mean():.4f} ± {results['test_roc_auc'].std():.4f} (n=5)")
# ── 問5: 考察 ──
print("""
問5 考察:
1. 陽性5%・10,000件 → StratifiedKFold(n_splits=5)
理由: データ量が十分。Stratified で陽性割合を維持。繰り返し不要(計算コスト)
2. 陽性50%・80件 → RepeatedStratifiedKFold(n_splits=5, n_repeats=20)
理由: 小データでフォールド分散が大きい。繰り返しで安定化。均衡なので通常KFoldも可
3. 陽性15%・300件・AP評価 → RepeatedStratifiedKFold(n_splits=5, n_repeats=10)
理由: 小〜中規模かつ不均衡。Stratified で比率維持 + 繰り返しで AP の分散を抑制
""")
🪜 Step-by-Step 解説
1KFold と StratifiedKFold の内部動作
# KFold はランダムにシャッフルして N 等分するだけ
kf = KFold(n_splits=5, shuffle=True, random_state=42)
# StratifiedKFold は y のクラス比率を各フォールドで維持
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
# split(X, y) に y を渡すことでクラス情報を利用
# KFold は split(X) でも split(X, y) でも動作は同じ
for train_idx, test_idx in skf.split(X, y):
# test_idx の y の比率 ≈ 全体の y の比率
pass
2cross_validate の scoring パラメータ
# 文字列リストで複数スコアを同時計算
results = cross_validate(
estimator, X, y,
cv=5,
scoring=['roc_auc', 'average_precision', 'f1'],
return_train_score=True, # 学習データのスコアも取得(過学習チェック)
)
# results は辞書。キーは test_<score>, train_<score>, fit_time, score_time
print(results.keys())
# dict_keys(['fit_time', 'score_time', 'test_roc_auc', 'test_average_precision', 'test_f1'])
3繰り返し CV のスコア数と信頼区間
rskf = RepeatedStratifiedKFold(n_splits=5, n_repeats=10, random_state=42)
scores = cross_val_score(pipeline, X, y, cv=rskf, scoring='average_precision')
# スコアは n_splits * n_repeats = 50 個
print(f"スコア数: {len(scores)}") # → 50
# 95% 信頼区間(±1.96 * std / sqrt(n))
n = len(scores)
ci = 1.96 * scores.std() / np.sqrt(n)
print(f"AP = {scores.mean():.4f} ± {ci:.4f} (95% CI)")
4LOOCV が遅い理由と対策(ShuffleSplit)
# LOOCV の計算コスト = N 回の学習
# N=500 なら 500回学習 → 通常の 100倍
# 対策: ShuffleSplit で近似
from sklearn.model_selection import ShuffleSplit
ss = ShuffleSplit(n_splits=100, test_size=0.2, random_state=42)
ss_scores = cross_val_score(pipeline, X, y, cv=ss, scoring='roc_auc')
print(f"ShuffleSplit (100回): {ss_scores.mean():.4f} ± {ss_scores.std():.4f}")
# LOOCV の近似として使えることが多い
5OOF(Out-of-Fold)予測の取り出し
from sklearn.model_selection import cross_val_predict
from sklearn.metrics import average_precision_score
# OOF 予測確率を取得(Kaggle の典型パターン)
oof_proba = cross_val_predict(
pipeline, X, y,
cv=StratifiedKFold(5, shuffle=True, random_state=42),
method='predict_proba'
)[:, 1]
oof_ap = average_precision_score(y, oof_proba)
print(f"OOF AP: {oof_ap:.4f}")
# Public LB と最も相関しやすいスコア
📐 数学・統計の補足(文系向け)
「分散を減らす」とは何か(試験の例え)
試験を 1回だけ受けた点数と、5回受けた平均点数、どちらが「本当の実力」に近いでしょうか?もちろん後者です。
繰り返し CV は「試験を 50回受けた平均」を出す手法です。分散(スコアのばらつき)は 1/√n に比例して縮小します。n=5 → n=50 で分散は √10 ≈ 3.2 倍 安定します。
std ∝ 1/√n のイメージ(n = CV スコア数)
LOOCV が高バイアス・低分散の理由
N-1 件で学習するため、学習セットがほぼフルサイズ = 本番モデルに近い(低バイアス)。ただし N 個のテスト予測が強く相関するため、分散の推定精度は意外と低くなるというトレードオフがあります。
🏆 Kaggleでの実践的な使い方
| 場面 | 推奨 CV | 理由 |
|---|---|---|
| タブラー不均衡分類(10万件超) | StratifiedKFold(5) | 高速かつ陽性比率維持 |
| 小データ(〜1000件)の分類 | RepeatedStratifiedKFold(5, 10) | 分散を安定化 |
| 時系列データ | TimeSeriesSplit(5) | 未来情報リークを防ぐ |
| 100件以下の医療データ | LeaveOneOut() | 最大限のデータを学習に活用 |
| Optuna でハイパラ最適化 | StratifiedKFold(3) | 速度優先・繰り返し不要 |
# Kaggle OOF 予測の典型パターン
from sklearn.model_selection import cross_val_predict, StratifiedKFold
from sklearn.metrics import average_precision_score
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
# OOF(Out-of-Fold)予測確率
oof_proba = cross_val_predict(pipeline, X_train, y_train, cv=cv, method='predict_proba')[:, 1]
# OOF での AP
oof_ap = average_precision_score(y_train, oof_proba)
print(f"OOF AP: {oof_ap:.4f}")
# この OOF スコアが Public LB と最も相関しやすい
# テストデータ全体への予測
pipeline.fit(X_train, y_train)
test_proba = pipeline.predict_proba(X_test)[:, 1]
🚫 よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
不均衡分類に KFold を使う |
StratifiedKFold の存在を知らない |
陽性割合がフォールドごとに大きく変動しスコアが不安定になる |
cross_validate の scoring に文字列1つを渡す |
複数スコアを知らない | リストで渡せば複数スコアを1回で取得できる(効率的) |
| LOOCV で AP を計算しようとする | 「使えば正確」という思い込み | 1件テストではクラス分布が定義できないため AP は計算不可。ROC-AUC を使う |
| 繰り返し CV は「不正解」と思う | 「同じデータを使い回す」という誤解 | 分割方法を変えて繰り返すのは正当な統計的手法。平均は不偏推定量 |
| 大規模データに LOOCV を使う | 「精度が高いから良い」という思い込み | N 回の学習が必要。N=10,000 なら StratifiedKFold の 2000倍の計算コスト |
🚀 次のステップ
- 発展:
TimeSeriesSplitで時系列データの CV を実装(リーク防止) - 次回予告(Day 059): Phase 2 総復習 — 線形回帰から PR 曲線まで、Phase 2 全テーマの問題を一括演習
📋 自己評価(解いた後に記入)
✍️
理解度: [ ] 完全理解 [ ] おおむね理解 [ ] 要復習
自分の回答:
気づき・メモ: