📚 背景知識(読んでから問題へ)
🎯
Day 051 — 評価指標を習得した次のステップ。「どのデータで評価するか」という 評価設計 を学びます。Kaggle上位勢が全員使う必須スキルです。
モデルを評価するとき、データを1回だけ「訓練用・テスト用」に分けると 運に左右される評価 になります。たまたまテストデータが簡単なデータなら高スコアに、難しければ低スコアになるだけかもしれません。
交差検証(Cross-Validation)の核心
データを複数のブロック(fold)に分けて、何度も訓練・評価を繰り返す手法。全データが一度ずつテストに使われるため、より信頼性の高い評価が得られます。
| 手法 | いつ使う | 特徴 |
|---|---|---|
| KFold | 回帰 / クラス比率が均等な分類 | データをK等分してランダムに分割 |
| StratifiedKFold | 分類(特に不均衡データ) | 各foldのクラス比率を均等に保つ |
| TimeSeriesSplit | 時系列データ | 未来のデータが訓練に漏れないよう順序を保持 |
| GroupKFold | グループが存在するデータ | 同じグループが訓練とテストに分かれないよう制御 |
🗂️ K-Fold の仕組み(K=5)
1000件のデータを5等分(各200件)。1回ずつfoldを変えてテストに使う。
Fold 1
TEST
train
train
train
train
≈ 0.8540
Fold 2
train
TEST
train
train
train
≈ 0.8610
Fold 3
train
train
TEST
train
train
≈ 0.8500
Fold 4
train
train
train
TEST
train
≈ 0.8520
Fold 5
train
train
train
train
TEST
≈ 0.8580
平均
5回のスコアをすべて平均 → より信頼性の高いスコアを算出
0.8550
💡
全1000件のデータが 一度だけ テストに使われる。1回の train_test_split よりはるかに安定した評価が得られます。
🗂️ データスキーマ(合成分類データ 1000件)
| 項目 | 値 | 説明 |
|---|---|---|
n_samples | 1000 | サンプル数 |
n_features | 10 | 特徴量数(feat_0 〜 feat_9) |
n_informative | 5 | 実際に予測に役立つ特徴量の数 |
n_redundant | 2 | informative から線形結合で生成される冗長特徴量 |
weights | [0.8, 0.2] | 不均衡: クラス0が80%、クラス1が20% |
label | 0 / 1 | 目的変数(2クラス分類) |
⚠️
クラス比率が 8:2 と偏っています。これが Stratified K-Fold を使う理由です。通常のK-Foldでは特定のfoldにクラス1が偏る可能性があります。
📝 問題
import pandas as pd
import numpy as np
from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import KFold, StratifiedKFold, cross_val_score
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.metrics import accuracy_score
np.random.seed(42)
X, y = make_classification(
n_samples=1000,
n_features=10,
n_informative=5,
n_redundant=2,
weights=[0.8, 0.2], # クラス比率 8:2(不均衡)
random_state=42
)
X = pd.DataFrame(X, columns=[f"feat_{i}" for i in range(10)])
y = pd.Series(y, name="label")
問1 — K-Fold CVの手動実装
KFold(n_splits=5, shuffle=True, random_state=42)を使って手動でCVを実装する- 各foldで
LogisticRegressionを学習しaccuracy_scoreで評価する - 5つのスコアと、その 平均 ± 標準偏差 を表示する
問2 — Stratified K-Fold との比較
StratifiedKFold(n_splits=5, shuffle=True, random_state=42)で同じ評価を実施する- 各foldのクラス1の比率(
y[test_idx].mean())を表示する - K-Fold と Stratified K-Fold でスコアの 標準偏差 を比較し、どちらが安定しているか確認する
問3 — cross_val_score を使った簡潔な実装
cross_val_score関数を使って問1・問2と同じ評価を1行で書くscoring="accuracy"とscoring="roc_auc"の両方で評価するn_jobs=-1(並列処理)を使って実行する
問4 — データリーク防止: Pipeline との組み合わせ
以下の悪い例(データリーク)と良い例(Pipeline)を実装し、スコアを比較する:
# 悪い例(データリーク)
scaler = StandardScaler()
X_scaled_leak = scaler.fit_transform(X) # CV の外でスケーリング!
scores_leak = cross_val_score(LogisticRegression(), X_scaled_leak, y, cv=5)
# 良い例(Pipeline)
pipeline = Pipeline([
("scaler", StandardScaler()),
("model", LogisticRegression())
])
scores_correct = cross_val_score(pipeline, X, y, cv=StratifiedKFold(5, shuffle=True, random_state=42))
🔍 ヒント(段階的開示)
ヒント1 — 方向性
- 問1:
for train_idx, test_idx in kf.split(X):でfoldをループ - 問2:
y.iloc[test_idx].mean()がクラス1の比率 - 問3:
cross_val_score(model, X, y, cv=skf, scoring="roc_auc", n_jobs=-1) - 問4: Pipeline に前処理を入れると各fold内でfitされる
ヒント2 — アプローチ
kf = KFold(n_splits=5, shuffle=True, random_state=42)
scores = []
for fold, (train_idx, test_idx) in enumerate(kf.split(X), 1):
X_train, X_test = X.iloc[train_idx], X.iloc[test_idx]
y_train, y_test = y.iloc[train_idx], y.iloc[test_idx]
model = LogisticRegression(random_state=42, max_iter=500)
model.fit(X_train, y_train)
score = accuracy_score(y_test, model.predict(X_test))
scores.append(score)
print(f"K-Fold CV: {np.mean(scores):.4f} ± {np.std(scores):.4f}")
ヒント3 — コード骨格(ほぼ答え)
# Stratified K-Fold でクラス比率を確認
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
for fold, (train_idx, test_idx) in enumerate(skf.split(X, y), 1):
ratio = y.iloc[test_idx].mean()
print(f"Fold {fold}: クラス1比率 = {ratio:.3f}")
# cross_val_score
cv_strategy = StratifiedKFold(5, shuffle=True, random_state=42)
scores_acc = cross_val_score(
LogisticRegression(max_iter=500), X, y,
cv=cv_strategy, scoring="accuracy", n_jobs=-1
)
scores_auc = cross_val_score(
LogisticRegression(max_iter=500), X, y,
cv=cv_strategy, scoring="roc_auc", n_jobs=-1
)
print(f"Accuracy: {np.mean(scores_acc):.4f} ± {np.std(scores_acc):.4f}")
print(f"ROC-AUC : {np.mean(scores_auc):.4f} ± {np.std(scores_auc):.4f}")
✅ 模範解答
import pandas as pd
import numpy as np
from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import KFold, StratifiedKFold, cross_val_score
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.metrics import accuracy_score
np.random.seed(42)
X, y = make_classification(
n_samples=1000, n_features=10, n_informative=5,
n_redundant=2, weights=[0.8, 0.2], random_state=42
)
X = pd.DataFrame(X, columns=[f"feat_{i}" for i in range(10)])
y = pd.Series(y, name="label")
# ── 問1: K-Fold CV 手動実装 ──
kf = KFold(n_splits=5, shuffle=True, random_state=42)
scores_kf = []
for fold, (train_idx, test_idx) in enumerate(kf.split(X), 1):
X_train, X_test = X.iloc[train_idx], X.iloc[test_idx]
y_train, y_test = y.iloc[train_idx], y.iloc[test_idx]
model = LogisticRegression(random_state=42, max_iter=500)
model.fit(X_train, y_train)
score = accuracy_score(y_test, model.predict(X_test))
scores_kf.append(score)
print(f"Fold {fold}: {score:.4f}")
print(f"K-Fold 平均: {np.mean(scores_kf):.4f} ± {np.std(scores_kf):.4f}")
# ── 問2: Stratified K-Fold との比較 ──
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores_skf = []
for fold, (train_idx, test_idx) in enumerate(skf.split(X, y), 1):
X_train, X_test = X.iloc[train_idx], X.iloc[test_idx]
y_train, y_test = y.iloc[train_idx], y.iloc[test_idx]
ratio = y_test.mean()
model = LogisticRegression(random_state=42, max_iter=500)
model.fit(X_train, y_train)
score = accuracy_score(y_test, model.predict(X_test))
scores_skf.append(score)
print(f"Fold {fold}: クラス1比率={ratio:.3f}, accuracy={score:.4f}")
print(f"Stratified 平均: {np.mean(scores_skf):.4f} ± {np.std(scores_skf):.4f}")
print(f"標準偏差比較 → K-Fold: {np.std(scores_kf):.4f} / Stratified: {np.std(scores_skf):.4f}")
# ── 問3: cross_val_score ──
cv_strategy = StratifiedKFold(5, shuffle=True, random_state=42)
model = LogisticRegression(random_state=42, max_iter=500)
scores_acc = cross_val_score(model, X, y, cv=cv_strategy, scoring="accuracy", n_jobs=-1)
scores_auc = cross_val_score(model, X, y, cv=cv_strategy, scoring="roc_auc", n_jobs=-1)
print(f"Accuracy: {np.mean(scores_acc):.4f} ± {np.std(scores_acc):.4f}")
print(f"ROC-AUC : {np.mean(scores_auc):.4f} ± {np.std(scores_auc):.4f}")
# ── 問4: Pipeline と データリーク ──
cv5 = StratifiedKFold(5, shuffle=True, random_state=42)
scaler = StandardScaler()
X_scaled_leak = scaler.fit_transform(X)
scores_leak = cross_val_score(
LogisticRegression(max_iter=500), X_scaled_leak, y, cv=cv5, scoring="accuracy"
)
pipeline = Pipeline([
("scaler", StandardScaler()),
("model", LogisticRegression(max_iter=500))
])
scores_correct = cross_val_score(pipeline, X, y, cv=cv5, scoring="accuracy")
print(f"[リーク有] {np.mean(scores_leak):.4f} ± {np.std(scores_leak):.4f}")
print(f"[Pipeline] {np.mean(scores_correct):.4f} ± {np.std(scores_correct):.4f}")
📌
期待される結果の傾向:
K-Fold の std > Stratified K-Fold の std(Stratified の方がスコアが安定)
ROC-AUC は Accuracy より不均衡データの評価に適した指標なので注視する
Pipeline のスコアはリーク有とほぼ同じかわずかに低いことが多い(今回はデータ数が多いので差が出にくい)
K-Fold の std > Stratified K-Fold の std(Stratified の方がスコアが安定)
ROC-AUC は Accuracy より不均衡データの評価に適した指標なので注視する
Pipeline のスコアはリーク有とほぼ同じかわずかに低いことが多い(今回はデータ数が多いので差が出にくい)
📊 K-Fold vs Stratified K-Fold の比較
各手法のスコア安定性(概念図)
💡
Stratified K-Fold は各foldのクラス比率を均等に保つため、スコアのばらつき(std)が小さくなります。不均衡データでは特に差が顕著です。
K値(分割数)とトレードオフ
🔗 データリーク vs Pipeline(可視化)
❌ 悪い例(データリーク)
全データ (1000件) で StandardScaler.fit()
↓ スケーリング済みデータを渡す
K-Fold で split()
テストデータの統計が既に漏れている!
全データの平均・標準偏差(テスト含む)でスケーリング → 情報漏洩
✅ 良い例(Pipeline)
K-Fold で split()
↓ 各fold内で独立処理
訓練データだけで StandardScaler.fit()
テストデータには transform のみ適用
各foldで独立してスケーリング → 情報漏洩なし
⚠️
なぜ差が出にくいことがあるのか? 今回のデータ(1000件)では差が小さいですが、少量データ・複雑な前処理・時系列データでは差が顕著になります。「差が出なかったから OK」ではなく、正しい方法を必ず使う習慣を。
🪜 Step-by-Step 解説
1 KFold の基本的な使い方
from sklearn.model_selection import KFold
kf = KFold(
n_splits=5, # データを5等分
shuffle=True, # 分割前にシャッフル(必須)
random_state=42 # 再現性のためにシードを固定
)
# split(X) は (訓練インデックスの配列, テストインデックスの配列) を返す
for train_idx, test_idx in kf.split(X):
print(f"train: {len(train_idx)}, test: {len(test_idx)}")
# → train: 800, test: 200 (5等分なので)
X_train = X.iloc[train_idx] # pandas では .iloc でインデックスを使う
X_test = X.iloc[test_idx]
shuffle=False(デフォルト)のままだとデータの並び順のまま分割されます。時系列以外では必ず shuffle=True を使いましょう。
2 StratifiedKFold でクラス比率を確認する
from sklearn.model_selection import StratifiedKFold
# 分類では split(X, y) に y を渡す(これが Stratified の鍵)
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
for fold, (train_idx, test_idx) in enumerate(skf.split(X, y), 1):
# テストデータのクラス1の比率を確認
class1_ratio = y.iloc[test_idx].mean()
print(f"Fold {fold}: クラス1比率 = {class1_ratio:.3f}")
# → 全fold で ≈ 0.200 になる(元データの比率を維持)
3 cross_val_score の便利な使い方
from sklearn.model_selection import cross_val_score
import numpy as np
# scoring 文字列一覧(よく使うもの)
# "accuracy" → 正解率
# "roc_auc" → ROC-AUC(分類)
# "neg_mean_squared_error" → MSE(符号反転、回帰)
# "neg_root_mean_squared_error" → RMSE(符号反転、回帰)
# "r2" → R²(回帰)
scores = cross_val_score(
LogisticRegression(max_iter=500),
X, y,
cv=StratifiedKFold(5, shuffle=True, random_state=42),
scoring="roc_auc",
n_jobs=-1 # 全CPUコアで並列実行 → 大量データで高速化
)
print(f"mean: {scores.mean():.4f}")
print(f"std: {scores.std():.4f}")
print(f"all: {scores.round(4)}")
4 Pipeline でデータリークを確実に防ぐ
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
# Pipeline のステップは (名前, オブジェクト) のタプルのリスト
pipeline = Pipeline([
("scaler", StandardScaler()), # Step 1: スケーリング
("model", LogisticRegression()) # Step 2: 学習
])
# cross_val_score に渡すだけで OK
# 内部で各fold ごとに: scaler.fit_transform(X_train) → model.fit(X_train_scaled)
# scaler.transform(X_test) → model.predict(X_test_scaled)
# が自動で行われる
scores = cross_val_score(pipeline, X, y, cv=5, scoring="accuracy")
print(f"Pipeline CV: {scores.mean():.4f} ± {scores.std():.4f}")
🔢 数学・統計の補足(文系向け)
「平均 ± 標準偏差」の読み方
| 表記 | 意味 | 良い値は? |
|---|---|---|
0.855 ± 0.008 | 5回の平均が0.855、ばらつきが0.008 | std が小さい = 安定した評価 |
0.855 ± 0.040 | 5回の平均は同じでも大きくブレる | std が大きい = 不安定 |
K値のトレードオフ(直感的説明)
| K値 | 訓練データ割合 | 評価回数 | 特徴 | 使い所 |
|---|---|---|---|---|
K=3 | 66.7% | 3回 | 訓練データが少ない → バイアス高 | データが非常に少ない時 |
K=5 | 80.0% | 5回 | バランスが良い | 標準的な選択 |
K=10 | 90.0% | 10回 | バイアス低・計算コスト高 | 精度重視の場合 |
| LOOCV | (N-1)/N | N回 | 最もバイアスが低いが非常に遅い | データが極端に少ない時のみ |
🏆 Kaggleでの実践的な使い方
| コンペタイプ | 推奨CV戦略 | 理由 |
|---|---|---|
| 一般的な分類 | StratifiedKFold(5) | クラス比率を均等化 |
| 一般的な回帰 | KFold(5) | クラス制約なし |
| 時系列予測 | TimeSeriesSplit | 未来情報の漏洩を防ぐ |
| グループあり | GroupKFold | 同グループが訓練・テストに分かれないよう制御 |
# Kaggle 上位の OOF(Out-of-Fold)予測テンプレート
import numpy as np
from sklearn.model_selection import StratifiedKFold
from sklearn.metrics import roc_auc_score
def run_cv_with_oof(model, X, y, n_splits=5):
skf = StratifiedKFold(n_splits=n_splits, shuffle=True, random_state=42)
oof_preds = np.zeros(len(y)) # 全サンプルの予測を格納
scores = []
for fold, (train_idx, val_idx) in enumerate(skf.split(X, y), 1):
X_tr, X_val = X.iloc[train_idx], X.iloc[val_idx]
y_tr, y_val = y.iloc[train_idx], y.iloc[val_idx]
model.fit(X_tr, y_tr)
oof_preds[val_idx] = model.predict_proba(X_val)[:, 1]
score = roc_auc_score(y_val, oof_preds[val_idx])
scores.append(score)
print(f"Fold {fold}: AUC = {score:.4f}")
oof_auc = roc_auc_score(y, oof_preds)
print(f"\nOOF AUC: {oof_auc:.4f} (CV: {np.mean(scores):.4f} ± {np.std(scores):.4f})")
return oof_preds # スタッキング・ブレンディングに使用
# 使用例
from sklearn.linear_model import LogisticRegression
oof = run_cv_with_oof(LogisticRegression(max_iter=500), X, y)
🔑
OOF(Out-of-Fold)予測は各サンプルが一度だけテストになる時の予測値。全サンプルを網羅するため、スタッキング(モデルの積み重ね)の入力として使われます。Kaggle Gold メダリストがほぼ全員使うテクニックです。
⚠️ よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
| 分類に通常の KFold を使う | 種類の違いを知らない | 分類では 必ず StratifiedKFold を使う |
| CV の外でスケーリングする | 書きやすさ優先 | Pipeline に前処理を入れる |
neg_mean_squared_error の符号を見落とす |
sklearn の符号反転に気づかない | -scores.mean() で正の値にする |
shuffle=False(デフォルト)のまま使う |
パラメータを確認しない | 時系列以外は shuffle=True, random_state=42 を指定 |
| CV スコアだけを見てモデルを選ぶ | CV スコア = 最終評価と思いがち | CV スコアと LB(Leaderboard)スコアの乖離も確認する |
🚀 次のステップ
- 発展:
TimeSeriesSplit— 時系列データでは未来のデータが訓練に混入しないよう、fold を時間順に設計する必要があります。金融・需要予測コンペで必須の知識です - 次回予告 (Day 052): ロジスティック回帰入門 — 「確率」として予測を出す仕組み。シグモイド関数・決定境界・係数の解釈を学びます
📋 自己評価(解いた後に記入)
自分の回答・気づき・メモ: