論文一覧に戻る 📚 用語集トップ 🗺 概念マップ
📚 用語解説
📚 用語解説
交差検証
Cross-Validation
ML基礎
別称: CV

🔖 キーワード索引

CVk-fold評価過学習汎化性能sklearn

交差検証 (cross-validation)」はデータを K 分割し、 学習用と検証用を入れ替えて K 回評価することで汎化性能を推定する手法。 ホールドアウトより低分散の推定が得られ、 ハイパーパラメータ調整・モデル選択の標準ツール。 本ページでは K-fold・Stratified K-fold・LOO・Group K-fold・時系列 CV の使い分けと、 leakage を起こさない pipeline 設計を整理する。

K-fold CVStratified K-foldLeave-One-Out (LOO)Group K-fold時系列 CV (TimeSeriesSplit)Nested CV汎化性能の不偏推定Leakage 防止 pipelinesklearn cross_val_score

これらのキーワードは「データ分割の戦略 → 学習/検証の反復 → スコアの平均と分散 → ハイパーパラメータ調整」という CV の標準フローを構成する。

💡 30秒で分かる結論

🍰 まずはやさしく

データを分けて何度も試す方法です。

正しく性能を測るために使います。

スマホのアプリで何度もテストする感じです。

具体的なやり方とコツを学びます。

交差検証 ── データを複数回分割して評価する手法

💡 実務で効く Tips 集

  1. fold 数のチューニング: 5 と 10 で結果がほぼ同じなら 5 を選ぶ(計算節約)。 結果が大きく違うなら 10 を採用。
  2. random_state は複数試す: 単一の seed の結果が不安定なら Repeated CV で平均化。
  3. fold ごとの予測値を必ず保存: cross_val_predict で OOF を取り、 残差プロットで「どこで間違えているか」を可視化。
  4. 計算時間は n_jobs で並列化: n_jobs=-1 で全コア使用。 大きく短縮できる。
  5. 本番モデルは「全データで再学習」: CV は評価用。 最終モデルは全データで再 fit する(より良いモデルになる)。
  6. fold 間で学習データ分布が偏らないか確認: StratifiedKFold を使うか、 fold ごとの y の統計を出して確認。
  7. 外部検証セット: 別年度・別地域・別期間のデータでもう一段検証すると現実的な性能が見える。
  8. CV と LB の乖離をモニタ: Kaggle 風に「CV が改善しても LB が下がる」場合は CV 設計を疑う。

🔮 これからの CV — 大規模・深層学習時代

深層学習の時代、 1 つのモデル学習に何時間〜何日もかかるため、 k=10 の CV を全部回すのは現実的ではありません。 代替手段として:

それでも、 「学習に使っていないデータで性能を測る」という CV の本質は変わりません。 道具は進化しても、 思想は普遍です。

🍳 さらに使えるレシピ — 47都道府県データの完全分析

レシピA: 学習時間と CV スコアの両方を計測

📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行) 都道府県 A1101(総人口) A1301(15歳未満人口) A1303(65歳以上人口) A4101(出生数) A9101(婚姻件数) 北海道 5,092,000 514,000 1,681,000 24,430 17,281 東京都 14,086,000 1,513,000 3,205,000 86,348 71,774 沖縄県 1,468,000 236,000 350,000 12,549 6,316 …(全 47 行)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import pandas as pd
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import Ridge

# pipe / X / y はこのあとのブロックで作っているので、ここでも用意しておく
_d = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
_d = _d[_d['SSDSE-B-2026'] == 2023]
X = _d[['A1101', 'A1301', 'A1303', 'A9101']].astype(float).values
y = _d['A4101'].astype(float).values          # 出生数を当てる
pipe = Pipeline([('s', StandardScaler()), ('m', Ridge(alpha=1.0))])

from sklearn.model_selection import cross_validate
res = cross_validate(pipe, X, y, cv=5,
                     scoring='r2', return_train_score=True)
print('fit_time     :', res['fit_time'])
print('score_time   :', res['score_time'])
print('train_score  :', res['train_score'])
print('test_score   :', res['test_score'])
print('train-test差 :', res['train_score'].mean() - res['test_score'].mean())
# 差が大きいと過学習サイン
📤 実行例(実測) fit_time : [0.00080276 0.0004189 0.00036216 0.00034595 0.00036025] score_time : [0.00024128 0.00016499 0.00015473 0.00016689 0.00015688] train_score : [0.99746407 0.99515543 0.99682259 0.99620975 0.99706303] test_score : [0.90007941 0.99527443 0.99230862 0.99475268 0.97173257] train-test差 : 0.02571343019910688

レシピB: スコア分布を箱ひげで

📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行) 都道府県 A1101(総人口) A1301(15歳未満人口) A1303(65歳以上人口) A4101(出生数) A9101(婚姻件数) 北海道 5,092,000 514,000 1,681,000 24,430 17,281 東京都 14,086,000 1,513,000 3,205,000 86,348 71,774 沖縄県 1,468,000 236,000 350,000 12,549 6,316 …(全 47 行)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import os
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import Ridge, Lasso, LinearRegression
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import cross_val_score, KFold

os.makedirs('outputs', exist_ok=True)  # 保存先のフォルダを作っておく

# ── この抜粋だけで動くように、X, y と比べるモデル一覧を用意する ──
_d = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
_d = _d[_d['SSDSE-B-2026'] == 2023]
X = _d[['A1101', 'A1301', 'A1303', 'A9101']].astype(float).values
y = _d['A4101'].astype(float).values          # 出生数を当てる
models = {
    'Linear': LinearRegression(),
    'Ridge':  Ridge(alpha=1.0),
    'Lasso':  Lasso(alpha=1.0),
    'RF':     RandomForestRegressor(n_estimators=100, random_state=0),
}

fold_scores = {}
for name, model in models.items():
    pipe = Pipeline([('s', StandardScaler()), ('m', model)])
    fold_scores[name] = cross_val_score(pipe, X, y, cv=KFold(10, shuffle=True, random_state=42),
                                        scoring='r2')

# ラベルは xticks で付ける(boxplot の引数名は版によって labels / tick_labels と違う)
_names = list(models.keys())
plt.boxplot([fold_scores[m] for m in _names])
plt.xticks(range(1, len(_names) + 1), _names, rotation=30)
plt.ylabel('R² (10-fold)')
plt.title('Model comparison on SSDSE-B-2026')
plt.tight_layout(); plt.savefig('outputs/cv_box.png', dpi=120)
for m in _names:
    print(f'{m:8s} 平均 R² = {fold_scores[m].mean():+.3f}')
📤 実行例(実測) Linear 平均 R² = +0.995 Ridge 平均 R² = +0.967 Lasso 平均 R² = +0.994 RF 平均 R² = +0.934

レシピC: 残差マップで地理的パターンを発見

📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行) 都道府県 A1101(総人口) A1301(15歳未満人口) A1303(65歳以上人口) A4101(出生数) A9101(婚姻件数) 北海道 5,092,000 514,000 1,681,000 24,430 17,281 東京都 14,086,000 1,513,000 3,205,000 86,348 71,774 沖縄県 1,468,000 236,000 350,000 12,549 6,316 …(全 47 行)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import os
import pandas as pd
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import Ridge
from sklearn.model_selection import cross_val_predict

os.makedirs('outputs', exist_ok=True)  # 保存先のフォルダを作っておく

# ── この抜粋だけで動くように、df・X・y・pipe を用意する ──
df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
df = df[df['SSDSE-B-2026'] == 2023]
X = df[['A1101', 'A1301', 'A1303', 'A9101']].astype(float).values
y = df['A4101'].astype(float).values
pipe = Pipeline([('s', StandardScaler()), ('m', Ridge(alpha=1.0))])

y_oof = cross_val_predict(pipe, X, y, cv=5)
residual = y - y_oof
res_df = pd.DataFrame({'都道府県': df['Prefecture'].values, 'residual': residual})
# 残差を地図にプロット(別途 GeoPandas を使う)
res_df.to_csv('outputs/residual_by_pref.csv', index=False)
print(res_df.reindex(res_df['residual'].abs().sort_values(ascending=False).index).head())
# 残差が偏っていれば特徴量不足のサイン
📤 実行例(実測) 都道府県 residual 12 東京都 -5237.112768 0 北海道 -4927.672402 46 沖縄県 2983.798646 26 大阪府 2905.233239 22 愛知県 2742.036742

レシピD: 特徴量重要度を CV で安定推定

📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行) 都道府県 A1101(総人口) A1301(15歳未満人口) A1303(65歳以上人口) A4101(出生数) A9101(婚姻件数) 北海道 5,092,000 514,000 1,681,000 24,430 17,281 東京都 14,086,000 1,513,000 3,205,000 86,348 71,774 沖縄県 1,468,000 236,000 350,000 12,549 6,316 …(全 47 行)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import pandas as pd
from sklearn.inspection import permutation_importance
from sklearn.ensemble import RandomForestRegressor

# ── この抜粋だけで動くように、X・y と特徴量名 features を用意する ──
_d = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
_d = _d[_d['SSDSE-B-2026'] == 2023]
features = ['A1101', 'A1301', 'A1303', 'A9101']   # 総人口・15歳未満・65歳以上・婚姻件数
X = _d[features].astype(float).values
y = _d['A4101'].astype(float).values               # 出生数

rf = RandomForestRegressor(n_estimators=300, random_state=42)
rf.fit(X, y)
perm = permutation_importance(rf, X, y, n_repeats=30, random_state=42, n_jobs=-1)
importance = pd.DataFrame({
    'feature': features,
    'importance': perm.importances_mean,
    'std': perm.importances_std,
}).sort_values('importance', ascending=False)
print(importance)
📤 実行例(実測) feature importance std 0 A1101 0.171333 0.032423 1 A1301 0.168706 0.031241 3 A9101 0.146268 0.028509 2 A1303 0.070613 0.013210

レシピE: 信頼区間付きの予測

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from sklearn.ensemble import GradientBoostingRegressor
# 分位回帰で予測区間
lower = GradientBoostingRegressor(loss='quantile', alpha=0.1, random_state=42)
upper = GradientBoostingRegressor(loss='quantile', alpha=0.9, random_state=42)
mean  = GradientBoostingRegressor(loss='squared_error', random_state=42)

for model in [lower, upper, mean]:
    pipe = Pipeline([('s', StandardScaler()), ('m', model)])
    score = cross_val_score(pipe, X, y, cv=5, scoring='r2')
    print(f'{model.loss}: R² = {score.mean():.3f}')
📤 実行例(実測) quantile: R² = 0.070 quantile: R² = 0.664 squared_error: R² = 0.932

🧬 さらに深掘り — CV の数理

期待値と分散

$\widehat{\mathrm{CV}}_k$ の期待値は $\mathbb{E}[\widehat{\mathrm{CV}}_k] = \mathbb{E}[L(y, \hat f^{(-V)}(x))]$ で、 学習データサイズが $(k-1)/k \cdot n$ のモデルの真の誤差。 LOOCV ではこれが $(n-1)/n \cdot n \approx n$ サイズに近く、 真の誤差にほぼ等しい。 k=2 では半分のデータで学習するため、 推定誤差は実際より悪く出る。

分散の上下界

CV 推定量の分散は fold 間の相関で決まる。 fold が完全独立なら $\mathrm{Var}(\widehat{\mathrm{CV}}_k) = \sigma^2/k$ だが、 実際には学習データが重複するため相関 $\rho > 0$ があり、 $\mathrm{Var} = \sigma^2(1/k + \rho(k-1)/k)$。 k が増えると 1/k 項は減るが $\rho(k-1)/k$ 項は増加し、 ある k で最小化。 経験的に k=10 前後がこの最小付近とされる。

バイアス・分散・複雑度のトレードオフ

CV はモデル複雑度を選ぶ際の指標。 学習誤差は複雑度が上がるほど単調減少だが、 検証誤差 (CV) は U 字型を描く。 最低点がベスト複雑度。 SSDSE-B (n=47) では Ridge の alpha や RF の max_depth について検証曲線を描くと、 この U 字が観察できる。

🌟 最後に — CV を「儀式」ではなく「対話」に

CV は「決まった手順を機械的に実行する儀式」ではなく、 「データとモデルの間の対話」です。 fold ごとのスコアを見れば「どのデータで間違えているか」がわかり、 学習曲線を見れば「データを増やすと改善するか」がわかり、 検証曲線を見れば「ハイパーパラメータの感受性」がわかります。

SSDSE-B-2026 のような小データでは、 CV の標準偏差が大きくなりがちです。 そこで諦めずに「なぜ大きいのか」「どの fold で外れているか」「外れている県の特徴は何か」を追求すると、 単なるスコアの数字を超えた知見が得られます。 CV は学びの入口であり、 出口でもあります。 データサイエンスを志すなら、 まずここを深く掘ってください。

🏫 授業用の練習ノートブック(完全版)

本セクションをそのまま Jupyter Notebook に貼り付けて実行できます。 SSDSE-B-2026 から CV までの一連の流れを 1 つのコードブロックで体験:

📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行) 都道府県 A1101(総人口) A1303(65歳以上人口) A4101(出生数) 北海道 5,092,000 1,681,000 24,430 東京都 14,086,000 3,205,000 86,348 沖縄県 1,468,000 350,000 12,549 …(全 47 行)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# =========================================================
# SSDSE-B-2026 で交差検証を体験する完全コード
# =========================================================
import pandas as pd, numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import (
    KFold, StratifiedKFold, LeaveOneOut,
    cross_val_score, cross_val_predict, cross_validate,
    GridSearchCV, RepeatedKFold,
)
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import Ridge
from sklearn.ensemble import RandomForestRegressor

# 1. データ読み込み
df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
df = df[df['SSDSE-B-2026']==2023].dropna(subset=['A4101','A1101','A1303']).reset_index(drop=True)
df['高齢化率'] = df['A1303']/df['A1101']*100

features = ['A1101','高齢化率']
X = df[features].values
y = df['A4101'].values
print(f'data: {X.shape}, target range: [{y.min():.2e}, {y.max():.2e}]')

# 2. 基本 5-fold CV
pipe = Pipeline([('s', StandardScaler()), ('m', Ridge(alpha=1.0))])
cv = KFold(5, shuffle=True, random_state=42)
scores = cross_val_score(pipe, X, y, cv=cv, scoring='r2')
print(f'5-fold R² = {scores.mean():.3f} ± {scores.std():.3f}')

# 3. LOOCV (47 fold)
loo = LeaveOneOut()
loo_scores = cross_val_score(pipe, X, y, cv=loo, scoring='neg_mean_absolute_error')
print(f'LOOCV MAE = {-loo_scores.mean():.2e}')

# 4. Repeated CV (50 fold)
rkf = RepeatedKFold(n_splits=5, n_repeats=10, random_state=42)
r_scores = cross_val_score(pipe, X, y, cv=rkf, scoring='r2')
print(f'Repeated 5-fold (50 runs) R² = {r_scores.mean():.3f} ± {r_scores.std():.3f}')

# 5. Grid Search
grid = GridSearchCV(pipe, param_grid={'m__alpha':[0.01,0.1,1,10,100]},
                    cv=cv, scoring='r2')
grid.fit(X, y)
print(f'best alpha = {grid.best_params_}, score = {grid.best_score_:.3f}')

# 6. OOF 予測
y_oof = cross_val_predict(pipe, X, y, cv=cv)
print(f'OOF R² = {1 - ((y-y_oof)**2).sum()/((y-y.mean())**2).sum():.3f}')

# 7. 残差分析
residual = y - y_oof
res_df = pd.DataFrame({'pref': df['Prefecture'], 'residual': residual}).sort_values('residual')
print('\n最も過小予測されている県:')
print(res_df.tail(5).to_string(index=False))
print('\n最も過大予測されている県:')
print(res_df.head(5).to_string(index=False))
📤 実行例(実測) data: (47, 2), target range: [3.26e+03, 8.63e+04] 5-fold R² = 0.984 ± 0.006 LOOCV MAE = 1.25e+03 Repeated 5-fold (50 runs) R² = 0.971 ± 0.058 best alpha = {'m__alpha': 1}, score = 0.984 OOF R² = 0.986 最も過小予測されている県: pref residual 兵庫県 2403.186866 愛知県 3529.410673 福岡県 4386.283328 大阪府 6103.938864 東京都 6746.249789 最も過大予測されている県: pref residual 北海道 -4403.095152 宮城県 -2375.633980 栃木県 -2225.043037 茨城県 -1977.379282 静岡県 -1916.159188

この一連のコードは、 「CV の基礎 → 拡張 → 解釈」の全体像を 1 ノートブックで把握できます。 講義スライドや実習ハンドアウトにもそのまま使えます。

🔭 上級トピック — Causal CV と Bayesian CV

伝統的 CV は「予測精度」を測るが、 因果推論の文脈では「反事実 (counterfactual) 予測の精度」を測る必要があり、 通常の CV では不十分。 Athey & Wager (2019) の Generalized Random Forest や Double ML は、 CV の中で因果推定量の偏りを補正する技術を組み込んでいます。

Bayesian の文脈では WAICPSIS-LOO (Pareto-Smoothed Importance Sampling LOO) が CV の代替として使われます。 LOOCV を全データから 1 回学習しただけで近似する手法で、 Stan や PyMC で実装されています。

公的統計の文脈では、 SSDSE-B のような全数データに対して因果効果を推定する研究も増加中です。 「東京と地方で同じ施策を実施したら効果はどう違うか」のような問いは、 因果 CV の出番です。

📌 サマリーカード — CV を 30 秒で語る

📜 詳細年表 — CV の発展史

出来事
1931Larson が「データを 2 分割して評価」のアイデアを提案
1968Mosteller & Tukey が "cross-validation" の用語を使用
1974Stone が JRSS-B で CV を統計理論として定式化
1975Geisser が "predictive sample reuse" として独立に提案
1979Efron が Bootstrap を提案。 CV のライバルとして登場
1995Kohavi の IJCAI 論文。 「k=10 が経験的に良い」の根拠
2004Bengio & Grandvalet: CV 分散の不偏推定量は存在しない
2007scikit-learn 初版リリース。 CV の API 標準化
2010Arlot & Celisse の包括的サーベイ
2017Vehtari らが PSIS-LOO を提案。 Bayesian の標準 CV へ
2023Bates et al. が JASA で CV の「真の意味」を再検討

CV は 1930 年代の素朴なアイデアから、 90 年の時を経て統計理論・機械学習・Bayesian の各分野で深化してきました。

🧰 各種ツールでの CV 実装

scikit-learn (Python)

1
2
from sklearn.model_selection import cross_val_score, KFold
scores = cross_val_score(model, X, y, cv=KFold(5, shuffle=True))

R (caret)

1
2
3
4
library(caret)
ctrl <- trainControl(method='cv', number=5)
fit  <- train(y ~ ., data=df, method='lm', trControl=ctrl)
print(fit)

XGBoost

1
2
3
4
5
6
7
import xgboost as xgb
dtrain = xgb.DMatrix(X, label=y)
cv_res = xgb.cv(params={'objective':'reg:squarederror'},
                dtrain=dtrain, num_boost_round=200,
                nfold=5, early_stopping_rounds=20,
                metrics='rmse', seed=42)
print(cv_res.tail())
📤 実行例(実測) train-rmse-mean train-rmse-std test-rmse-mean test-rmse-std 52 7.516601 2.176814 3969.389258 3643.738409 53 6.685224 2.019341 3969.214456 3643.604951 54 6.192113 1.997426 3969.251521 3643.458725 55 5.561644 1.733808 3969.229553 3643.302436 56 5.121152 1.595318 3969.097785 3643.276387

LightGBM

1
2
3
4
5
import lightgbm as lgb
cv_res = lgb.cv({'objective':'regression','metric':'rmse'},
                lgb.Dataset(X, y), num_boost_round=200,
                nfold=5, stratified=False, seed=42,
                callbacks=[lgb.early_stopping(20)])
📤 実行例(実測) [LightGBM] [Info] Auto-choosing col-wise multi-threading, the overhead of testing was 0.000208 seconds. You can set `force_col_wise=true` to remove the overhead. [LightGBM] [Info] Total Bins 34 [LightGBM] [Info] Number of data points in the train set: 36, number of used features: 2 [LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.000157 seconds. You can set `force_row_wise=true` to remove the overhead. And if memory is not enough, you can set `force_col_wise=true`. [LightGBM] [Info] Total Bins 34 [LightGBM] [Info] Number of data points in the train set: 36, number of used features: 2 [LightGBM] [Info] Auto-choosing col-wise multi-threading, the overhead …(以下略)

🎯 詳細ケース集 — 場面別 CV 設計

ケース1: SSDSE-B-2026 で 4 つの回帰モデルを CV で公平に比較

同じ KFold (random_state=42) を全モデルで使うことで、 fold が共通になり比較が公平。 違う seed の CV 結果を比べると差は意味を持ちません。

ケース2: 不均衡分類の StratifiedKFold

「都道府県を高齢化率 30% を境に 2 値分類」する場合、 35 県が陽性、 12 県が陰性のような不均衡。 StratifiedKFold で fold ごとの比率を保つ。 通常 KFold だと fold によっては陽性 0 件になり評価不能になる。

ケース3: 時系列の TimeSeriesSplit

SSDSE-B の年度別パネルを使い、 「2013-2018 → 2019」「2013-2019 → 2020」「2013-2020 → 2021」と段階的に検証。 これが現実的な予測精度に最も近い評価。

ケース4: 地域グループでの GroupKFold

8 地域を 4 fold (=2 地域ずつ) に分け、 「他地域で学習 → 対象地域を予測」。 地域間の汎化を厳しく評価したい時。

ケース5: 47 サンプル全部を使った LOOCV

「都道府県 1 つを除いて 46 県で学習 → 1 県を予測」を 47 回繰り返す。 全データを最大限活用しつつ、 1 県ずつの予測誤差を可視化できる利点。

📝 論文記述のテンプレート

本研究では、 47 都道府県データ (n=47) に対し Ridge 回帰モデルを構築し、
予測性能を 5-fold 交差検証 (random_state=42, shuffle=True) で評価した。
全ての前処理 (StandardScaler) は scikit-learn の Pipeline 内で各 fold 内で
fit することにより、 検証セットからのデータリークを防いだ。
評価指標は決定係数 R² および RMSE を用い、 5 fold の平均 ± 標準偏差を報告する。
ハイパーパラメータ (Ridge の α) は、 内側 3-fold の Grid Search による
Nested CV で選択し、 候補は {0.1, 1, 10, 100} とした。

結果として、 R² = 0.98 ± 0.01、 RMSE = 1,800 ± 550 人 (出生数) を得た。
Out-of-Fold 予測の残差プロットからは、 東京都が最大の正残差 (約 +6,700 人)
を示し、 当該県が外れ値として影響することが確認された。
比較として、 LinearRegression および k-NN (k=5) でも同じ CV 設定で評価し、
Ridge が両者を有意に上回ることを Wilcoxon 符号付き順位検定で確認した
(p < 0.05)。
  

このような記述を含めれば、 査読者の不安を最小化できます。

🌟 キーテイクアウェイ

  1. CV は「学習に使わなかったデータでの誤差」を $k$ 回の平均で推定する不偏的手法。
  2. k=5 か k=10 が経験的な定番。 計算余裕があり論文等正式報告なら Repeated を併用。
  3. 前処理は必ず Pipeline 内で fold 内 fit。 これが第一の鉄則。
  4. 時系列・グループ性・不均衡など、 データの構造を反映した分割を選ぶ。
  5. ハイパー選択と最終評価は Nested CV で分離する。
  6. SSDSE-B-2026 のような小データでは LOOCV が現実的な選択肢。
  7. 結果は平均だけでなく標準偏差も報告し、 fold ごとのバラつきを明示する。

🧠 雑多な深掘りトピック

CV と Bootstrap の違い

CV は非復元分割で、 全データが必ず一度だけ検証に使われる。 Bootstrap は復元抽出で、 同じサンプルが複数回使われる一方、 ~37% のサンプルは抽出されず "OOB (Out-Of-Bag)" として検証に使われる。 統計量の分散推定なら Bootstrap、 モデル評価なら CV、 というのが大まかな使い分けです。

CV と情報量規準 (AIC, BIC) の関係

LOOCV は線形モデルにおいて AIC とほぼ等価。 Stone (1977) の結果として知られる。 ただし非線形・非パラメトリックモデルでは CV のほうが汎用的。 BIC は事前分布つきの周辺尤度近似で、 CV とは異なる目的(モデル選択 vs 予測誤差推定)を持つ。

CV で測れないもの

CV は「過去データへの汎化」を測りますが、 「未来データへの汎化」「他地域への汎化」「他文化圏への汎化」は別の枠組み (transportability) が必要です。 SSDSE-B の 2023 年データで CV が高精度でも、 2030 年に同じ精度を出す保証はありません(人口動態の構造変化)。

CV の計算量削減技法

LOOCV を Ridge 回帰で行う場合、 ハット行列の対角成分から閉形式で計算できる(PRESS 統計量)。 また Stochastic CV (一部 fold のみ評価) や Subset CV (データの 20% だけ使って評価) などの近似手法もあります。 大規模深層学習では特に重要。

CV のロバスト化

通常 CV は平均誤差を報告しますが、 中央値や分位点を使うことで外れ値に頑健になります。 また Trimmed CV (上下 10% を除いた平均) も論文で使われます。 SSDSE-B では東京都が常に外れ値なので、 Trimmed CV で「東京を除いた 46 県での汎化」も併報すると有用です。

📝 最終チェックリスト — CV を使う前に

このチェックリストを毎回確認するだけで、 CV 関連の事故は 9 割減ります。

🧷 補足 — 47都道府県データを使い倒す

CV は道具ですが、 SSDSE-B-2026 のような小サンプル公的統計データに適用する際には、 サンプル数の少なさを補う工夫が重要です。 たとえば 47 サンプルしかない場合、 5-fold で各 fold は約 9〜10 サンプルになり、 fold ごとのスコアのばらつきが大きくなります。 そのため、 単一の k=5 ではなく Repeated 5-fold (3〜10 回繰り返し) を併用するのが現実的です。 また、 LOOCV は計算コストが許せばより安定した推定になります。

SSDSE-B には 2013-2023 年の 11 年分のパネルデータがあり、 「年度を fold とする」分割も有効です。 これは時間的汎化能力を直接測れる方法で、 公共政策の評価や将来予測の文脈で特に重要です。 1 年分を test、 残りを train として 11 fold CV を回せば、 「ある年のデータが他年度の予測にどれだけ役立つか」が見えてきます。

最後に、 CV の結果は「数値」だけでなく「物語」として語ることが大事です。 「R² = 0.85 ± 0.06」を見て、 「これは何を意味するのか」「実用上はどれくらい当てになるのか」「外れ値はどの県か」「特徴量を変えるとどう変わるか」を考察する習慣を持つと、 単なる機械的な評価から脱却し、 データサイエンティストとしての洞察力が育ちます。

交差検証は、 機械学習の評価における最重要技術の1つでありながら、 同時に「データに対する誠実な向き合い方」を体現する技術でもあります。 リークなく、 適切な分割で、 結果を平均と分散で語る ― この3点が守られている分析は、 信頼に値する分析です。 SSDSE-B-2026 を題材に、 ぜひこの基礎を身につけて、 将来の様々なデータ分析の場面で武器にしてください。

🧷 補遺 — 実装の注意点 5 件

  1. numpy 配列と DataFrame の混在: cross_val_score は両方を受け付けるが、 列順序がずれる事故を防ぐため、 特徴量は DataFrame で持ち、 列名を保持するのが安全。
  2. NaN を含む配列: そのまま渡すと多くの sklearn モデルでエラー。 Pipeline 内で SimpleImputer を入れるか、 事前に dropna する。 dropna する場合は CV の外でも問題ないが、 サンプル数が減る点に注意。
  3. n_jobs=-1 の罠: 全コア並列だが、 メモリが足りないと OS が swap して逆に遅くなる。 大規模データでは n_jobs=4 など適切な数に。
  4. cross_val_predict の制約: 確率予測 (predict_proba) を使うと「同じサンプルが複数 fold で重複なく出力される」性質に注意。 集約方法を明示的に指定する。
  5. warning の扱い: ConvergenceWarning などが大量に出ると本当のエラーを見落とす。 warnings.filterwarnings で適切に抑制 (一時的に)。

🪜 実データで CV を回すときの段取り

  1. 探索的データ解析 (EDA): まず df.describe() と分布プロット。 ターゲットの偏り、 説明変数の欠損率を把握。
  2. Baseline モデル: 平均予測 (DummyRegressor) で「最低限の R²」を知る。 CV で 0 近辺なら正常。
  3. シンプルなモデル: LinearRegression で 5-fold CV。 ここで R² が 0.6 未満なら特徴量設計を疑う。
  4. 正則化: Ridge / Lasso で過学習耐性をつける。 alpha を validation_curve でスキャン。
  5. 非線形モデル: RandomForest / GradientBoosting。 ハイパーは GridSearchCV で内側 CV。
  6. 解釈: cross_val_predict で OOF 取得 → 残差プロット → 特徴量重要度。
  7. 外部検証: 別年度・別地域のデータで最終確認。

この段取りに沿って進めれば、 CV を「儀式」ではなく「対話」として運用できます。

🖼 視覚で確認する:交差検証の関連図

交差検証は「データ分割と汎化誤差推定」の組み合わせです。 関連する3点の図を並べて、 概念的な位置づけを確認します。

単回帰の学習例
図1: 学習データに対する回帰モデルのフィット。 訓練誤差だけ見ると過学習を見落とすため、 別の分割で汎化誤差を確認する必要がある。
残差プロットによる検証
図2: 残差プロットで予測のズレを可視化。 CV の各 fold ごとに残差を確認すると、 局所的な失敗パターンを掴める。
正則化による過学習抑制
図3: 正則化強度の選択は CV で行う代表例。 k-fold CV で λ を変えて MSE を比較し、 最も汎化誤差が低い λ を採用する。

3点の図はそれぞれ「学習」「診断」「ハイパーパラメータ選択」の場面を示しており、 CV はそのいずれにも組み込まれます。

📝 理解度チェック — 交差検証

交差検証の理解度を確認する練習問題。 自分でコードを書いて確かめると一段定着する。

  1. 練習問題 1: SSDSE-B-2026 で「出生数」を「総人口」「消費支出」で予測するモデルを 5-fold CV で評価するとき、 各 fold のサイズが約 9〜10 件になる理由を述べよ。
  2. 練習問題 2: 時系列データに通常の k-fold CV を使うと leakage が起きる理由を、 「未来 → 過去の予測」になる構造で説明せよ。
  3. 練習問題 3: 不均衡分類データで stratified k-fold が必要な理由を、 各 fold のクラス分布の観点から述べよ。
  4. 練習問題 4: nested CV における外側 CV と内側 CV の役割の違いを説明し、 楽観バイアスがなぜ防げるかを述べよ。
  5. 練習問題 5: Pipeline(前処理 + モデル)を CV にかけるときに「fold 内で fit」する重要性を、 標準化 leakage の観点から述べよ。

💬 これらの問題に答えられれば、 実務での「正しい CV 設計」ができるようになる。 自分でデータを動かしながら理解を深めよう。

📍 文脈 ── どこで出会うか

🍰 まずはやさしく

結果のばらつきを防ぐ保険のようなものです。

評価の信頼性を高めるために使います。

1回のテストだけで成績を決めないのと似ています。

この手法がどこで使われるかを見ていきましょう。

「精度95%でした」と言うとき、 1回のtrain/testだけだと運次第。 CVは「複数回試して平均」する保険です。 競技でも論文でも、 評価の信頼性は CV で担保するのが標準。

本ページでは「cross validation」を扱う。 統計データ分析コンペティション (2026) の教材で、 SSDSE-B-2026 (47 都道府県 × 複数年 × 100 超列) の実データを使った再現可能な学習を目指す。

「cross validation」は統計・データサイエンスの体系における重要概念のひとつ。 本ページは「定義・直感・数式・実装・落とし穴・関連手法」の 6 視点で構成され、 各視点は独立して読めるが順序通り読むと体系的な理解が得られる。

🎨 直感で掴む

🍰 まずはやさしく

模試を何度も受けて平均を出すイメージです。

たまたま当たっただけという運をなくします。

部活の練習試合を何度もして実力を測る感じです。

図を使って直感的に仕組みを理解しましょう。

「テスト勉強」に例えるなら:

図:5-fold CV の模式図。 5回の試行で各サンプルが必ず1回ずつテスト側に回る。

SSDSE-B-2026 で考えると分かりやすい。 47 都道府県データで「総人口ほか → 出生数」を回帰したいとき、 単純に 32 県で学習・15 県でテストすると、 偶然どの県がテスト側に入るかで $R^2$ が 0.92 にも 1.00 にもブレる。 5-fold CV なら 9〜10 県ずつテストを 5 回回し、 全 47 県が必ず 1 回テストに使われるため、 1 回分割より未知データ汎化性能の推定が安定する(例: $R^2 = 0.98 \pm 0.01$)。

選択指針として、 サンプル数 $n$ が小さい (たとえば $n < 100$) なら LOOCV か 10-fold、 中程度なら 5-fold、 時系列データなら TimeSeriesSplit (未来を未学習で守る)、 クラス分布が偏っているなら StratifiedKFold (各 fold のラベル比率を揃える) が定石。 また、 同一被験者から複数サンプルがある場合は GroupKFold で被験者単位に分け、 リークを防ぐ。

📐 定義/数式

🍰 まずはやさしく

計算式で正しさを証明する方法です。

精度のばらつきを数字で表すために使います。

テストの点数の平均とズレを計算する感じです。

数式を使った定義と、詳しい計算方法を読みます。

【k-fold CV のスコア推定】
$$\text{CV score} = \frac{1}{k}\sum_{i=1}^{k} \text{Score}(f_i, D_{\text{test},i})$$
$f_i$ は fold $i$ の学習モデル、 $D_{\text{test},i}$ は fold $i$ のテストデータ

標準誤差も算出できる:$SE = \sigma_{\text{folds}} / \sqrt{k}$。 これで信頼区間付きで「精度 = $0.85 \pm 0.02$」と報告可能。

📐 統計検定との接続 — モデル比較を統計的に語る

「モデルA より B が良い」と言うには、 単なる CV スコアの差だけでなく統計的有意性を見るのが望ましい。 Wilcoxon 符号付き順位検定や Friedman 検定が定番。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from sklearn.model_selection import KFold, cross_val_score
from sklearn.linear_model import Ridge, Lasso
from scipy.stats import wilcoxon

cv = KFold(5, shuffle=True, random_state=42)
score_A = cross_val_score(Pipeline([('s',StandardScaler()),('m',Ridge())]), X, y, cv=cv, scoring='neg_root_mean_squared_error')
score_B = cross_val_score(Pipeline([('s',StandardScaler()),('m',Lasso())]), X, y, cv=cv, scoring='neg_root_mean_squared_error')

# fold ごとに対応のあるサンプル(同じ分割)
stat, p = wilcoxon(score_A, score_B)
print(f'Ridge vs Lasso: stat={stat:.2f}, p={p:.4f}')
if p < 0.05:
    print('差は統計的に有意')
📤 実行例(実測) Ridge vs Lasso: stat=7.00, p=1.0000

5 fold だけでは検定力が弱いので、 Repeated CV (30 fold 程度) を使うのが現実的。

🛠 MLOps と CV — 再現性と監査

本番運用では CV の結果を MLflow や Weights & Biases に記録するのが標準。 「いつ、 どのデータで、 どんなハイパーパラメータで、 CV スコアいくつだったか」が追跡可能になります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import mlflow
mlflow.set_experiment('ssdse-b-prediction')
with mlflow.start_run(run_name='ridge-5fold'):
    cv = KFold(5, shuffle=True, random_state=42)
    pipe = Pipeline([('s',StandardScaler()),('m',Ridge(alpha=1.0))])
    scores = cross_val_score(pipe, X, y, cv=cv, scoring='r2')
    mlflow.log_param('model','Ridge')
    mlflow.log_param('alpha', 1.0)
    mlflow.log_param('cv_k', 5)
    mlflow.log_metric('r2_mean', scores.mean())
    mlflow.log_metric('r2_std', scores.std())
    for i,s in enumerate(scores):
        mlflow.log_metric(f'fold_{i}_r2', s)

監査・規制対応・論文付録のいずれにも有用。 CV 結果は「再現可能な数値」であるべき。

🎯 データ品質と CV — 何を信用するか

CV スコアが優秀でも、 元データの品質が低ければ意味がありません。 SSDSE-B のような公的統計データでも以下に注意:

📖 さらに学ぶ — 入門〜上級

入門

中級

上級

🔬 数式を言葉で読み解く

k-fold CV のスコア式 $\text{CV score} = \dfrac{1}{k}\sum_{i=1}^{k} \text{Score}(f_i, D_{\text{test},i})$ に登場する記号を 1 つずつ確認します。 SSDSE-B-2026 の 47 都道府県データを $k=5$ で分割する文脈で具体化します。

記号読み方意味SSDSE-B-2026 (k=5) での例
$k$ケー (分割数)データを何個の fold に分けるか。 慣例 5 または 1047 県を 5 分割 → 各 fold 約 9〜10 県
$i$アイ (添え字)fold 番号 ($1 \le i \le k$)$i=1$ なら 1 番目の fold (北海道〜青森ほか)
$D_{\text{test},i}$D テスト i (テスト集合)fold $i$ のテストデータ (検証用)$D_{\text{test},1}$ = 1 番目の fold に含まれる 9〜10 県
$D_{\text{train},i}$D 訓練 ifold $i$ 以外の残り全データ (学習用)$D_{\text{train},1}$ = 残り 37〜38 県
$f_i$エフ i (モデル)$D_{\text{train},i}$ で学習されたモデル ($i$ ごとに別物)$f_1$ = 残り 37 県で訓練した回帰モデル
$\text{Score}$スコア関数予測精度の指標 (R², RMSE, accuracy, など)出生数予測なら $R^2$ や RMSE
$\text{CV score}$シーブイスコア全 fold の平均スコア (汎化誤差の推定値)5 fold の R² の平均 (例: 0.98)
$\sigma_{\text{folds}}$シグマ foldfold 間スコアの標準偏差 (ばらつき)5 fold の R² の SD (例: 0.01)

式の読み下し: 「$k$ 個ある fold それぞれについて、 (a) その fold を除いた残りで学習し ($f_i$ を得る)、 (b) 学習に使わなかった $D_{\text{test},i}$ で予測しスコアを取り、 (c) $k$ 個のスコアを単純平均する」。 これにより全データが過不足なく 1 回ずつ検証に使われ、 汎化誤差の不偏推定が得られます。

派生概念:

k
分割数。 慣例は 5 or 10。 大きいほど分散減・計算重 (LOOCV は $k=n$ の極限)
Stratified
分類で各 fold 内のクラス比率を揃える。 不均衡データで必須
Repeated k-fold
k-fold を複数回ランダム分割でくり返し平均。 fold 分割依存の偶然を平均化
Nested CV
外側で $f_i$ の評価、 内側でハイパーパラメータ選択。 「調整時の漏洩」を防ぐ

🔬 数式を言葉で読み解く(4要素構造/1200字以上)

交差検証 (Cross-Validation) の核心は、 「学習に使わなかったデータでモデルの性能を測る」ことです。 k-fold CV は数学的には次のように定式化されます:$$\widehat{\mathrm{CV}}(f) = \frac{1}{k} \sum_{j=1}^{k} \frac{1}{|V_j|} \sum_{i \in V_j} L\bigl(y_i,\ \hat{f}^{(-j)}(x_i)\bigr)$$ ここで $V_j$ は $j$ 番目の検証 fold(インデックス集合)、 $\hat{f}^{(-j)}$ は $V_j$ を除いた残り全データで学習されたモデル、 $L$ は損失関数(回帰なら $L(y,\hat y) = (y-\hat y)^2$、 分類なら 0-1 損失や交差エントロピー)です。 これは「汎化誤差 $\mathbb{E}_{(x,y) \sim p}[L(y,f(x))]$ の不偏推定量」として、 単純な訓練誤差より遥かに信頼できる指標になります。

要素1: 分割 $V_1, V_2, \dots, V_k$ — 「どう分けるか」

データ集合 $\mathcal{D} = \{(x_i, y_i)\}_{i=1}^{n}$ を互いに素な $k$ 個の fold $V_1, \dots, V_k$ に分けます($\bigcup_j V_j = \{1,\dots,n\}$ かつ $j \neq j'$ なら $V_j \cap V_{j'} = \emptyset$)。 各 fold のサイズは $|V_j| \approx n/k$。 SSDSE-B-2026 (47都道府県) を $k=5$ で分ければ各 fold は約 9〜10 県。 重要な性質は(a) 全データが必ず一度だけ検証に使われる(b) 学習・検証は完全に分離されること。 これによりホールドアウト法 (単一の train/test 分割) と比べて、 推定の分散が大幅に減ります。 ただし都道府県データのように地理的相関がある場合は単純ランダム分割が偏ることもあり、 層化 (Stratified) や地域別 (Group K-Fold) を選びます。

要素2: 学習 $\hat{f}^{(-j)}$ — 「fold ごとに別モデル」

$j$ 番目の fold では、 検証セット $V_j$ を除いた残り $\mathcal{D} \setminus V_j$(約 $\frac{k-1}{k}n$ サンプル) を学習に使い、 モデル $\hat{f}^{(-j)}$ を得ます。 ここでの重要点は「fold ごとに完全に別のモデルを訓練する」こと。 ハイパーパラメータ調整や前処理(標準化・特徴量選択)も各 fold 内で完結させなければ、 検証セットの情報が学習にリークします(典型的な落とし穴)。 scikit-learn では Pipeline + cross_val_score を使うことでリーク防止が自動化されます。

要素3: 損失関数 $L$ — 「何を測るか」

損失関数 $L(y, \hat{y})$ はタスクで変えます。 回帰なら MSE = $(y-\hat y)^2$、 RMSE = $\sqrt{\mathrm{MSE}}$、 MAE = $|y-\hat y|$、 MAPE = $|y-\hat y|/|y|$ など。 分類なら 0-1 損失(誤分類率)、 交差エントロピー、 log-loss など。 SSDSE-B で「出生数を総人口・高齢化率・年平均気温で予測」する回帰タスクなら RMSE か MAE を CV スコアにします。 損失の選択は意思決定の損失構造を反映すべきです(外れ値に頑健にしたいなら MAE、 大きな誤りを強くペナルティするなら MSE)。

要素4: 平均と分散 — 「k 個の値をどう要約するか」

$k$ 個の fold ごとの誤差 $e_j = \frac{1}{|V_j|} \sum_{i \in V_j} L(y_i, \hat{f}^{(-j)}(x_i))$ を平均して $\widehat{\mathrm{CV}} = \frac{1}{k}\sum_j e_j$ を得ます。 同時に分散 $s^2 = \frac{1}{k-1}\sum_j (e_j - \widehat{\mathrm{CV}})^2$ も計算し、 「誤差の平均 ± 標準偏差」を報告するのが標準。 $k$ が大きい(LOOCV: $k=n$)と平均は不偏に近いが計算コストが高く、 $k=5$ や $10$ が経験的に良いバランス。 同じ実験を複数回の反復 (repeated) CVで行い、 分散をさらに減らす手法もあります。

🔬 理論的背景 — バイアス・分散・k の影響

CV 推定量 $\widehat{\mathrm{CV}}_k$ には統計的に重要な性質があります:

経験的に k=5 や k=10 が「バイアス・分散・計算量」のスイートスポット。 SSDSE-B のような小データでは LOOCV のバイアスの優位が分散の悪化を上回ることもあり、 ケースバイケースで比較すべきです。

📝 さらに学ぶ — CV と隣接トピック

CV はモデル評価の基礎ですが、 隣接する技術と組み合わせるとさらに強力になります。 Out-Of-Fold (OOF) 予測は CV の各 fold で得た予測値を全データ分つなげたもので、 Stacking(メタ学習)の入力になります。 Permutation Importance は CV 内で特徴量をシャッフルして性能低下を測る重要度指標。 SHAP(説明可能AI)も OOF 予測上で計算するとリークを防げます。

Kaggle 等の競技では「CV と LB (Leaderboard) の相関」を重視します。 CV が改善しても LB が下がるなら、 CV 設計が public test に偏っている可能性。 逆に CV が安定していて LB と相関しているなら、 そのモデル選択は信頼できる、 という判断が経験的に使われます。 公的統計データでも、 「年度間の汎化」(2023年で学習 → 2024年で検証)は実務的に重要な評価軸です。

🧪 CV の種類を実装で見る

KFold

1
2
3
4
5
6
from sklearn.model_selection import KFold
import numpy as np
X = np.arange(20).reshape(20,1); y = np.arange(20)
kf = KFold(n_splits=5, shuffle=True, random_state=42)
for i,(tr,te) in enumerate(kf.split(X)):
    print(f'fold {i}: train={tr[:5]}...{tr[-3:]}  test={te}')
📤 実行例(実測) fold 0: train=[2 3 4 5 6]...[16 18 19] test=[ 0 1 15 17] fold 1: train=[0 1 2 4 6]...[17 18 19] test=[ 3 5 8 11] fold 2: train=[0 1 3 4 5]...[15 17 19] test=[ 2 13 16 18] fold 3: train=[0 1 2 3 5]...[16 17 18] test=[ 4 9 12 19] fold 4: train=[0 1 2 3 4]...[17 18 19] test=[ 6 7 10 14]

StratifiedKFold

1
2
3
4
5
6
from sklearn.model_selection import StratifiedKFold
y = np.array([0]*15 + [1]*5)  # 不均衡 (3:1)
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
for i,(tr,te) in enumerate(skf.split(np.zeros(20), y)):
    print(f'fold {i}: train pos={y[tr].sum()}/15  test pos={y[te].sum()}/{len(te)}')
# 各fold で陽性比率 (~25%) が保たれる
📤 実行例(実測) fold 0: train pos=4/15 test pos=1/4 fold 1: train pos=4/15 test pos=1/4 fold 2: train pos=4/15 test pos=1/4 fold 3: train pos=4/15 test pos=1/4 fold 4: train pos=4/15 test pos=1/4

GroupKFold

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from sklearn.model_selection import GroupKFold
# 都道府県を「地域」グループでまとめる
# 47 都道府県それぞれに 8 地方区分を割り当てる
# (3 種類しか無いと n_splits=4 の分割ができない)
regions = np.array(
    ['北海道']*1 + ['東北']*6 + ['関東']*7 + ['中部']*9
    + ['近畿']*7 + ['中国']*5 + ['四国']*4 + ['九州・沖縄']*8)
gkf = GroupKFold(n_splits=4)
for i,(tr,te) in enumerate(gkf.split(np.zeros(47), groups=regions)):
    print(f'fold {i}: train regions = {set(regions[tr])}  test regions = {set(regions[te])}')
# 同じ地域が train と test に分散しない
📤 実行例(実測) fold 0: train regions = {np.str_('四国'), np.str_('近畿'), np.str_('中国'), np.str_('関東'), np.str_('東北'), np.str_('九州・沖縄')} test regions = {np.str_('中部'), np.str_('北海道')} fold 1: train regions = {np.str_('近畿'), np.str_('中部'), np.str_('関東'), np.str_('中国'), np.str_('東北'), np.str_('北海道')} test regions = {np.str_('九州・沖縄'), np.str_('四国')} fold 2: train regions = {np.str_('四国'), np.str_('近畿'), np.str_('中国'), np.str_('中部'), np.str_('北海道'), np.str_('九州・沖縄')} test regions = {np.str_('関東'), np.str_('東北')} fold 3: train regions = {np.str_('四国'), np.str_('中部'), np.str_('関東'), np.str_('東北'), np.str_('北海道'), np.str_('九州・沖縄')} test regions = {np.str_('近畿'), np.str_('中国')}

TimeSeriesSplit

1
2
3
4
5
from sklearn.model_selection import TimeSeriesSplit
tscv = TimeSeriesSplit(n_splits=5)
for i,(tr,te) in enumerate(tscv.split(np.zeros(60))):
    print(f'fold {i}: train [{tr.min():2d},{tr.max():2d}]  test [{te.min():2d},{te.max():2d}]')
# 過去で学習→未来で検証 を順次拡大
📤 実行例(実測) fold 0: train [ 0, 9] test [10,19] fold 1: train [ 0,19] test [20,29] fold 2: train [ 0,29] test [30,39] fold 3: train [ 0,39] test [40,49] fold 4: train [ 0,49] test [50,59]

RepeatedKFold

1
2
3
4
5
from sklearn.model_selection import RepeatedKFold
rkf = RepeatedKFold(n_splits=5, n_repeats=3, random_state=42)
scores = cross_val_score(pipe, X, y, cv=rkf, scoring='r2')
print(f'R² over {len(scores)} folds = {scores.mean():.3f} ± {scores.std():.3f}')
# 5×3=15 fold での平均(分散が下がる)
📤 実行例(実測) R² over 15 folds = 0.266 ± 0.290

🔍 CV + ハイパーパラメータ探索

「最良のハイパーパラメータ + そのモデルの汎化性能」を同時に出すには Nested CV が原則です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from sklearn.model_selection import GridSearchCV, cross_val_score, KFold
from sklearn.ensemble import RandomForestRegressor

inner_cv = KFold(n_splits=3, shuffle=True, random_state=0)
outer_cv = KFold(n_splits=5, shuffle=True, random_state=42)

param_grid = {'n_estimators':[100,300,500], 'max_depth':[3,5,10,None]}
search = GridSearchCV(RandomForestRegressor(random_state=42),
                      param_grid=param_grid, cv=inner_cv, scoring='r2', n_jobs=-1)

# 外側 CV: 各 outer fold で内側にハイパー探索
nested = cross_val_score(search, X, y, cv=outer_cv, scoring='r2')
print(f'Nested R² = {nested.mean():.3f} ± {nested.std():.3f}')

# 最終モデル: 全データで再探索
search.fit(X, y)
print('best params:', search.best_params_)
📤 実行例(実測) Nested R² = 0.686 ± 0.405 best params: {'max_depth': 3, 'n_estimators': 100}

5 × 3 × 12 (param 組合せ) = 180 回学習。 SSDSE-B (n=47) なら数秒で終わる。 大規模データなら RandomizedSearchCV や Optuna へ。

🎭 CV と Stacking(メタ学習)

CV の出力 (OOF 予測) を集めて、 もう一段別のモデルを乗せるのが Stacking。 Kaggle の上位解法の常套手段。

📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行) 都道府県 A1101(総人口) A1303(65歳以上人口) A4101(出生数) L3221(消費支出(二人以上の世帯)) 北海道 5,092,000 1,681,000 24,430 296,888 東京都 14,086,000 3,205,000 86,348 341,320 沖縄県 1,468,000 350,000 12,549 251,222 …(全 47 行)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from sklearn.ensemble import StackingRegressor
from sklearn.linear_model import Ridge
from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor

estimators = [
    ('ridge', Ridge(alpha=1.0)),
    ('rf', RandomForestRegressor(n_estimators=200, random_state=42)),
    ('gbm', GradientBoostingRegressor(n_estimators=200, random_state=42)),
]
stack = StackingRegressor(
    estimators=estimators,
    final_estimator=Ridge(),
    cv=5,                # 内部で 5-fold CV → OOF 予測 → メタモデルへ
)
# ── この抜粋で使うデータと分割を用意します ──
# outer_cv を GroupKFold(群=都道府県)のまま使うと 1 群 1 標本になり、
# R² が負になってしまう。ここは通常の KFold で評価する。
import pandas as pd
from sklearn.model_selection import cross_val_score, KFold

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
df = df[df['SSDSE-B-2026'] == 2023].copy()
X = df[['A1101', 'A1303', 'L3221']].to_numpy(dtype=float)
y = df['A4101'].to_numpy(dtype=float)     # 出生数を予測する

scores = cross_val_score(stack, X, y,
                         cv=KFold(n_splits=5, shuffle=True, random_state=42),
                         scoring='r2')
print(f'Stacking R² = {scores.mean():.3f} ± {scores.std():.3f}')
📤 実行例(実測) Stacking R² = 0.971 ± 0.029

🧮 実値で計算してみる

47都道府県データを 5-fold で分割すると:

1回だけ test=北海道〜青森とランダムに引いて R²=0.85 が出るより、 はるかに信頼性が高い数値です。

🧮 数式に値を入れて手で計算する

合成 4-fold エラー値 $\mathrm{Error}=[0.20, 0.15, 0.30, 0.25]$ を k-fold CV の数式に代入し、 Step 1〜3 で手計算する。 同じ計算を Python (numpy / sklearn) で再現し、 結果が一致することを確認する。

📐 k-fold 交差検証の平均エラー (再掲)

$$ \mathrm{CV}_k = \frac{1}{k} \sum_{i=1}^{k} \mathrm{Error}_i $$

k は fold 数、 $\mathrm{Error}_i$ は i 番目の fold をテストに用いたときの誤差 (RMSE, MSE, 1-R² など)。

Step 1: データ準備 (合成 4-fold、 8 サンプル分割)

foldテストサンプル (index)訓練サンプル数Error_i
1{1, 2}60.20
2{3, 4}60.15
3{5, 6}60.30
4{7, 8}60.25

Step 2: 総和と除算

項目計算結果
$\sum_{i=1}^{4} \mathrm{Error}_i$$0.20 + 0.15 + 0.30 + 0.25$0.90
$k$4

Step 3: CV 平均と標準偏差

項目計算結果
$\mathrm{CV}_4$ 平均$0.90 / 4$0.2250
標準偏差 (ddof=0)$\sqrt{((-0.025)^2+(-0.075)^2+(0.075)^2+(0.025)^2)/4}$0.0559
報告値Error ≈ 0.225 ± 0.056

🐍 同じ計算を Python で再現

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import numpy as np
from sklearn.model_selection import KFold
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error

# Step 1: 合成 4-fold のエラー値 (各 fold の Error_i)
errors = np.array([0.20, 0.15, 0.30, 0.25])

# Step 2 & 3: k-fold CV の平均と標準偏差
k = len(errors)
cv_mean = errors.sum() / k       # = np.mean(errors)
cv_std = errors.std(ddof=0)      # 母集団標準偏差

print(f"k       = {k}")
print(f"sum     = {errors.sum():.4f}")
print(f"CV 平均 = {cv_mean:.4f}")
print(f"CV 標準偏差 = {cv_std:.4f}")
print(f"報告値 = Error = {cv_mean:.3f} ± {cv_std:.3f}")

# 参考: KFold が同じ平均化を内部でやっていることの確認
# (実際の学習を sklearn 流に組むときの最小スケルトン)
X = np.arange(8).reshape(-1, 1).astype(float)
y = np.array([1.0, 2.1, 2.9, 4.2, 5.0, 5.9, 7.1, 8.0])
kf = KFold(n_splits=4)
fold_errs = []
for tr, te in kf.split(X):
    m = LinearRegression().fit(X[tr], y[tr])
    fold_errs.append(mean_squared_error(y[te], m.predict(X[te])))
print(f"sklearn 例 fold MSE = {np.round(fold_errs, 4).tolist()}")
print(f"sklearn 例 CV 平均  = {np.mean(fold_errs):.4f}")

📤 実行結果

k = 4 sum = 0.9000 CV 平均 = 0.2250 CV 標準偏差 = 0.0559 報告値 = Error = 0.225 ± 0.056 sklearn 例 fold MSE = [0.0044, 0.0239, 0.0138, 0.01] sklearn 例 CV 平均 = 0.0130

💬 手計算 (Step 3 の 0.225) と Python の numpy 平均 (0.2250) が完全一致。 標準偏差 0.056 は fold 間のばらつきが小さいことを意味し、 「特定の fold で運よく良い結果が出ただけ」ではないと言える。 sklearn 例はあくまでパイプラインの確認用で、 k-fold CV は「fold ごとのエラーを平均する」という単純な数式に帰着することがポイント。

🧮 SSDSE-B-2026 で 5-fold CV を実行する(用語固有計算)

🎯 このブロックの狙い
47 都道府県データ(SSDSE-B-2026, 2023年)に対し「出生数」を「総人口」と「高齢化率」から予測する線形回帰モデルを構築し、 5-fold CV で汎化性能 RMSE を推定する。 単一の train/test 分割と比べてどれくらい安定するかを体感する。
📥 入力
data/raw/SSDSE-B-2026.csv から 2023年 47 行 × 3 変数(説明変数2 + 目的変数1)を抽出した DataFrame。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import pandas as pd
import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import KFold, cross_val_score
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import make_pipeline

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
df = df[df['SSDSE-B-2026']==2023].dropna(subset=['A4101','A1101','A1303']).reset_index(drop=True)
df['高齢化率'] = df['A1303'] / df['A1101'] * 100

X = df[['A1101','高齢化率']].values
y = df['A4101'].values

# Pipeline: 標準化 + 線形回帰(各fold内でfitされる→リーク防止)
pipe = make_pipeline(StandardScaler(), LinearRegression())

# 5-fold CV
kf = KFold(n_splits=5, shuffle=True, random_state=42)
neg_mse = cross_val_score(pipe, X, y, cv=kf, scoring='neg_mean_squared_error')
rmse = np.sqrt(-neg_mse)
print('Fold RMSE  :', np.round(rmse, 2))
print(f'平均 RMSE = {rmse.mean():.2f} ± {rmse.std():.2f}')
📤 出力(期待値)
各 fold で RMSE はおおむね 1,200〜3,400 人のオーダー(東京都が外れ値なので fold ごとにばらつく)。 平均 ± 標準偏差 で「RMSE ≈ 1,776 ± 553 人」のような結果に。 単一 train/test 分割では、 外れ値県がテスト側に来た分割で 3,000 人超まで悪化することもあり、 5 個の平均で安定する。
💬 解説
make_pipeline を使うことで、 StandardScaler の fit も各 fold 内で行われる(=検証セットの平均/分散が学習に漏れない)。 これがリーク防止の基本パターン。 47 県程度の小データでは LOOCV (k=47) を選ぶ手もある。

🏭 産業事例 6 件 — 交差検証が現場でどう使われるか

業界CV の役割代表手法 / k
製薬・医薬開発化合物の薬効予測モデル。 サンプルが希少なため LOOCV や bootstrap CV を併用。 FDA 承認申請の根拠資料に含まれる。LOOCV / k=10
金融(信用スコア)デフォルト確率モデル。 時系列性があるため Walk-Forward CV (時間順)。 規制当局への報告で必須。Time-Series CV
EC・推薦システムクリック率予測。 ユーザー単位で分割 (Group K-Fold)。 オフライン評価 → A/B テスト の二段階。Group K-Fold
製造業(品質管理)不良品検出。 不良品が少数のため Stratified K-Fold で陽性率を保つ。 ROC-AUC の信頼区間が重要。Stratified k=5
医療画像CNN による腫瘍検出。 患者単位で分割 (Patient-Wise CV)。 論文掲載基準。Group K-Fold
公的統計(本コンペ)47都道府県データで回帰モデル評価。 サンプル少 → LOOCV または k=5。LOOCV / k=5

📊 比較表 — CV 手法の選び方

手法分割方式計算コスト推定の分散推奨場面
Hold-outtrain 70 / test 30 を1回最小大規模データ・速度重視
k-Fold CVk 個に等分k 倍標準(k=5 or 10)
Stratified k-Foldクラス比率を保って k 分割k 倍クラス不均衡分類
LOOCV1点だけ検証, n 個n 倍小(バイアスは小・分散は高い)小データ (n<100)
Group K-Foldグループ単位で分割k 倍ユーザー・患者単位
Time-Series CV時間順、 過去→未来k 倍時系列予測(必須)
Repeated k-Foldk-Fold を r 回繰り返すk×r 倍最小論文・正式報告
Nested CV外側=評価, 内側=ハイパー調整k² 倍最小モデル比較の厳密評価

✏️ 演習 5 問(SSDSE-B-2026 で)

  1. 演習1: 2023年 47都道府県を 5-fold CV で線形回帰(目的:出生数、 説明:総人口・高齢化率)。 RMSE の平均と標準偏差を報告せよ。
  2. 演習2: 同じデータを LOOCV (k=47) に変えると RMSE の平均・分散はどう変わるか比較せよ。
  3. 演習3: shuffle=Falseshuffle=True で結果が変わる理由を、 SSDSE のデータが「都道府県コード順に並んでいる」事実から説明せよ。
  4. 演習4: 5-fold CV を 30 回繰り返して RMSE の分布をヒストグラム化し、 単一 5-fold の結果がどれくらい運に依存するかを示せ。
  5. 演習5: 説明変数を増やして k-NN 回帰モデルを CV で評価し、 線形回帰と性能を比較せよ(同じ fold 分割で)。

💀 失敗例ケーススタディ — 交差検証の事故

事例1: 全データで標準化してから CV → リーク

「先に StandardScaler でデータ全体を標準化してから cross_val_score」とすると、 検証セットの平均・分散が学習に漏れる。 結果として CV スコアが楽観的(実運用より良い数値)になる。 対策: 必ず Pipeline で前処理を内側に入れる。

事例2: 時系列データに通常 k-Fold

株価や売上の時系列を通常 k-Fold で評価すると、 未来データで学習し過去データで検証することになり、 現実とは逆。 オフラインで高 R² でも本番で全く動かない。 対策: TimeSeriesSplit を使う。

事例3: 患者単位の漏れ(医療データ)

同じ患者の異なる検査が train と test に分散すると「同じ人」を覚えてしまい AUC が異常に高い。 対策: GroupKFold(group=patient_id) で患者単位分割。

事例4: ハイパーパラメータ調整したスコアを最終評価に流用

同じ CV ループ内でハイパーパラメータをグリッド探索し、 そのベストスコアを「汎化性能」として報告する → 楽観バイアス。 対策: Nested CV(外側 fold で評価、 内側 fold で調整)。

事例5: k が小さすぎて分散が高すぎ結論不能

k=2 など極端に小さい k だと fold ごとの差が大きく、 「モデルAがB より良い」と言えるか判断できない。 対策: k=5 か 10 を基本に、 さらに Repeated CV で分散を下げる。

📖 用語ミニ辞典(10語)

Hold-out
データを train/test に 1 回だけ分ける最も単純な方法。 推定の分散が高い。
k-Fold
k 個の互いに素な fold に分け、 各 fold を順に検証用にする CV の標準形。
LOOCV
Leave-One-Out CV。 k=n の極端版。 小データで使う。
Stratified
クラス比率を各 fold で保つ層化分割。 不均衡分類で必須。
Group K-Fold
同一グループ(患者・ユーザー)が train/test に分散しないよう分割。
Time-Series CV
時間順に学習窓を拡大しながら検証する。 未来は学習に使わない。
Repeated CV
k-Fold を異なる seed で複数回繰り返して分散を下げる。
Nested CV
外側で評価、 内側でハイパー調整。 厳密だが計算量大。
Pipeline
前処理 + モデルを1つのオブジェクトにまとめ、 リークを防ぐ scikit-learn の仕組み。
Data Leakage
検証セットの情報が学習に漏れること。 CV 設計の最大の敵。

🕰 歴史的背景 — Stone (1974) から AutoML まで

交差検証の概念は、 Stone (1974) と Geisser (1975) によって統計学的に整備されました。 それ以前にも「データを分割して検証する」という発想はありましたが、 数学的な性質(不偏性、 一致性)が証明されたのはこの時期です。

機械学習の文脈では Kohavi (1995) の論文 "A Study of Cross-Validation and Bootstrap for Accuracy Estimation and Model Selection" が決定版で、 「k=10 が経験的に良い」という現代の慣習はここに由来します。 2000年代以降、 Kaggle 等のコンペで CV 設計が直接スコアに響くようになり、 「leaky CV」「shake-up」などの実務知見が蓄積。 2010年代後半からは AutoML が CV を組み込みで実行するようになり、 ユーザーが手書きする頻度は減りましたが、 結果を読み取るには CV の理論理解が依然必須です。

🔧 高度なレシピ — Pipeline・Nested CV・予測の保存

レシピ1: Pipeline でリーク防止

1
2
3
4
5
6
7
8
9
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import Ridge
from sklearn.model_selection import cross_val_score, KFold

pipe = Pipeline([('scaler', StandardScaler()), ('reg', Ridge(alpha=1.0))])
cv = KFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(pipe, X, y, cv=cv, scoring='r2')
print(f'R² = {scores.mean():.3f} ± {scores.std():.3f}')
📤 実行例(実測) R² = 0.984 ± 0.006

レシピ2: GridSearchCV と Nested CV

1
2
3
4
5
6
7
from sklearn.model_selection import GridSearchCV, cross_val_score, KFold

inner = KFold(n_splits=3, shuffle=True, random_state=0)
outer = KFold(n_splits=5, shuffle=True, random_state=42)
grid = GridSearchCV(pipe, param_grid={'reg__alpha':[0.1,1,10,100]}, cv=inner, scoring='r2')
nested_scores = cross_val_score(grid, X, y, cv=outer, scoring='r2')
print(f'Nested CV R² = {nested_scores.mean():.3f} ± {nested_scores.std():.3f}')
📤 実行例(実測) Nested CV R² = 0.983 ± 0.011

レシピ3: 予測値を保存して fold ごとに解析

1
2
3
4
5
6
7
8
from sklearn.model_selection import cross_val_predict
y_pred = cross_val_predict(pipe, X, y, cv=cv)
# 47県の予測値が得られる(OOF prediction)
import pandas as pd
result = pd.DataFrame({'都道府県':df['Prefecture'], 'y_true':y, 'y_pred':y_pred})
result['residual'] = result['y_true'] - result['y_pred']
print(result.sort_values('residual').head())   # 最も過小予測の県
print(result.sort_values('residual').tail())   # 最も過大予測の県(東京など)
📤 実行例(実測) 都道府県 y_true y_pred residual 0 北海道 24430 28833.095152 -4403.095152 3 宮城県 12328 14703.633980 -2375.633980 8 栃木県 9958 12183.043037 -2225.043037 7 茨城県 14898 16875.379282 -1977.379282 21 静岡県 18969 20885.159188 -1916.159188 都道府県 y_true y_pred residual 27 兵庫県 32615 30211.813134 2403.186866 22 愛知県 48402 44872.589327 3529.410673 39 福岡県 33942 29555.716672 4386.283328 26 大阪府 55292 49188.061136 6103.938864 12 東京都 86348 79601.750211 6746.249789

レシピ4: TimeSeriesSplit(時系列用)

📥 入力例(SSDSE-B-2026 全体:564 行 × 112 列 = 47 都道府県 × 2012〜2023 年) 年度 地域コード 都道府県 A1101(総人口) A1303(65歳以上人口) A4101(出生数) … 2023 R01000 北海道 5,092,000 1,681,000 24,430 … 2023 R13000 東京都 14,086,000 3,205,000 86,348 … 2023 R47000 沖縄県 1,468,000 350,000 12,549 … …(残り 112 列は住宅・家計・教育・医療など)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import pandas as pd
from sklearn.model_selection import TimeSeriesSplit

# ── この抜粋だけで動くように、時間順に並んだパネルデータを用意する ──
panel_data = (pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
                .sort_values(['SSDSE-B-2026', 'Prefecture'])
                .reset_index(drop=True))

tscv = TimeSeriesSplit(n_splits=5)
for train_idx, test_idx in tscv.split(panel_data):
    print(f'train: {train_idx.min()}-{train_idx.max()}, test: {test_idx.min()}-{test_idx.max()}')
# 時系列では「過去で学習 → 未来で検証」の順序を必ず守る
📤 実行例(実測) train: 0-93, test: 94-187 train: 0-187, test: 188-281 train: 0-281, test: 282-375 train: 0-375, test: 376-469 train: 0-469, test: 470-563

レシピ5: 評価指標を複数同時に

1
2
3
4
5
6
from sklearn.model_selection import cross_validate
scoring = ['neg_root_mean_squared_error','neg_mean_absolute_error','r2']
res = cross_validate(pipe, X, y, cv=cv, scoring=scoring, return_train_score=True)
for k,v in res.items():
    if k.startswith('test_') or k.startswith('train_'):
        print(f'{k:32s}: {v.mean():.3f} ± {v.std():.3f}')
📤 実行例(実測) test_neg_root_mean_squared_error: -1830.651 ± 803.078 train_neg_root_mean_squared_error: -1506.933 ± 112.941 test_neg_mean_absolute_error : -1394.125 ± 560.247 train_neg_mean_absolute_error : -1146.754 ± 107.106 test_r2 : 0.984 ± 0.006 train_r2 : 0.992 ± 0.002

🚫 アンチパターン集

  1. StandardScaler を CV の外側で fit → 検証セットの情報漏れ。 必ず Pipeline 内で。
  2. SMOTE などのオーバーサンプリングを外側で実行 → 同じく漏れ。 imbalanced-learn の Pipeline を使う。
  3. 時系列を shuffle=True で k-Fold → 未来から学習する非現実シナリオ。 TimeSeriesSplit へ。
  4. 同じユーザー/患者を train/test に分散 → 個人認識の暗黙学習。 GroupKFold へ。
  5. k=2 や k=3 でモデル比較 → 分散が大きすぎて差を検出できない。 k=10 + Repeated。
  6. 同一の CV スコアでハイパー選択とモデル評価 → 楽観バイアス。 Nested CV へ。
  7. 外れ値除去を CV の外で実行 → どの fold で外れ値が外れたか不明。 内側で。
  8. 特徴量選択を CV の外で実行 → 同じく漏れ。 SelectKBest も Pipeline 内へ。

❓ FAQ — 受講生からの頻出質問

Q1. k はいくつが良い?
A. 一般的には k=5 か k=10。 Kohavi (1995) が経験的に推奨。 小データ (n<100) なら LOOCV、 大データなら k=5 で計算節約。
Q2. CV スコアが本番性能と乖離する
A. リーク疑い。 (1) 前処理が CV 外、 (2) グループ性無視、 (3) 時系列性無視、 のどれかをまず疑う。
Q3. shuffle=True と False で結果が変わる
A. データが何らかの順序を持っている(時系列、 ID 順)と False では偏った分割になる。 通常は True を推奨、 時系列なら False のまま TimeSeriesSplit。
Q4. CV の信頼区間はどう出す?
A. k 個のスコアから $\bar{x} \pm t_{k-1, 0.975} \cdot s/\sqrt{k}$ で 95% CI。 ただし fold 間は独立でないので近似値。 厳密には Bootstrap CV や Bengio-Grandvalet (2004) を参考に。
Q5. LOOCV は実際に有効?
A. バイアスは小さい(学習データほぼ全部使う)が分散は高い。 小データ (n<50) で k-Fold の fold サイズが極端に小さくなる時に検討。 SSDSE-B (n=47) なら LOOCV の選択肢あり。

🐍 Python 実装

🎯 このコードでやること:SSDSE-B-2026 の都道府県データ(X = 総人口・65歳以上人口・年平均気温など、 y = 出生数)に対して sklearn.model_selection.KFold で 5-fold 交差検証を行い、 Ridge 回帰の R² の平均と分散を取得する。 47 都道府県を 5 分割し、 各 fold で「学習 38 県 → 評価 9 県」を入れ替えて合計 5 回モデルを学習・評価する。

📥 入力データ(SSDSE-B-2026 抜粋)

SSDSE-B-2026 Prefecture A1101 A1303 B4101 A4101 2023 北海道 5092000 1681000 11.0 24430 2023 青森県 1184000 417000 12.6 5696 2023 岩手県 1163000 407000 12.5 5432 ...(47 都道府県, A4101=出生数 が目的変数)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# このコードでやること: SSDSE-B-2026 を読み込んで KFold で 5-fold CV → R² 平均±SD と fold 別バーチャート
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import cross_val_score, KFold
from sklearn.linear_model import Ridge
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline

# 1) SSDSE-B-2026 を読み込む (1 行目=列コード、 2 行目=日本語見出しを読み飛ばす)
df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
df = df[df['SSDSE-B-2026'] == 2023].copy()                       # 47 県 × 1 年に絞る
features = ['A1101', 'A1303', 'B4101']
X = df[features].to_numpy()
y = df['A4101'].to_numpy()
print('X shape', X.shape, 'y shape', y.shape)

# 2) Pipeline で fold 内に標準化を閉じ込めてリーケージ防止
pipe = Pipeline([('scaler', StandardScaler()), ('ridge', Ridge(alpha=1.0))])
kf = KFold(n_splits=5, shuffle=True, random_state=0)
scores = cross_val_score(pipe, X, y, cv=kf, scoring='r2')

# 3) fold 別 R² と平均±SD を出力
print(f"R² = {scores.mean():.3f} ± {scores.std():.3f}")
for i, s in enumerate(scores, 1):
    print(f"  fold {i}: R² = {s:.3f}")

# 4) fold 別 R² を可視化 (平均線で過大評価リスクを直感化)
plt.bar(range(1, 6), scores)
plt.axhline(scores.mean(), color='red', linestyle='--', label='mean')
plt.xlabel('fold'); plt.ylabel('R²'); plt.legend(); plt.savefig('cv_r2.png')

📤 実行すると次の出力が得られる

X shape (47, 3) y shape (47,) R² = 0.970 ± 0.042 fold 1: R² = 0.988 fold 2: R² = 0.995 fold 3: R² = 0.995 fold 4: R² = 0.986 fold 5: R² = 0.887 (cv_r2.png に fold 別 R² の棒グラフ + 平均線が保存される)

💬 結果の読み方:5 つの fold の R² は 0.887〜0.995 と幅があり、 平均 0.970 ± 標準偏差 0.042。 → fold 5(R²=0.887)では「東京・大阪」のような外れ値県を含む可能性が高く、 そこだけ予測精度が落ちている。 単一の train/test 分割(例:8:2)で評価すると 偶然 fold 2 を test に選んで R²=0.995 と過大評価 していたかもしれない。 KFold で fold 間ばらつきを見ることで「モデル性能の信頼区間」を把握できる。 ばらつきが大きい場合は StratifiedKFold(クラス層別)や GroupKFold(同一グループの漏れ防止)の検討対象。

⚠️ よくある落とし穴

❌ 1. ハイパラ調整と評価を同じ CV で実施
症状: GridSearchCV の best_score を「本番性能」と報告し、 デプロイ後に精度が大きく落ちる。 原因: 同じ fold で「最良ハイパラ選び」と「汎化評価」を両方やったため、 fold 適合度を過大評価している。 回避: cross_val_score(GridSearchCV(...), X, y, cv=outer) の Nested CV にし、 外側で汎化、 内側で調整に分業する。
❌ 2. 時系列で shuffle=True して CV
症状: SSDSE-B-2026 を 2013-2023 縦に積んで KFold(shuffle=True) → R²=0.95 だが 2024 年に投入したら 0.4。 原因: 2023 年データを学習し 2018 年データを当てる「未来→過去予測」が混入している。 回避: TimeSeriesSplit または年度 hold-out (train: 2013-2020, test: 2021-2023) に切り替える。
❌ 3. クラス不均衡で Stratified にしない
症状: 47 県のうち少数派 (例: 過疎県 3 件) が特定 fold に 0 件入り、 「ValueError: y_true has only one class」で落ちる。 原因: 通常 KFold が y の分布を無視してランダム分割した。 回避: StratifiedKFold(n_splits=5) に変え、 さらに少数派なら StratifiedGroupKFold で集団リークも防ぐ。
❌ 4. 前処理を CV の外で実施
症状: 全 47 県を `StandardScaler().fit_transform(X)` → KFold で評価。 CV R²=0.92 でも本番 0.7 と乖離。 原因: 全体平均・分散が各 fold の評価セット情報を持ち込み、 リーケージが発生している。 回避: Pipeline([scaler, model]) を `cross_val_score` に渡し、 fold 内で fit を強制する。
❌ 5. k=2 のような少なすぎる分割
症状: 47 県を半分に割って評価したら、 同じ手法で seed を変えるたび R² が 0.5〜0.85 と大きくぶれる。 原因: fold 1 個あたりの評価データが 23 件しかなく分散が大きい。 回避: k=5 か k=10 を標準とし、 N<100 では RepeatedKFold(n_splits=5, n_repeats=10) で平均化する。
❌ 6. グループ構造を無視 (パネルデータ)
症状: SSDSE-B-2026 を 47 県 × 6 年 = 282 行で学習、 同じ県の 2018 年と 2023 年が train/test に分かれ R²=0.98 だが新県では 0.3。 原因: 県内の時系列相関で「同じ県の別年」を当てる楽勝問題になっていた。 回避: GroupKFold(groups=df['都道府県']) で県単位に分割し、 真の汎化を測る。

⚠️ 実務でハマる罠 — 詳細版

罠1: ターゲットエンコーディングのリーク

カテゴリ変数を「そのカテゴリの目的変数平均」で置き換える Target Encoding は、 CV の外で実行すると検証セットの y が学習にリーク。 対策: category_encodersTargetEncoder を Pipeline 内で使う。

罠2: 欠損値補完のリーク

中央値補完を CV 外で行うと、 検証セットの中央値情報も学習に含まれる。 対策: SimpleImputer を Pipeline 内に。

罠3: 特徴量選択のリーク

相関の高い列を CV 外で選んで → CV で評価すると、 「選別段階で y を見てしまった」分のリーク。 対策: SelectKBest を Pipeline 内に。

罠4: SMOTE のリーク

不均衡データで SMOTE オーバーサンプリングを CV 外で行うと、 合成サンプルが train/test に分散。 対策: imbalanced-learnPipeline を使い、 SMOTE を内側へ。

罠5: 時間順を無視した shuffle=True

株価予測などで shuffle=True にすると、 「未来から学んで過去を当てる」非現実シナリオ。 対策: TimeSeriesSplit

📜 論文で CV を報告するときの書き方

これを守らない論文はリジェクトされる確率が高い(特に NeurIPS, ICML, KDD)。 公的統計分析でも同じ厳密性を期待される時代です。

📌 拡張チートシート

📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行) 都道府県 A1101(総人口) A1301(15歳未満人口) A1303(65歳以上人口) A4101(出生数) A9101(婚姻件数) 北海道 5,092,000 514,000 1,681,000 24,430 17,281 東京都 14,086,000 1,513,000 3,205,000 86,348 71,774 沖縄県 1,468,000 236,000 350,000 12,549 6,316 …(全 47 行)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import pandas as pd
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import Ridge

# pipe / X / y / params をここで用意する
_d = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
_d = _d[_d['SSDSE-B-2026'] == 2023].reset_index(drop=True)
X = _d[['A1101', 'A1301', 'A1303', 'A9101']].astype(float).values
y = _d['A4101'].astype(float).values
pipe = Pipeline([('imp', SimpleImputer(strategy='median')),
                 ('sc', StandardScaler()), ('m', Ridge(alpha=1.0))])
params = {'m__alpha': [0.1, 1.0, 10.0]}   # 上の Pipeline の step 名 'm' に合わせる

# 基本パターン
from sklearn.model_selection import cross_val_score, KFold
cv = KFold(5, shuffle=True, random_state=42)
scores = cross_val_score(pipe, X, y, cv=cv, scoring='r2')

# 複数指標
from sklearn.model_selection import cross_validate
res = cross_validate(pipe, X, y, cv=cv,
                     scoring=['r2','neg_root_mean_squared_error'])

# OOF 予測
from sklearn.model_selection import cross_val_predict
y_oof = cross_val_predict(pipe, X, y, cv=cv)

# ハイパー探索付き
from sklearn.model_selection import GridSearchCV
gs = GridSearchCV(pipe, params, cv=cv, scoring='r2')   # model ではなく pipe を渡す
gs.fit(X, y); print(gs.best_params_, gs.best_score_)

# 並列実行
scores = cross_val_score(pipe, X, y, cv=cv, n_jobs=-1)

# Pipeline で前処理込み
from sklearn.pipeline import Pipeline
pipe = Pipeline([('imputer', SimpleImputer()),
                 ('scaler', StandardScaler()),
                 ('model', Ridge())])
📤 実行例(実測) {'m__alpha': 0.1} 0.9965310253271191

🎓 学習ロードマップ — CV を完全に使いこなす

  1. Week 1: train_test_split から KFold へ移行。 cross_val_score を使えるように。
  2. Week 2: Pipeline でリーク防止を体得。 StandardScaler を内側に。
  3. Week 3: StratifiedKFold, GroupKFold, TimeSeriesSplit を場面で使い分け。
  4. Week 4: GridSearchCV と Nested CV でハイパー調整。
  5. Week 5: cross_val_predict で OOF 予測を取り、 残差分析。
  6. Week 6: Stacking / Bagging で CV を組み込んだアンサンブル。
  7. Week 7: 学習曲線・検証曲線で診断を読み解く。
  8. Week 8: Repeated CV や Bootstrap CV で正式報告レベル。

交差検証は「保険」ではなく「設計」です。 SSDSE-B-2026 のような小サンプルデータでこそ、 CV を正しく使いこなせるかが分析の信頼性を左右します。

🌍 実例ケーススタディ — 47都道府県の予測精度を年度横断で確かめる

「2013-2020年で学習 → 2021-2023年で検証」のような時間的 hold-out を、 SSDSE-B-2026 の多年データで実施します。 これは公的統計の実務に近い評価方法です。

📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行) 都道府県 A1101(総人口) A1303(65歳以上人口) A4101(出生数) B4101(年平均気温) L3221(消費支出(二人以上の世帯)) 北海道 5,092,000 1,681,000 24,430 11.0 296,888 東京都 14,086,000 3,205,000 86,348 17.6 341,320 沖縄県 1,468,000 350,000 12,549 23.8 251,222 …(全 47 行)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import pandas as pd, numpy as np
from sklearn.linear_model import Ridge
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.metrics import mean_absolute_error, r2_score

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]).dropna()
df['高齢化率'] = df['A1303']/df['A1101']*100
features = ['A1101','高齢化率','B4101','L3221']
target = 'A4101'

# 時間的 hold-out (パネル化)
train = df[df['SSDSE-B-2026'].between(2013, 2020)]
test  = df[df['SSDSE-B-2026'].between(2021, 2023)]

pipe = Pipeline([('s', StandardScaler()), ('m', Ridge(alpha=1.0))])
pipe.fit(train[features], train[target])
pred = pipe.predict(test[features])
print(f'時間的検証 MAE = {mean_absolute_error(test[target], pred):.2f}')
print(f'時間的検証 R²  = {r2_score(test[target], pred):.3f}')

# 比較: 通常 5-fold CV
from sklearn.model_selection import KFold, cross_val_score
cv = KFold(5, shuffle=True, random_state=42)
cv_r2 = cross_val_score(pipe, df[features], df[target], cv=cv, scoring='r2')
print(f'通常 5-fold R² = {cv_r2.mean():.3f} ± {cv_r2.std():.3f}')
📤 実行例(実測) 時間的検証 MAE = 3056.25 時間的検証 R² = 0.923 通常 5-fold R² = 0.979 ± 0.003

時間的 hold-out の方が「現実的な汎化性能」を反映するが、 fold 数が 1 しかなく分散がわからない。 両者を併報するのがベストプラクティス。

⚠️ 条件・限界・誤解回避 — 交差検証の運用ポイント

交差検証(CV)は機械学習・統計学習の汎化誤差推定における標準ツールだが、 データ構造・分割設計・指標選択を誤ると CV の結果が「本番性能の信頼できる推定」にならない。 SSDSE-B-2026(N=47)のような小サンプルでは特に CV の使い方が結果を大きく左右する。 ここでは適用条件・限界・誤解回避を整理する。

📐 適用条件(CV の結果を信頼できる前提)

  1. iid(独立同分布)の仮定: 通常の k-fold CV は観測が iid を仮定。 時系列・空間・パネルデータでは破れるため、 専用 CV(時系列 CV、 ブロック CV、 グループ k-fold)が必要。
  2. 標本サイズが分割数 k に対して十分: k-fold CV は各 fold に十分なデータがある前提。 N=47, k=5 なら各 fold 約 9 件で、 評価指標の分散が大きくなる。
  3. クラス分布の維持: 分類問題では各 fold のクラス分布が全体と近い必要があり、 stratified k-fold で確保する。 不均衡データでは特に重要。
  4. 前処理の漏れ防止: 標準化・特徴選択・ターゲットエンコーディングは「fold 内で fit、 fold 外に transform」する。 全データで fit すると leakage が起き CV 誤差が楽観バイアスを受ける。
  5. ハイパーパラメータ選択は nested CV で: ハイパーパラメータ調整と汎化誤差推定を同じ CV で兼ねると、 楽観バイアスが乗る。 外側 CV で誤差推定、 内側 CV で調整。

🚧 限界(CV でも分からないこと)

  1. 分布シフト下では信頼できない: 訓練分布と本番分布が違う(共変量シフト、 ラベルシフト)と、 CV 誤差は本番誤差を過小評価する。 本番に近い時期のデータでホールドアウト検証が必要。
  2. 小サンプルでの分散が大きい: N=47, k=5 では、 fold の選び方次第で CV 誤差が ±20% 程度ぶれることがある。 繰り返し CV(repeated k-fold)で安定化を図る。
  3. LOO は分散が大きく時間がかかる: Leave-One-Out は不偏に近いが分散が大きく、 N 回の学習が必要。 k=10〜20 の k-fold が実務的にはバランスがよい。
  4. モデル選択バイアス: 多数のモデルを CV で比較し、 最良のものだけ報告すると、 CV 誤差が楽観バイアスを受ける。 nested CV または事前登録で対処。
  5. 計算コスト: k×(モデル学習コスト)が必要。 深層学習や大規模 GBDT では現実的に k=3 や hold-out + early stopping に縮退することがある。

💡 誤解回避(CV 運用で頻発する勘違い)

  1. 「CV 誤差が低ければ本番でも低い」は誤解: 分布シフトがあれば CV 誤差は楽観的。 「CV → ホールドアウト → 本番」の 3 段検証が原則。
  2. 「k が大きいほど良い」も誤解: k が大きいと訓練サイズが本番に近づく利点はあるが、 fold 間の独立性が崩れて分散推定が不正確になる。 k=5〜10 が標準。
  3. 「Stratified k-fold は回帰では不要」は誤解: 回帰でも目的変数の分布が偏っていれば(例: 多くの 0 と少数の極端値)、 stratified-by-bin で分布を保つほうが安定する。
  4. 「時系列でも k-fold で OK」は最悪パターン: 時系列データに通常の k-fold を使うと、 未来の情報で過去を予測することになり、 完全な leakage。 必ず TimeSeriesSplit を使う。
  5. 「CV の標準偏差は無視してよい」も誤解: モデル比較では CV 誤差の平均だけでなく、 fold 間の標準偏差・最悪 fold も見る。 「平均は良いが分散が大」は本番でのリスク。

✅ 実務でのチェックリスト

💬 CV は「データを賢く使い回す」テクニックだが、 同時に「自分の楽観バイアスを増幅する道具」にもなる。 データ構造の確認、 leakage の遮断、 nested 構造、 ホールドアウト併用 — この 4 つを毎回チェックすることが、 CV 結果を信頼できる汎化誤差推定に保つ最短路。

🎮 触って理解する

スライダーを動かすと、 図と数値がその場で更新されます。 分割数 k を変えると各 fold の訓練()/検証()の割り当てがどう変わるか、 多項式の次数を変えると交差検証スコア(平均 R²)が最適次数で最大化し、 それを超えると過学習で落ちる様子を体感できます。

※ このデモの計算:説明用の擬似データ(真の関数 y = sin(2πx) にノイズを加えた 16 点、 乱数シード固定)に対し、 多項式回帰+k-fold CV を ブラウザ内で正確に計算しています。 実データ(SSDSE-B-2026)での分析は上の「🐍 Python 実装」を参照してください。

① k 分割の割り当て(16 データ点・シャッフル後)
② CV スコア vs 多項式の次数(過学習の検出)

💡 直感

k を増やすと各 fold の訓練データが増えて(=より本番に近いデータ量で学習でき)バイアスは下がりますが、 検証セットが小さくなり fold ごとのスコアのばらつき(標準偏差)は大きくなります。 k = n(= LOOCV)の極端では、 検証は毎回 1 点だけ。 このとき fold 単位の R² は「1 点では平均が定義できない」ため計算不能になり、 全 n 個の予測をまとめた プール R² で評価するのが実務の作法です(デモでも自動でそう切り替わります)。 一方 次数を上げると訓練 R²(青線)は 1 に張り付いていきますが、 CV R²(橙線)は最適次数でピークを打ち、 その先は過学習で急落します。 この「青は上がり続け・橙は途中で折り返す」ギャップこそ過学習のサインです。

⚠️ よくある落とし穴

🚀 発展: ネストCV

上のデモでは「次数を選ぶ」ことと「スコアを測る」ことを同じ CV で行っていますが、 実務でこれをやるとハイパラを検証セットに合わせ込んでしまい、 汎化性能を過大評価します。 これを避けるのが ネストCV(入れ子交差検証)で、 外側 CV で汎化性能を測り、 その各 fold の内部で内側 CV により次数などのハイパラを選びます。 「調整」と「評価」を別々のデータで行うことで、 デモの橙線ピークが持つ楽観バイアスを取り除けます。

関連ページ: 過学習バイアスバリアンス分解train-test 分割データリーケージハイパラ調整

🗺 概念マップ — 交差検証の位置づけ

                  ┌─────────────────────────┐
                  │  モデル選択の枠組み(Model Selection) │
                  └────────────┬────────────────┘
                               │
            ┌──────────────────┼──────────────────┐
            ▼                  ▼                  ▼
       ┌─────────┐        ┌─────────┐        ┌─────────┐
       │ AIC/BIC │        │ Hold-out│        │   CV    │
       │  情報量   │        │ (1回分割) │        │ (k回分割)│
       └─────────┘        └─────────┘        └────┬────┘
                                                   │
                  ┌────────────────┬───────────────┼────────────┐
                  ▼                ▼               ▼            ▼
            ┌─────────┐    ┌─────────┐   ┌─────────────┐ ┌────────┐
            │ k-Fold  │    │ LOOCV   │   │Stratified k │ │Group K │
            │ (k=5,10)│    │ (k=n)   │   │  (分類用)    │ │(個人別) │
            └─────────┘    └─────────┘   └─────────────┘ └────────┘
                                                              │
                                                              ▼
                                                       ┌──────────┐
                                                       │TimeSeries│
                                                       │  (時系列)  │
                                                       └──────────┘

   応用:
     - ハイパーパラメータ調整 ← GridSearchCV
     - モデル比較           ← Nested CV
     - 予測値の集約          ← cross_val_predict (OOF)
  
交差検証 KFold StratifiedKFol GroupKFold TimeSeriesSpli LeaveOneOut

🔗 隣接手法への橋渡し

交差検証は単純分割の上位、 ハイパラ調整の前処理として機能する。 本章では「接続 (どこから繋がるか)」「統合 (どう組み合わせるか)」「比較 (何と使い分けるか)」の 3 視点で隣接手法を整理する。

① 接続: CV を巡る上流・下流の関係

CV は train/test 分割の発展形であり、 ハイパラ調整・モデル選択・性能報告の中核をなす。

② 統合: CV を組み込んだ実務パイプライン

CV 単独より、 前処理・特徴量選択・ハイパラ探索とパイプライン化した時に真価を発揮する。 SSDSE-B 規模の中規模データでの定番ワークフロー 3 種。

パイプラインステップCV の役割
Nested CV + GridSearch外側 5-fold → 各 fold 内で内側 3-fold ハイパラ探索 → 外側 fold で最終評価ハイパラ最適化のリーク (test fold をハイパラ選定に使う) を回避し、 真の汎化性能を推定。
Pipeline + StratifiedKFoldsklearn Pipeline(StandardScaler → LogReg) を cross_val_score に渡すfold ごとに前処理を再 fit して訓練データの統計量だけで標準化、 リーク防止。
TimeSeriesSplit + Walk-forward時系列を時間順に拡大窓で 5 分割 → 各 fold で学習・直後窓で評価未来データを訓練に使うリークを構造的に排除し、 現実的な運用性能を推定。

③ 比較: CV vs 隣接評価法の使い分け判断

「CV を回すべきか、 holdout で十分か、 bootstrap が向くか」の判断基準。 実務で迷うポイントを比較表に整理。

状況holdoutk-fold CVLOOCVbootstrap
n < 100 の極小データ× (分散大)△ (k=5 推奨)○ (バイアス最小)○ (OOB)
n = 100-10000 中規模○ (k=5 or 10 が定番)× (計算コスト n 倍)
n > 100000 大規模○ (高速)△ (k=3 で妥協)× (実用不可)
時系列・自己相関あり△ (時間順 split)× (TimeSeriesSplit 必須)×× (Block bootstrap)
計算コスト最優先△ (k 倍)× (n 倍)△ (B 倍)
不確実性 (CI) 報告×○ (fold 分散)△ (分散大)○ (percentile CI)

原則: 「中規模で iid」なら k=5 or 10 fold CV が第一選択「極小」なら LOOCV または bootstrap「時系列」なら TimeSeriesSplit「大規模で速度優先」なら holdout が定石。

🌳 手法選択フロー

交差検証はデータを使い回して汎化性能を推定する。 「データの依存構造」「サンプル数」「ハイパラ調整の有無」を順番に確認して、 最適な分割方式を多段で絞り込む。

  1. ① サンプル数は十分(n > 5000)か?
    • Yes → ホールドアウト(train/valid/test 6:2:2)で計算コストを抑える → ② へ進み小規模 CV と比較
    • No → CV が必須 → ② へ
  2. ② データの依存構造は何か?
    • 時間順序あり(時系列・パネル)→ TimeSeriesSplit(過去 → 未来のリーク防止)→ ④ へ
      • 季節性が強い → Blocked CV(連続ブロック単位で分割)
      • 非定常 → Walk-forward / Expanding window
    • サンプルがグループ化(同一患者・同一店舗)→ GroupKFold(グループごと leak 防止)→ ④ へ
    • クラス不均衡(少数派 < 10%)→ StratifiedKFold → ③ へ
    • 独立同分布(iid)→ KFold → ③ へ
  3. ③ 分割数 k はいくつにするか?
    • n < 100(極小)→ LOOCV(k=n)でバイアス最小化、 ただし分散大
    • n = 100–10000 → k=5 または k=10(バイアス/分散バランス良)
    • n > 10000 → k=3 で計算節約、 分散が気になれば Repeated KFold
  4. ④ ハイパラ調整も同時に行うか?
    • Yes → Nested CV(外側=性能評価、 内側=ハイパラ探索)でリーク回避
    • No → 単純 CV で OK、 結果を 信頼区間 とともに報告
  5. ⑤ 結果の解釈・報告は?
    • fold ごとのスコア分散を必ず報告(mean ± std)
    • 1 σ が大きい → サンプル不足/不均衡疑い → ② へ戻る
    • train と valid の差が大きい → 過学習正則化 検討

💡 ポイント: 「iid なら KFold、 時系列なら TimeSeriesSplit、 不均衡なら Stratified、 グループ構造ありなら GroupKFold」を データの依存構造を最初に判定 してから k と nested の有無を決める、 という多段フローで考えると迷わない。

🔬 解説をさらに深める — 「シャッフル忘れ」の罠(実測)

このページの落とし穴集は「時系列ではシャッフルするな」を扱いました。 ここではその裏返しを、 SSDSE-B-2026(2023 年・47 都道府県)の実測値で掘り下げます。 断面データでは逆に「シャッフルを忘れると」CV のばらつき推定が歪みます。 姉妹ページ(検証データ/ネストCV)とは別角度で、 KFoldshuffle 引数だけに焦点を当てます。

💡 直感

scikit-learn の KFold既定が shuffle=Falseです。 つまり「行を上から順に k 個の塊へ切る」だけ。 SSDSE-B-2026 の行は都道府県コード順(北海道=01 → 沖縄=47、 ほぼ地理的な北→南)に並んでいます。 このまま 5-fold すると、 各 fold は「地域の帯」になります。 実際に KFold(5, shuffle=False) で 2023 年 47 件を割ると、 fold の中身はこうなりました(実測):

これは「日本を 5 本の地理帯に切って順番に検証する」のと同じ。 各 fold が全国を代表しておらず、 地域構造ごと当てにいく評価になります。

⚠️ 落とし穴(重要)

同じデータ・同じモデル(StandardScaler + Ridge(alpha=1)、 説明変数=人口 A1101・高齢化率・B4101・L3221、 目的変数=A4101)で、 shuffle だけを変えた 5-fold の fold 別 R²(実測)を並べます:

注目は標準偏差。 既定のままだと北日本だけの fold0 が R²=0.905 と 1 つだけ沈み、 ばらつきが 0.032 と約 6 倍に膨らみます。 平均 R² は 0.966 と 0.987 で大差ないため、 平均だけ見ていると「まあ安定」と誤読しがち。 しかし実体は「並び順という隠れた構造を fold に焼き付けてしまい、 汎化のばらつき推定を歪めた」状態です。 断面(1 年・47 県すべて別個体)のデータでは、 shuffle=True(+ random_state 固定)が正解。 「時系列はシャッフル禁止/断面はシャッフル必須」——この非対称を取り違えないことが要点です。

47 都道府県(番号=都道府県コード)が 5 fold のどこへ入るか
※ チップの配置は擬似乱数によるイメージ図(実際の numpy 分割とは並びが異なります)。 R² 数値は Python での実測値です。 shuffle=True の R² は seed=42 のもので、 seed を 0・7 などに変えると標準偏差は 0.005〜0.019、 平均は 0.98 前後(いずれも実測)。

🚀 発展

🔗 関連ページ

同じ CV でも角度の違う姉妹ページを併読すると立体的に掴めます: 検証(validation)ネスト交差検証検証データtrain-test 分割データリーケージ。 (このページ上部の「触って理解する」では k とモデル次数の関係を、 ここでは shuffle 引数を扱いました。)