「交差検証 (cross-validation)」はデータを K 分割し、 学習用と検証用を入れ替えて K 回評価することで汎化性能を推定する手法。 ホールドアウトより低分散の推定が得られ、 ハイパーパラメータ調整・モデル選択の標準ツール。 本ページでは K-fold・Stratified K-fold・LOO・Group K-fold・時系列 CV の使い分けと、 leakage を起こさない pipeline 設計を整理する。
これらのキーワードは「データ分割の戦略 → 学習/検証の反復 → スコアの平均と分散 → ハイパーパラメータ調整」という CV の標準フローを構成する。
🍰 まずはやさしく
データを分けて何度も試す方法です。
正しく性能を測るために使います。
スマホのアプリで何度もテストする感じです。
具体的なやり方とコツを学びます。
交差検証 ── データを複数回分割して評価する手法
n_jobs=-1 で全コア使用。 大きく短縮できる。StratifiedKFold を使うか、 fold ごとの y の統計を出して確認。深層学習の時代、 1 つのモデル学習に何時間〜何日もかかるため、 k=10 の CV を全部回すのは現実的ではありません。 代替手段として:
それでも、 「学習に使っていないデータで性能を測る」という CV の本質は変わりません。 道具は進化しても、 思想は普遍です。
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()) # 差が大きいと過学習サイン |
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}') |
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()) # 残差が偏っていれば特徴量不足のサイン |
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) |
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}') |
$\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 は「決まった手順を機械的に実行する儀式」ではなく、 「データとモデルの間の対話」です。 fold ごとのスコアを見れば「どのデータで間違えているか」がわかり、 学習曲線を見れば「データを増やすと改善するか」がわかり、 検証曲線を見れば「ハイパーパラメータの感受性」がわかります。
SSDSE-B-2026 のような小データでは、 CV の標準偏差が大きくなりがちです。 そこで諦めずに「なぜ大きいのか」「どの fold で外れているか」「外れている県の特徴は何か」を追求すると、 単なるスコアの数字を超えた知見が得られます。 CV は学びの入口であり、 出口でもあります。 データサイエンスを志すなら、 まずここを深く掘ってください。
本セクションをそのまま Jupyter Notebook に貼り付けて実行できます。 SSDSE-B-2026 から CV までの一連の流れを 1 つのコードブロックで体験:
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)) |
この一連のコードは、 「CV の基礎 → 拡張 → 解釈」の全体像を 1 ノートブックで把握できます。 講義スライドや実習ハンドアウトにもそのまま使えます。
伝統的 CV は「予測精度」を測るが、 因果推論の文脈では「反事実 (counterfactual) 予測の精度」を測る必要があり、 通常の CV では不十分。 Athey & Wager (2019) の Generalized Random Forest や Double ML は、 CV の中で因果推定量の偏りを補正する技術を組み込んでいます。
Bayesian の文脈では WAIC や PSIS-LOO (Pareto-Smoothed Importance Sampling LOO) が CV の代替として使われます。 LOOCV を全データから 1 回学習しただけで近似する手法で、 Stan や PyMC で実装されています。
公的統計の文脈では、 SSDSE-B のような全数データに対して因果効果を推定する研究も増加中です。 「東京と地方で同じ施策を実施したら効果はどう違うか」のような問いは、 因果 CV の出番です。
| 年 | 出来事 |
|---|---|
| 1931 | Larson が「データを 2 分割して評価」のアイデアを提案 |
| 1968 | Mosteller & Tukey が "cross-validation" の用語を使用 |
| 1974 | Stone が JRSS-B で CV を統計理論として定式化 |
| 1975 | Geisser が "predictive sample reuse" として独立に提案 |
| 1979 | Efron が Bootstrap を提案。 CV のライバルとして登場 |
| 1995 | Kohavi の IJCAI 論文。 「k=10 が経験的に良い」の根拠 |
| 2004 | Bengio & Grandvalet: CV 分散の不偏推定量は存在しない |
| 2007 | scikit-learn 初版リリース。 CV の API 標準化 |
| 2010 | Arlot & Celisse の包括的サーベイ |
| 2017 | Vehtari らが PSIS-LOO を提案。 Bayesian の標準 CV へ |
| 2023 | Bates et al. が JASA で CV の「真の意味」を再検討 |
CV は 1930 年代の素朴なアイデアから、 90 年の時を経て統計理論・機械学習・Bayesian の各分野で深化してきました。
1 2 | from sklearn.model_selection import cross_val_score, KFold scores = cross_val_score(model, X, y, cv=KFold(5, shuffle=True)) |
1 2 3 4 | library(caret) ctrl <- trainControl(method='cv', number=5) fit <- train(y ~ ., data=df, method='lm', trControl=ctrl) print(fit) |
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()) |
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)]) |
同じ KFold (random_state=42) を全モデルで使うことで、 fold が共通になり比較が公平。 違う seed の CV 結果を比べると差は意味を持ちません。
「都道府県を高齢化率 30% を境に 2 値分類」する場合、 35 県が陽性、 12 県が陰性のような不均衡。 StratifiedKFold で fold ごとの比率を保つ。 通常 KFold だと fold によっては陽性 0 件になり評価不能になる。
SSDSE-B の年度別パネルを使い、 「2013-2018 → 2019」「2013-2019 → 2020」「2013-2020 → 2021」と段階的に検証。 これが現実的な予測精度に最も近い評価。
8 地域を 4 fold (=2 地域ずつ) に分け、 「他地域で学習 → 対象地域を予測」。 地域間の汎化を厳しく評価したい時。
「都道府県 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)。
このような記述を含めれば、 査読者の不安を最小化できます。
CV は非復元分割で、 全データが必ず一度だけ検証に使われる。 Bootstrap は復元抽出で、 同じサンプルが複数回使われる一方、 ~37% のサンプルは抽出されず "OOB (Out-Of-Bag)" として検証に使われる。 統計量の分散推定なら Bootstrap、 モデル評価なら CV、 というのが大まかな使い分けです。
LOOCV は線形モデルにおいて AIC とほぼ等価。 Stone (1977) の結果として知られる。 ただし非線形・非パラメトリックモデルでは CV のほうが汎用的。 BIC は事前分布つきの周辺尤度近似で、 CV とは異なる目的(モデル選択 vs 予測誤差推定)を持つ。
CV は「過去データへの汎化」を測りますが、 「未来データへの汎化」「他地域への汎化」「他文化圏への汎化」は別の枠組み (transportability) が必要です。 SSDSE-B の 2023 年データで CV が高精度でも、 2030 年に同じ精度を出す保証はありません(人口動態の構造変化)。
LOOCV を Ridge 回帰で行う場合、 ハット行列の対角成分から閉形式で計算できる(PRESS 統計量)。 また Stochastic CV (一部 fold のみ評価) や Subset CV (データの 20% だけ使って評価) などの近似手法もあります。 大規模深層学習では特に重要。
通常 CV は平均誤差を報告しますが、 中央値や分位点を使うことで外れ値に頑健になります。 また Trimmed CV (上下 10% を除いた平均) も論文で使われます。 SSDSE-B では東京都が常に外れ値なので、 Trimmed CV で「東京を除いた 46 県での汎化」も併報すると有用です。
このチェックリストを毎回確認するだけで、 CV 関連の事故は 9 割減ります。
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 を題材に、 ぜひこの基礎を身につけて、 将来の様々なデータ分析の場面で武器にしてください。
この段取りに沿って進めれば、 CV を「儀式」ではなく「対話」として運用できます。
交差検証は「データ分割と汎化誤差推定」の組み合わせです。 関連する3点の図を並べて、 概念的な位置づけを確認します。
3点の図はそれぞれ「学習」「診断」「ハイパーパラメータ選択」の場面を示しており、 CV はそのいずれにも組み込まれます。
交差検証の理解度を確認する練習問題。 自分でコードを書いて確かめると一段定着する。
💬 これらの問題に答えられれば、 実務での「正しい CV 設計」ができるようになる。 自分でデータを動かしながら理解を深めよう。
🍰 まずはやさしく
結果のばらつきを防ぐ保険のようなものです。
評価の信頼性を高めるために使います。
1回のテストだけで成績を決めないのと似ています。
この手法がどこで使われるかを見ていきましょう。
「精度95%でした」と言うとき、 1回のtrain/testだけだと運次第。 CVは「複数回試して平均」する保険です。 競技でも論文でも、 評価の信頼性は CV で担保するのが標準。
本ページでは「cross validation」を扱う。 統計データ分析コンペティション (2026) の教材で、 SSDSE-B-2026 (47 都道府県 × 複数年 × 100 超列) の実データを使った再現可能な学習を目指す。
「cross validation」は統計・データサイエンスの体系における重要概念のひとつ。 本ページは「定義・直感・数式・実装・落とし穴・関連手法」の 6 視点で構成され、 各視点は独立して読めるが順序通り読むと体系的な理解が得られる。
🍰 まずはやさしく
模試を何度も受けて平均を出すイメージです。
たまたま当たっただけという運をなくします。
部活の練習試合を何度もして実力を測る感じです。
図を使って直感的に仕組みを理解しましょう。
「テスト勉強」に例えるなら:
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 で被験者単位に分け、 リークを防ぐ。
🍰 まずはやさしく
計算式で正しさを証明する方法です。
精度のばらつきを数字で表すために使います。
テストの点数の平均とズレを計算する感じです。
数式を使った定義と、詳しい計算方法を読みます。
標準誤差も算出できる:$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('差は統計的に有意') |
5 fold だけでは検定力が弱いので、 Repeated CV (30 fold 程度) を使うのが現実的。
本番運用では 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 スコアが優秀でも、 元データの品質が低ければ意味がありません。 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 または 10 | 47 県を 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 訓練 i | fold $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}}$ | シグマ fold | fold 間スコアの標準偏差 (ばらつき) | 5 fold の R² の SD (例: 0.01) |
式の読み下し: 「$k$ 個ある fold それぞれについて、 (a) その fold を除いた残りで学習し ($f_i$ を得る)、 (b) 学習に使わなかった $D_{\text{test},i}$ で予測しスコアを取り、 (c) $k$ 個のスコアを単純平均する」。 これにより全データが過不足なく 1 回ずつ検証に使われ、 汎化誤差の不偏推定が得られます。
派生概念:
交差検証 (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))]$ の不偏推定量」として、 単純な訓練誤差より遥かに信頼できる指標になります。
データ集合 $\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) を選びます。
$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 を使うことでリーク防止が自動化されます。
損失関数 $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)。
$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で行い、 分散をさらに減らす手法もあります。
CV 推定量 $\widehat{\mathrm{CV}}_k$ には統計的に重要な性質があります:
経験的に k=5 や k=10 が「バイアス・分散・計算量」のスイートスポット。 SSDSE-B のような小データでは LOOCV のバイアスの優位が分散の悪化を上回ることもあり、 ケースバイケースで比較すべきです。
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年で検証)は実務的に重要な評価軸です。
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}') |
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%) が保たれる |
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 に分散しない |
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}]') # 過去で学習→未来で検証 を順次拡大 |
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 での平均(分散が下がる) |
「最良のハイパーパラメータ + そのモデルの汎化性能」を同時に出すには 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_) |
5 × 3 × 12 (param 組合せ) = 180 回学習。 SSDSE-B (n=47) なら数秒で終わる。 大規模データなら RandomizedSearchCV や Optuna へ。
CV の出力 (OOF 予測) を集めて、 もう一段別のモデルを乗せるのが Stacking。 Kaggle の上位解法の常套手段。
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}') |
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) で再現し、 結果が一致することを確認する。
$$ \mathrm{CV}_k = \frac{1}{k} \sum_{i=1}^{k} \mathrm{Error}_i $$
k は fold 数、 $\mathrm{Error}_i$ は i 番目の fold をテストに用いたときの誤差 (RMSE, MSE, 1-R² など)。
| fold | テストサンプル (index) | 訓練サンプル数 | Error_i |
|---|---|---|---|
| 1 | {1, 2} | 6 | 0.20 |
| 2 | {3, 4} | 6 | 0.15 |
| 3 | {5, 6} | 6 | 0.30 |
| 4 | {7, 8} | 6 | 0.25 |
| 項目 | 計算 | 結果 |
|---|---|---|
| $\sum_{i=1}^{4} \mathrm{Error}_i$ | $0.20 + 0.15 + 0.30 + 0.25$ | 0.90 |
| $k$ | — | 4 |
| 項目 | 計算 | 結果 |
|---|---|---|
| $\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 |
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}") |
💬 手計算 (Step 3 の 0.225) と Python の numpy 平均 (0.2250) が完全一致。 標準偏差 0.056 は fold 間のばらつきが小さいことを意味し、 「特定の fold で運よく良い結果が出ただけ」ではないと言える。 sklearn 例はあくまでパイプラインの確認用で、 k-fold CV は「fold ごとのエラーを平均する」という単純な数式に帰着することがポイント。
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}') |
make_pipeline を使うことで、 StandardScaler の fit も各 fold 内で行われる(=検証セットの平均/分散が学習に漏れない)。 これがリーク防止の基本パターン。 47 県程度の小データでは LOOCV (k=47) を選ぶ手もある。| 業界 | 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 |
| 手法 | 分割方式 | 計算コスト | 推定の分散 | 推奨場面 |
|---|---|---|---|---|
| Hold-out | train 70 / test 30 を1回 | 最小 | 大 | 大規模データ・速度重視 |
| k-Fold CV | k 個に等分 | k 倍 | 中 | 標準(k=5 or 10) |
| Stratified k-Fold | クラス比率を保って k 分割 | k 倍 | 中 | クラス不均衡分類 |
| LOOCV | 1点だけ検証, n 個 | n 倍 | 小(バイアスは小・分散は高い) | 小データ (n<100) |
| Group K-Fold | グループ単位で分割 | k 倍 | 中 | ユーザー・患者単位 |
| Time-Series CV | 時間順、 過去→未来 | k 倍 | 中 | 時系列予測(必須) |
| Repeated k-Fold | k-Fold を r 回繰り返す | k×r 倍 | 最小 | 論文・正式報告 |
| Nested CV | 外側=評価, 内側=ハイパー調整 | k² 倍 | 最小 | モデル比較の厳密評価 |
shuffle=False と shuffle=True で結果が変わる理由を、 SSDSE のデータが「都道府県コード順に並んでいる」事実から説明せよ。「先に StandardScaler でデータ全体を標準化してから cross_val_score」とすると、 検証セットの平均・分散が学習に漏れる。 結果として CV スコアが楽観的(実運用より良い数値)になる。 対策: 必ず Pipeline で前処理を内側に入れる。
株価や売上の時系列を通常 k-Fold で評価すると、 未来データで学習し過去データで検証することになり、 現実とは逆。 オフラインで高 R² でも本番で全く動かない。 対策: TimeSeriesSplit を使う。
同じ患者の異なる検査が train と test に分散すると「同じ人」を覚えてしまい AUC が異常に高い。 対策: GroupKFold(group=patient_id) で患者単位分割。
同じ CV ループ内でハイパーパラメータをグリッド探索し、 そのベストスコアを「汎化性能」として報告する → 楽観バイアス。 対策: Nested CV(外側 fold で評価、 内側 fold で調整)。
k=2 など極端に小さい k だと fold ごとの差が大きく、 「モデルAがB より良い」と言えるか判断できない。 対策: k=5 か 10 を基本に、 さらに Repeated CV で分散を下げる。
交差検証の概念は、 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 の理論理解が依然必須です。
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}') |
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}') |
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()) # 最も過大予測の県(東京など) |
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()}') # 時系列では「過去で学習 → 未来で検証」の順序を必ず守る |
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}') |
🎯 このコードでやること:SSDSE-B-2026 の都道府県データ(X = 総人口・65歳以上人口・年平均気温など、 y = 出生数)に対して sklearn.model_selection.KFold で 5-fold 交差検証を行い、 Ridge 回帰の R² の平均と分散を取得する。 47 都道府県を 5 分割し、 各 fold で「学習 38 県 → 評価 9 県」を入れ替えて合計 5 回モデルを学習・評価する。
📥 入力データ(SSDSE-B-2026 抜粋):
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') |
📤 実行すると次の出力が得られる:
💬 結果の読み方: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(同一グループの漏れ防止)の検討対象。
cross_val_score(GridSearchCV(...), X, y, cv=outer) の Nested CV にし、 外側で汎化、 内側で調整に分業する。TimeSeriesSplit または年度 hold-out (train: 2013-2020, test: 2021-2023) に切り替える。StratifiedKFold(n_splits=5) に変え、 さらに少数派なら StratifiedGroupKFold で集団リークも防ぐ。Pipeline([scaler, model]) を `cross_val_score` に渡し、 fold 内で fit を強制する。RepeatedKFold(n_splits=5, n_repeats=10) で平均化する。GroupKFold(groups=df['都道府県']) で県単位に分割し、 真の汎化を測る。カテゴリ変数を「そのカテゴリの目的変数平均」で置き換える Target Encoding は、 CV の外で実行すると検証セットの y が学習にリーク。 対策: category_encoders の TargetEncoder を Pipeline 内で使う。
中央値補完を CV 外で行うと、 検証セットの中央値情報も学習に含まれる。 対策: SimpleImputer を Pipeline 内に。
相関の高い列を CV 外で選んで → CV で評価すると、 「選別段階で y を見てしまった」分のリーク。 対策: SelectKBest を Pipeline 内に。
不均衡データで SMOTE オーバーサンプリングを CV 外で行うと、 合成サンプルが train/test に分散。 対策: imbalanced-learn の Pipeline を使い、 SMOTE を内側へ。
株価予測などで shuffle=True にすると、 「未来から学んで過去を当てる」非現実シナリオ。 対策: TimeSeriesSplit。
random_state=42 を明記。これを守らない論文はリジェクトされる確率が高い(特に NeurIPS, ICML, KDD)。 公的統計分析でも同じ厳密性を期待される時代です。
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())]) |
train_test_split から KFold へ移行。 cross_val_score を使えるように。交差検証は「保険」ではなく「設計」です。 SSDSE-B-2026 のような小サンプルデータでこそ、 CV を正しく使いこなせるかが分析の信頼性を左右します。
「2013-2020年で学習 → 2021-2023年で検証」のような時間的 hold-out を、 SSDSE-B-2026 の多年データで実施します。 これは公的統計の実務に近い評価方法です。
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}') |
時間的 hold-out の方が「現実的な汎化性能」を反映するが、 fold 数が 1 しかなく分散がわからない。 両者を併報するのがベストプラクティス。
交差検証(CV)は機械学習・統計学習の汎化誤差推定における標準ツールだが、 データ構造・分割設計・指標選択を誤ると CV の結果が「本番性能の信頼できる推定」にならない。 SSDSE-B-2026(N=47)のような小サンプルでは特に CV の使い方が結果を大きく左右する。 ここでは適用条件・限界・誤解回避を整理する。
💬 CV は「データを賢く使い回す」テクニックだが、 同時に「自分の楽観バイアスを増幅する道具」にもなる。 データ構造の確認、 leakage の遮断、 nested 構造、 ホールドアウト併用 — この 4 つを毎回チェックすることが、 CV 結果を信頼できる汎化誤差推定に保つ最短路。
スライダーを動かすと、 図と数値がその場で更新されます。 分割数 k を変えると各 fold の訓練(青)/検証(橙)の割り当てがどう変わるか、 多項式の次数を変えると交差検証スコア(平均 R²)が最適次数で最大化し、 それを超えると過学習で落ちる様子を体感できます。
※ このデモの計算:説明用の擬似データ(真の関数 y = sin(2πx) にノイズを加えた 16 点、 乱数シード固定)に対し、 多項式回帰+k-fold CV を ブラウザ内で正確に計算しています。 実データ(SSDSE-B-2026)での分析は上の「🐍 Python 実装」を参照してください。
k を増やすと各 fold の訓練データが増えて(=より本番に近いデータ量で学習でき)バイアスは下がりますが、 検証セットが小さくなり fold ごとのスコアのばらつき(標準偏差)は大きくなります。 k = n(= LOOCV)の極端では、 検証は毎回 1 点だけ。 このとき fold 単位の R² は「1 点では平均が定義できない」ため計算不能になり、 全 n 個の予測をまとめた プール R² で評価するのが実務の作法です(デモでも自動でそう切り替わります)。 一方 次数を上げると訓練 R²(青線)は 1 に張り付いていきますが、 CV R²(橙線)は最適次数でピークを打ち、 その先は過学習で急落します。 この「青は上がり続け・橙は途中で折り返す」ギャップこそ過学習のサインです。
Pipeline に入れて fold 内で fit しましょう。StratifiedKFold で各 fold のクラス比率を揃えます。shuffle=True にすると「未来で学習して過去を当てる」非現実的な評価になり、 本番で崩れます。 TimeSeriesSplit や年度ホールドアウトを使います。上のデモでは「次数を選ぶ」ことと「スコアを測る」ことを同じ 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)
交差検証は単純分割の上位、 ハイパラ調整の前処理として機能する。 本章では「接続 (どこから繋がるか)」「統合 (どう組み合わせるか)」「比較 (何と使い分けるか)」の 3 視点で隣接手法を整理する。
CV は train/test 分割の発展形であり、 ハイパラ調整・モデル選択・性能報告の中核をなす。
CV 単独より、 前処理・特徴量選択・ハイパラ探索とパイプライン化した時に真価を発揮する。 SSDSE-B 規模の中規模データでの定番ワークフロー 3 種。
| パイプライン | ステップ | CV の役割 |
|---|---|---|
| Nested CV + GridSearch | 外側 5-fold → 各 fold 内で内側 3-fold ハイパラ探索 → 外側 fold で最終評価 | ハイパラ最適化のリーク (test fold をハイパラ選定に使う) を回避し、 真の汎化性能を推定。 |
| Pipeline + StratifiedKFold | sklearn Pipeline(StandardScaler → LogReg) を cross_val_score に渡す | fold ごとに前処理を再 fit して訓練データの統計量だけで標準化、 リーク防止。 |
| TimeSeriesSplit + Walk-forward | 時系列を時間順に拡大窓で 5 分割 → 各 fold で学習・直後窓で評価 | 未来データを訓練に使うリークを構造的に排除し、 現実的な運用性能を推定。 |
「CV を回すべきか、 holdout で十分か、 bootstrap が向くか」の判断基準。 実務で迷うポイントを比較表に整理。
| 状況 | holdout | k-fold CV | LOOCV | bootstrap |
|---|---|---|---|---|
| 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 が定石。
交差検証はデータを使い回して汎化性能を推定する。 「データの依存構造」「サンプル数」「ハイパラ調整の有無」を順番に確認して、 最適な分割方式を多段で絞り込む。
💡 ポイント: 「iid なら KFold、 時系列なら TimeSeriesSplit、 不均衡なら Stratified、 グループ構造ありなら GroupKFold」を データの依存構造を最初に判定 してから k と nested の有無を決める、 という多段フローで考えると迷わない。
このページの落とし穴集は「時系列ではシャッフルするな」を扱いました。 ここではその裏返しを、 SSDSE-B-2026(2023 年・47 都道府県)の実測値で掘り下げます。 断面データでは逆に「シャッフルを忘れると」CV のばらつき推定が歪みます。 姉妹ページ(検証データ/ネストCV)とは別角度で、 KFold の shuffle 引数だけに焦点を当てます。
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 固定)が正解。 「時系列はシャッフル禁止/断面はシャッフル必須」——この非対称を取り違えないことが要点です。
shuffle=True でも 1 つの seed の std は seed 次第でぶれます(実測で 0.005〜0.019)。 RepeatedKFold(n_splits=5, n_repeats=10) で複数 seed を平均すれば、 fold 割りの運に依存しない安定した推定になります。 N=47 の小標本では特に有効。GroupKFold(groups=Prefecture) で県ごとに fold を分けるのが正解——「シャッフルすべきか/群で切るべきか」はデータの個体の独立性で決まります。StratifiedKFold(連続値は分位でビン化)で各 fold の分布を揃えるほうが、 fold 間 R² のばらつきをさらに抑えられます。同じ CV でも角度の違う姉妹ページを併読すると立体的に掴めます:
検証(validation) /
ネスト交差検証 /
検証データ /
train-test 分割 /
データリーケージ。
(このページ上部の「触って理解する」では k とモデル次数の関係を、 ここでは shuffle 引数を扱いました。)