🎨 直感で掴む 📐 分割式 🧮 実値で計算 🐍 train_test_split 🐍 KFold 🐍 Stratified 🐍 時系列分割 ⚠️ 落とし穴 🌐 関連手法 📚 グループ教材
60 / 20 / 20 または 70 / 15 / 15。 サンプルが少ない時は k-fold CV、 時系列なら time-series split、 不均衡分類なら stratified split を選ぶ。モデル学習は「受験勉強」、 train は「過去問」、 validation は「模試」、 test は「本試験」。 過去問だけで勉強すると「答えを覚える」だけになる(過学習)。 模試で「弱点を見つけて勉強法を変える」(ハイパラ調整)。 本試験は当日 1 回しか受けられない(test set は最後に 1 回)。 もし「模試の点を見て勉強法を 100 回変える」と、 模試の問題に最適化しすぎて本試験では失敗する(validation set overfitting)。
| 役割 | 英語 | 用途 | 頻度 | 受験比喩 |
|---|---|---|---|---|
| 学習 | Training set | パラメータ推定 | 毎エポック使う | 過去問 |
| 検証 | Validation set | ハイパラ調整・モデル選択 | 数十回〜数百回 | 模試 |
| テスト | Test set | 最終性能評価 | 最後に 1 回だけ | 本試験 |
全データを $\mathcal{D} = \mathcal{D}_{\text{train}} \cup \mathcal{D}_{\text{val}} \cup \mathcal{D}_{\text{test}}$ と分割。 各部分集合は互いに素 ($\mathcal{D}_i \cap \mathcal{D}_j = \emptyset$)。 比率を $r_{\text{tr}} : r_{\text{val}} : r_{\text{te}}$ とすると:
$$ |\mathcal{D}_{\text{train}}| = r_{\text{tr}} N,\ \ |\mathcal{D}_{\text{val}}| = r_{\text{val}} N,\ \ |\mathcal{D}_{\text{test}}| = r_{\text{te}} N,\ \ r_{\text{tr}} + r_{\text{val}} + r_{\text{te}} = 1 $$
$$ \text{k-fold CV: } \mathcal{D}_{\text{train}} = \bigcup_{j \neq i} \mathcal{F}_j,\ \mathcal{D}_{\text{val}} = \mathcal{F}_i,\ i = 1, \ldots, k $$
検証スコアの標準誤差は $SE \approx \sigma / \sqrt{|\mathcal{D}_{\text{val}}|}$ に従う。 例えば「精度の差 1% を検出したい」場合、 必要な検証セットサイズは Bernoulli の場合:
$$ N_{\text{val}} \ge \left(\frac{z_{\alpha/2} + z_\beta}{\Delta}\right)^2 \cdot 2 p (1-p) $$
$p \approx 0.85$、 $\Delta = 0.01$、 $\alpha=0.05$、 $\beta=0.20$ なら $N_{\text{val}} \approx 20000$ 件必要。 SSDSE-B-2026 の 564 件では「1% 差」は検出不能、 「5〜6% 差」程度の比較しかできない。
SSDSE-B-2026 単年(47 県、 2023 年)を 60/20/20 で分けると検証セット 10 件しかなく評価が不安定。 全 12 年(564 件)を使えば検証セット 113 件で実用範囲。 さらに 5-fold CV にすれば fold あたり 113 件 × 5 回で安定する。
| データ規模 | 分割 | train | val | test | val 上の SE |
|---|---|---|---|---|---|
| 47 件 (1 年) | 60/20/20 | 27 | 10 | 10 | ±0.16 (大) |
| 47 件 (1 年) | 5-fold CV | 37 | 10 ×5 | — | ±0.07 (中) |
| 564 件 (12 年) | 60/20/20 | 338 | 113 | 113 | ±0.05 |
| 564 件 (12 年) | 5-fold CV | 451〜452 | 113 ×4・112 ×1 | — | ±0.02 (小) |
| 564 件 (12 年) | LOOCV | 563 | 1 ×564 | — | ±0.04 (大、 計算重) |
🎯 このコードでやること: SSDSE-B-2026 を 60/20/20 で train/val/test に分割。 train_test_split を 2 回呼んで 3 分割を実現。
📥 入力データ: SSDSE-B-2026 47 件 (2023 年単年)、 X=[A1101, A1301, A4101]、 y=A1102 (日本人人口) など。
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.model_selection import train_test_split df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) d = df[df['SSDSE-B-2026']==2023] X = d[['A1101', 'A1301', 'A4101']].values y = d['A1102'].values if 'A1102' in d.columns else d['A4101'].values # まず 80/20 で train+val と test を分け、 次に train+val を 75/25 で再分割 # 結果: train=60%, val=20%, test=20% X_temp, X_test, y_temp, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) X_train, X_val, y_train, y_val = train_test_split( X_temp, y_temp, test_size=0.25, random_state=42 ) print(f'Total: {len(X)} 件') print(f' Train: {len(X_train)} 件 ({len(X_train)/len(X)*100:.1f}%)') print(f' Val: {len(X_val)} 件 ({len(X_val)/len(X)*100:.1f}%)') print(f' Test: {len(X_test)} 件 ({len(X_test)/len(X)*100:.1f}%)') |
📤 実行すると次の出力が得られる:
💬 結果の読み方: 47 件を 60/20/20 にすると検証セットが 10 件しかない。 これは「モデル A の精度 0.85、 モデル B の精度 0.83、 どちらが良い?」を判定するには不十分(SE 大)。 SSDSE 1 年規模では k-fold CV が必須。 random_state=42 を固定すれば再現性が保てる。
🎯 このコードでやること: SSDSE-B-2026 (47 件) で 5-fold CV を実装し、 各 fold の train/val サイズと、 LinearRegression のスコアを出力。
📥 入力データ: SSDSE-B-2026 47 県 2023 年、 X=[A1101]、 y=A4101 (出生数)。
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.model_selection import KFold from sklearn.linear_model import LinearRegression df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) d = df[df['SSDSE-B-2026']==2023] X = d[['A1101']].values y = d['A4101'].values kf = KFold(n_splits=5, shuffle=True, random_state=42) scores = [] for i, (tr_idx, va_idx) in enumerate(kf.split(X)): X_tr, X_va = X[tr_idx], X[va_idx] y_tr, y_va = y[tr_idx], y[va_idx] lr = LinearRegression().fit(X_tr, y_tr) score = lr.score(X_va, y_va) print(f'Fold {i+1}: train={len(tr_idx)}, val={len(va_idx)}, R²={score:.4f}') scores.append(score) import numpy as np print(f'\\n5-fold 平均 R²: {np.mean(scores):.4f} ± {np.std(scores):.4f}') |
📤 実行すると次の出力が得られる:
💬 結果の読み方: 5-fold で R² = 0.980 ± 0.016。 SD 0.016 は小さく、 線形回帰がこのデータによくフィットしていることを示す。 もし単一 holdout (val 10 件) で R²=0.98 と報告されたら「fold によっては 0.95 になるかも」という不確実性が伴う(実際 Fold 3 は 0.95)が、 CV では複数 fold で平均化されているので堅牢。
🎯 このコードでやること: SSDSE-B-2026 の人口を 3 分位で「大県/中県/小県」に分類するタスクを想定し、 各 fold でクラス比率が同じになる Stratified CV を実装。
📥 入力データ: SSDSE-B-2026 47 県、 人口 (A1101) の 3 分位ラベル (small/medium/large)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | import pandas as pd from sklearn.model_selection import StratifiedKFold import numpy as np df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) d = df[df['SSDSE-B-2026']==2023] # 人口を 3 分位でラベル化 q33, q67 = d['A1101'].quantile([0.33, 0.67]) def cls(x): return 'large' if x>=q67 else ('medium' if x>=q33 else 'small') y = d['A1101'].apply(cls).values X = d[['A4101','A1301']].values # 出生数、0-14歳 print('全体クラス比:', pd.Series(y).value_counts(normalize=True).round(3).to_dict()) skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) for i, (tr_idx, va_idx) in enumerate(skf.split(X, y)): val_dist = pd.Series(y[va_idx]).value_counts(normalize=True).round(3).to_dict() print(f'Fold {i+1} val ({len(va_idx)}件): {val_dist}') |
📤 実行すると次の出力が得られる:
💬 結果の読み方: 全 fold でクラス比率がほぼ 1:1:1 に揃った。 不均衡データ(少数クラス 5%)で通常 KFold を使うと、 ある fold には少数クラスが 0 件入ってしまい評価が壊れる。 Stratified が必須。 SSDSE では人口分布は連続値だが、 「3 分位ラベル化 → Stratified」で似た学習・検証バランスを保てる。
🎯 このコードでやること: SSDSE-B-2026 を「2012〜2023 年」の時系列データとして、 train が過去、 val が未来になる分割。 通常 KFold だと「未来で学習して過去で検証」になり、 実運用では役に立たない。
📥 入力データ: SSDSE-B-2026 全 12 年 × 47 県 = 564 件、 年順にソート。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import pandas as pd from sklearn.model_selection import TimeSeriesSplit df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) d = df.sort_values('SSDSE-B-2026').reset_index(drop=True) X = d[['A1101']].values y = d['A4101'].values years = d['SSDSE-B-2026'].values tscv = TimeSeriesSplit(n_splits=5) for i, (tr_idx, va_idx) in enumerate(tscv.split(X)): tr_years = sorted(set(years[tr_idx])) va_years = sorted(set(years[va_idx])) print(f'Fold {i+1}: train={len(tr_idx)}件 ({tr_years[0]}〜{tr_years[-1]}), ' f'val={len(va_idx)}件 ({va_years[0]}〜{va_years[-1]})') |
📤 実行すると次の出力が得られる:
💬 結果の読み方: train が「最初の数年」、 val が「次の数年」になり、 時間方向の順序が保たれる。 これにより「2024 年を予測する」という実運用シナリオに近い評価ができる。 KFold だと「2023 年で学習 → 2012 年で検証」が起きうるが、 時系列予測としては無意味(リーク)。
sklearn.pipeline.Pipeline + cross_val_score で自動化できる。GroupKFold で「顧客単位 / 県単位」で分割する。| 領域 | 手法 | 関係 |
|---|---|---|
| 基本分割 | Hold-out / train_test_split | 単純な 1 回切り |
| 交差検証 | k-fold CV / LOOCV | 複数回平均で堅牢 |
| 特殊分割 | Stratified / Group / TimeSeries | データ特性に応じて |
| 階層化 | Nested CV | ハイパラ調整と性能評価を分離 |
| リスク | データリーク / 過学習 | 不適切分割で発生 |
前提: 訓練データ / テストデータ / ホールドアウト
並列: 交差検証 / ブートストラップ / 早期終了
発展: 入れ子 CV / ハイパラ調整 / データリーク / 汎化性能
ハイパラ調整と性能評価を同じ val で行うと、 val set overfitting で評価が楽観的になる。 Nested CV は「外側 CV で性能評価、 内側 CV でハイパラ調整」の 2 層構造:
$$ \text{Outer CV (k=5): } \mathcal{D} \to \{\mathcal{D}^{out}_i\}_{i=1}^{5} $$
$$ \text{Inner CV (k=3): } \mathcal{D}^{out}_{train} \to \{\mathcal{D}^{in}_j\}_{j=1}^{3} \to \text{best hyperparameter} $$
Cawley & Talbot (2010) によれば、 通常の grid search + CV だと「最良 CV スコア」が約 3〜5% 楽観的にバイアス。 Nested CV は計算量が k_outer × k_inner 倍だが、 真の汎化性能を見るには必須。
🎯 このコードでやること: SSDSE-B-2026 で RandomForest のハイパラ (n_estimators, max_depth) を内側 3-fold で調整、 外側 5-fold で性能評価する nested CV。
📥 入力データ: SSDSE-B-2026 47 県、 X=[A1101, A1301]、 y=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 30 | import pandas as pd import numpy as np from sklearn.model_selection import GridSearchCV, KFold, cross_val_score from sklearn.ensemble import RandomForestRegressor df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) d = df[df['SSDSE-B-2026']==2023] X = d[['A1101', 'A1301']].values y = d['A4101'].values param_grid = { 'n_estimators': [50, 100, 200], 'max_depth': [3, 5, None], } inner_cv = KFold(n_splits=3, shuffle=True, random_state=0) outer_cv = KFold(n_splits=5, shuffle=True, random_state=42) # 内側 CV でハイパラ調整、 外側 CV で評価 grid = GridSearchCV(RandomForestRegressor(random_state=0), param_grid, cv=inner_cv, scoring='r2', n_jobs=-1) nested_scores = cross_val_score(grid, X, y, cv=outer_cv, scoring='r2') print(f'Nested CV R² (外側 5-fold): {nested_scores}') print(f'平均: {nested_scores.mean():.4f} ± {nested_scores.std():.4f}') # 全データでハイパラ調整時の楽観バイアス比較 grid.fit(X, y) print(f'\\n全データ grid search の best CV score: {grid.best_score_:.4f}') print(f'差分 (楽観バイアス): {grid.best_score_ - nested_scores.mean():.4f}') |
📤 実行すると次の出力が得られる:
💬 結果の読み方: この実測では外側 Fold 1 の R² が 0.81 と低く、 nested CV 平均は 0.9537 ± 0.0711。 差分がマイナスになったのは、 47 件の小標本では fold 構成のばらつき(分散)が楽観バイアスより大きく、 バイアスの符号すら安定しないため。 ハイパラ空間が広い深層学習やテキスト分類では 3〜5% の楽観バイアスが観測される(Cawley & Talbot 2010)ため、 「報告するスコアは nested CV 由来であるべき」という原則自体は変わらない。 小標本ではまず分散(fold 間ばらつき)を疑うこと。
🎯 このコードでやること: SSDSE-B-2026 で「StandardScaler → Ridge 回帰」のパイプラインを構築し、 cross_val_score で評価。 これにより fold ごとに「scaler は train 部分でだけ fit」が保証され、 リークが起こらない。
📥 入力データ: SSDSE-B-2026 47 県 (2023)、 X=3 特徴量、 y=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 | import pandas as pd import numpy as np 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 df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) d = df[df['SSDSE-B-2026']==2023] X = d[['A1101', 'A1301', 'E1101']].values y = d['A4101'].values # NG パターン: 先に全データ scale → CV scaler_bad = StandardScaler().fit(X) X_scaled = scaler_bad.transform(X) ridge = Ridge(alpha=1.0) bad_scores = cross_val_score(ridge, X_scaled, y, cv=5, scoring='r2') print(f'NG (リーク): R² = {bad_scores.mean():.4f} ± {bad_scores.std():.4f}') # OK パターン: Pipeline で fold ごとに fit pipe = Pipeline([ ('scaler', StandardScaler()), ('ridge', Ridge(alpha=1.0)), ]) good_scores = cross_val_score(pipe, X, y, cv=5, scoring='r2') print(f'OK (リーク防止): R² = {good_scores.mean():.4f} ± {good_scores.std():.4f}') print(f'差分: {bad_scores.mean() - good_scores.mean():.4f}') |
📤 実行すると次の出力が得られる:
💬 結果の読み方: SSDSE では差が 0.0001 と小さいが、 これは「scale のパラメータ (mean, std)」が train と全体でほぼ同じだから。 サンプル数が少ない / 分布が偏る場合、 差は 0.05 以上になることもある。 Pipeline は 常に使うべき習慣。 SimpleImputer、 PCA、 SelectKBest なども必ず Pipeline 内に置く。
SSDSE-B-2026 全 12 年データ (564 件) を通常 KFold で分けると、 同じ県の 2023 年データが train、 2022 年データが val に入ることがある。 「同じ県の異なる年」は強く相関するため、 これは group leakage。 GroupKFold で「県単位」で分割すれば防げる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | import pandas as pd from sklearn.model_selection import GroupKFold df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) X = df[['A1101', 'E1101']].values y = df['A4101'].values groups = df['Code'].values # 県コードでグループ化 gkf = GroupKFold(n_splits=5) for i, (tr_idx, va_idx) in enumerate(gkf.split(X, y, groups)): tr_codes = set(groups[tr_idx]) va_codes = set(groups[va_idx]) overlap = tr_codes & va_codes print(f'Fold {i+1}: train={len(tr_idx)}件 ({len(tr_codes)}県), ' f'val={len(va_idx)}件 ({len(va_codes)}県), ' f'重複県={len(overlap)}') |
📤 実行すると次の出力が得られる:
重複県 0 で「同じ県の異なる年」が train と val に入らない。 「未知の県の出生数を予測する」という設定では GroupKFold が必須。 顧客 ID、 患者 ID、 セッション ID でも同じ原則。
| 状況 | 推奨手法 | 理由 |
|---|---|---|
| N > 10000、 IID | 単純な 60/20/20 holdout | val 2000 件で SE 小、 計算軽量 |
| N < 1000、 IID | 5-fold or 10-fold CV | 分散小、 holdout だと信頼性低 |
| N < 100 | LOOCV or Stratified k-fold (k 大) | バイアス低減優先 |
| 不均衡分類 (少数 5%) | StratifiedKFold | 各 fold で少数クラスを保証 |
| 時系列予測 | TimeSeriesSplit / blocked CV | 未来→過去のリーク防止 |
| パネル / クラスタデータ | GroupKFold | 同一個体の漏出防止 |
| ハイパラ最適化 | Nested CV | 調整と評価を分離 |
| 確率推定 / 校正 | StratifiedKFold + Calibration | Brier / Reliability の正確評価 |
各戦略の「バイアス・分散・計算量」を比較する:
| 手法 | 学習回数 | バイアス | 分散 | 計算量 |
|---|---|---|---|---|
| Holdout (80/20) | 1 | 中(train 8 割で過小評価可能) | 大(1 回だけ) | 最小 |
| k=5 CV | 5 | 小 | 中 | 5× |
| k=10 CV | 10 | 小(train 9 割で適切) | 小 | 10× |
| LOOCV (k=N) | N | 最小(train N-1) | 大(fold が高度相関) | N× (重) |
| Repeated k-fold | k×R | 小 | 最小 | k×R (重) |
| Bootstrap (.632) | B | 中 | 小 | B× (B=200〜1000) |
実務の落としどころ: k=5 CVが「精度・速度・分散のバランス最良」で最も普及。 N が極端に少ない (< 100) なら LOOCV、 N が大きい (> 10万) なら単一 holdout で十分。
| 業界 | 推奨 | 理由 |
|---|---|---|
| 医療画像 (CNN) | GroupKFold (患者単位) | 同一患者の複数スライス漏出防止 |
| 金融予測 | Walk-Forward (時系列) | 未来情報の漏出防止 |
| EC 推薦 | 時系列 + ユーザ単位 hybrid | 既存ユーザ vs 新規ユーザ |
| テキスト分類 | StratifiedKFold | クラス不均衡 |
| 音声認識 | GroupKFold (話者単位) | 同一話者の漏出防止 |
| 公衆衛生 (SSDSE 系) | 時系列 (年別) + 地域別 GroupKFold | 県の同時相関、 年次トレンド |
| Kaggle コンペ | 主催者指定の Public/Private 分割 | リーダーボード過適合警戒 |
精度 $p$ の二値分類器の精度差 $\Delta p$ を検出するための検証セットサイズ:
$$ N_{\text{val}} \ge \frac{(z_{\alpha/2} + z_\beta)^2 \cdot p (1-p)}{\Delta p^2} $$
例: $p = 0.85, \Delta p = 0.02, \alpha=0.05$ (両側), $\beta=0.20$ なら:
$$ N_{\text{val}} \ge \frac{(1.96 + 0.84)^2 \cdot 0.85 \cdot 0.15}{0.02^2} = \frac{7.84 \cdot 0.1275}{0.0004} \approx 2499 $$
「2 つのモデルの精度差 2% を 80% の検出力で検出するには検証セット 2500 件が必要」。 これより小さな検証セットでの「モデル A の方が良い」報告は統計的に弱い。 SSDSE 規模 (564 件) では 5% 以上の精度差しか確実に検出できない。
🎯 このコードでやること: RepeatedKFold で 5-fold CV を 10 回繰り返し、 random_state による結果のブレを実測する。
📥 入力データ: SSDSE-B-2026 47 県、 X=人口、 y=出生数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | import pandas as pd import numpy as np from sklearn.model_selection import RepeatedKFold, cross_val_score from sklearn.linear_model import LinearRegression df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) d = df[df['SSDSE-B-2026']==2023] X = d[['A1101']].values y = d['A4101'].values rkf = RepeatedKFold(n_splits=5, n_repeats=10, random_state=42) scores = cross_val_score(LinearRegression(), X, y, cv=rkf, scoring='r2') print(f'5-fold × 10回 = {len(scores)} 個の R² スコア') print(f'平均: {scores.mean():.4f}') print(f'標準偏差: {scores.std():.4f}') print(f'95% CI (パーセンタイル): [{np.percentile(scores, 2.5):.4f}, ' f'{np.percentile(scores, 97.5):.4f}]') print(f'最小: {scores.min():.4f}, 最大: {scores.max():.4f}') |
📤 実行すると次の出力が得られる:
💬 結果の読み方: 1 回の 5-fold CV(random_state=42、 上の②)では 0.9798 ± 0.0160 だったが、 seed を変えて 10 回繰り返すと平均 0.9772、 SD は 0.0291 と約 1.8 倍に広がる(最小は 0.83 まで落ちる fold もある)。 「random_state 1 個だけ」の評価は信頼性が低い。 論文では Repeated K-fold か少なくとも複数 seed の平均を報告するべき。
検証データの新しい使い方として、 conformal prediction (CP) では validation set を「calibration set」と呼び、 予測区間の有限サンプル保証に使う:
$$ \text{Calibration set } \mathcal{D}_{\text{cal}} \to \text{nonconformity scores } \{s_i\}_{i=1}^{n_{\text{cal}}} $$
$$ \text{予測区間 } C(x) = \{y : s(x, y) \le \hat{q}_{1-\alpha}\}, \quad \hat{q}_{1-\alpha} = \text{Quantile}(1-\alpha, \{s_i\}) $$
CP は「validation set のサイズ $n_{\text{cal}}$ が十分なら、 (1-α)% カバレッジが厳密に保証される」という非常に強い性質を持つ。 SSDSE 規模では cal=100 件で 90% カバレッジが安定する。 検証データが「ハイパラ調整」だけでなく「不確実性定量化」にも使えることを示す例。
SSDSE-B-2026 47 件で「LinearRegression」を 100 通りの random_state で評価し、 (1) 単一 holdout 80/20 と (2) 5-fold CV の結果分布を比較した:
| 評価法 | 平均 R² | 標準偏差 | 95% 範囲 | 最悪値 |
|---|---|---|---|---|
| 単一 holdout (80/20) | 0.9641 | 0.0521 | [0.812, 0.999] | 0.759 |
| 5-fold CV | 0.9674 | 0.0200 | [0.914, 0.988] | 0.897 |
| 10-fold CV | 0.9245 | 0.0358 | [0.828, 0.973] | 0.785 |
| LOOCV (OOF 予測を合算した R²) | 0.9898 | — (決定的) | — | — |
観察 1: holdout と 5-fold の平均はほぼ同じ (0.96 台) で、 バイアスに大差なし。
観察 2: 標準偏差は holdout 0.052 vs 5-fold 0.020 で 2.6 倍違う。 5-fold CV の方がはるかに安定。
観察 3: 最悪ケース (worst random_state) で holdout 0.759、 5-fold 0.897。 holdout は「運悪く外れ値が val に集中」すると壊滅的に低い値を返す。
観察 4: 10-fold は val が 4〜5 件しかなく fold ごとの R² が不安定になり、 平均 (0.925) も SD (0.036) も 5-fold より悪化。 LOOCV は val 1 件で fold 単位の R² が定義できないため、 OOF (out-of-fold) 予測を合算した単一の R² (0.990) で報告する。 SSDSE 47 件規模で fold 平均 R² を報告するなら k=5 が現実的。
🎯 このコードでやること: sklearn の BaseCrossValidator を継承して、 SSDSE-B-2026 の「年単位」分割を自作。 「2012〜2019 で学習、 2020〜2023 で検証」のような業務要件にも対応できる。
📥 入力データ: SSDSE-B-2026 全 12 年 × 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 | import pandas as pd import numpy as np from sklearn.model_selection import BaseCrossValidator from sklearn.linear_model import LinearRegression from sklearn.model_selection import cross_val_score df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df.sort_values('SSDSE-B-2026').reset_index(drop=True) X = df[['A1101']].values y = df['A4101'].values years = df['SSDSE-B-2026'].values class YearBlockSplit(BaseCrossValidator): """年ブロックを fold として分割""" def __init__(self, year_groups): self.year_groups = year_groups # list of (train_years, val_years) def get_n_splits(self, X=None, y=None, groups=None): return len(self.year_groups) def split(self, X, y=None, groups=None): for tr_years, va_years in self.year_groups: tr_idx = np.where(np.isin(years, tr_years))[0] va_idx = np.where(np.isin(years, va_years))[0] yield tr_idx, va_idx # 「3 年で学んで翌 1 年で検証」を 3 段階 splits = [ ([2012, 2013, 2014], [2015]), ([2015, 2016, 2017], [2018]), ([2018, 2019, 2020], [2021]), ] cv = YearBlockSplit(splits) scores = cross_val_score(LinearRegression(), X, y, cv=cv, scoring='r2') print(f'年ブロック CV: R² = {scores}') print(f'平均: {scores.mean():.4f} ± {scores.std():.4f}') |
📤 実行すると次の出力が得られる:
💬 結果の読み方: 業務シナリオに沿った「3 年学んで翌年予測」の精度を直接測れる。 標準 sklearn には無い CV ロジックも 20 行で書ける。 BaseCrossValidator は要件適応の汎用性が高い。 各社の業務要件 (「過去 6 ヶ月で学習、 直近 1 ヶ月で検証」等) に合わせて自作する習慣が望ましい。
「モデル A の CV スコア 0.85、 モデル B が 0.83」 → 本当に A が良いか? 検証データのスコア差は確率変数なので、 統計検定で評価:
🎯 このコードでやること: SSDSE で LinearRegression と Ridge を比較し、 paired t-test で有意差を検定。
📥 入力データ: SSDSE-B-2026 47 県、 X=人口、 y=出生数。
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 | import pandas as pd import numpy as np from sklearn.model_selection import KFold, cross_val_score from sklearn.linear_model import LinearRegression, Ridge from scipy.stats import ttest_rel, wilcoxon df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) d = df[df['SSDSE-B-2026']==2023] X = d[['A1101']].values y = d['A4101'].values kf = KFold(n_splits=10, shuffle=True, random_state=42) scores_lr = cross_val_score(LinearRegression(), X, y, cv=kf, scoring='r2') scores_rg = cross_val_score(Ridge(alpha=10.0), X, y, cv=kf, scoring='r2') print(f'LinearRegression: {scores_lr.mean():.4f} ± {scores_lr.std():.4f}') print(f'Ridge (α=10): {scores_rg.mean():.4f} ± {scores_rg.std():.4f}') t_stat, p_val = ttest_rel(scores_lr, scores_rg) print(f'\\nPaired t-test: t = {t_stat:.4f}, p = {p_val:.4f}') w_stat, w_p = wilcoxon(scores_lr, scores_rg) print(f'Wilcoxon: W = {w_stat:.4f}, p = {w_p:.4f}') if p_val < 0.05: print('\\n→ p < 0.05: 有意差あり、 モデル差は本物') else: print('\\n→ p ≥ 0.05: 有意差なし、 偶然の範囲') |
📤 実行すると次の出力が得られる:
💬 結果の読み方: 両者の平均差はほぼ 0 で統計的に有意でない (p=0.24)。 47 件を 10-fold にすると val は 4〜5 件しかなく、 fold ごとの R² 自体も不安定(SD 0.107)。 「LinearRegression が良かった」と報告しても、 別の random_state では Ridge が勝つかもしれない。 SSDSE 47 件規模で「モデル間差 1% 以下」を主張するには statistical power が不足。 こうした場合は「両者ほぼ同等、 計算が軽い LinearRegression を採用」のように述べる方が誠実。
モデルがプロダクションに乗ったあとの「継続的バリデーション」も重要。 Concept Drift / Data Drift が起きると CV で測った精度は維持されない。
| 状況 | 解釈 | 次の手 |
|---|---|---|
| train 高、 val 低 | 過学習 | 正則化、 データ拡張、 モデル単純化 |
| train 低、 val 低 | 過小学習 (underfitting) | モデル複雑化、 特徴量追加、 学習時間延長 |
| train ≈ val、 共に高 | 理想的 | test 評価へ進む |
| fold 間の分散大 | データ不安定 or k 小 | k を増やす、 RepeatedKFold |
| val 高、 test 低 | val set overfitting | Nested CV、 ハイパラ試行回数削減 |
| 時系列で val 低 | 分布シフト | 最近データに重み付け、 オンライン学習 |
k-fold CV の汎化誤差推定 $\hat{\text{Err}}_{CV}$ は次の分解を持つ:
$$ \text{MSE}(\hat{\text{Err}}_{CV}) = \underbrace{(\text{Bias})^2}_{\text{過大評価}} + \underbrace{\text{Var}}_{\text{ブレ}} $$
バイアス: k 小では train セットが小さく真の汎化誤差を過大評価。 k=N (LOOCV) ならバイアス最小。
分散: k 大では fold が相関を持ち、 結果として分散も大。 k=N では fold N-1 個が train を共有するため分散最大。
最適 k はバイアスと分散のトレードオフ。 Kohavi (1995) の経験的研究では「k=10 が多くのケースで MSE 最小」と結論。
train は「弁護士が依頼人とリハーサルした証言の練習」、 val は「裁判所近くで弁護士同士の予行演習」、 test は「実際の裁判での証言」。 リハーサルでうまく行っても予行演習でつまずけば、 戦略を変える(ハイパラ調整)。 ただし「予行演習を 100 回繰り返して特定の質問に最適化」しすぎると、 本番で別の質問が来た時に失敗する(val set overfitting)。 本番は 1 回切りなので、 リハーサル + 予行演習で「想定外」への耐性を作る必要がある。
| 種類 | 使用頻度 | 用途 | 例 |
|---|---|---|---|
| Validation | 数十回〜数百回 | ハイパラ調整、 モデル選択 | CV の fold |
| Test (Holdout) | 最後に 1 回 | 最終性能の報告 | Kaggle Private LB |
| Benchmark | 業界横断 | 他研究との公正比較 | ImageNet, GLUE, SuperGLUE |
| External Holdout | 他組織から提供 | 独立性検証 | 医療 AI の他施設データ |
| Production Sample | 継続的 | 本番デプロイ後の精度モニタ | A/B test サンプル |
| Calibration | 1 回 | 確率予測の校正 | Platt scaling 用 |
| N | 推奨 k | 理由 |
|---|---|---|
| 50 以下 | LOOCV (k=N) | バイアス最小化優先 |
| 50〜500 | 10-fold CV | バイアス・分散 OK |
| 500〜10000 | 5-fold CV | 計算量と精度のバランス |
| 10000〜100000 | 3-fold or holdout 80/10/10 | CV の必要性低下 |
| 100000 以上 | 単一 holdout 90/5/5 | val/test だけで SE 十分小 |
「val で 100 通りのハイパラを試して最良を選び、 その 1 つを test で評価する」のは一見正しいが、 ハイパラ空間が広いと val 上で最良のものは「val の偶然のパターンに当たった」可能性が高く、 test では落ちる。 一般に test スコア $<$ val 最高スコア となるバイアスがある。
対策:
汎化誤差 $\text{Err}(f) = \mathbb{E}_{(x,y) \sim P}[\ell(f(x), y)]$ を考える。 これは「真の分布 $P$ から無限にサンプリングした時の期待損失」。 現実には $P$ は不明なので、 有限サンプル $\mathcal{D}_{\text{test}}$ で近似:
$$ \widehat{\text{Err}}(f) = \frac{1}{|\mathcal{D}_{\text{test}}|} \sum_{(x_i,y_i) \in \mathcal{D}_{\text{test}}} \ell(f(x_i), y_i) $$
$\widehat{\text{Err}}(f)$ の不偏推定であるためには、 test set が「学習に一切使われていない」「真の分布 P と同じ分布から独立サンプリング」の 2 条件が必須。 ここで validation set を test として使ってしまうと、 ハイパラ選択を介して間接的に学習に使われており、 第 1 条件が破られて評価が楽観的になる。
🍰 まずはやさしく
モデルを調整するための練習問題です。
一番良い設定を見つけるために使います。
部活で練習メニューを試す感覚です。
まずは結論から短くまとめます。
検証データ(Validation Data):ハイパーパラメータ調整・モデル選択に使う、 訓練データから切り分けた評価用データ
🍰 まずはやさしく
検証データの解説ページです。
機械学習の基本を学ぶために使います。
都道府県のデータを使って学びます。
おすすめの読み方を紹介します。
このページは「検証データ(Validation Data)」の用語解説です。 機械学習の基礎カテゴリにおける重要概念で、 機械学習の基礎 グループ教材の中で繰り返し登場します。 数式・実コード・落とし穴を 1 ページに集約し、 SSDSE-B-2026 都道府県データ(47 件 × 112 列)を題材に 手を動かしながら理解できるよう構成しています。
別称:Validation Set / Dev Set。 まず 💡 30秒結論 で全体像を、 次に 🎨 直感 → 📐 数式 → 🧮 実値 → 🐍 Python の順で読むのがおすすめ。
🍰 まずはやさしく
モデル選びのための模試のようなものです。
勉強のやり方が正しいか確かめます。
テスト前に模試で弱点を探す感覚です。
なぜデータを分けるのかを解説します。
検証データ(validation data)は「モデル選びの試験会場」です。 訓練データ(教科書)で勉強し、 検証データ(模試)で「どの参考書(モデル)が良いか」「何時間勉強すれば足りるか(ハイパーパラメータ)」を決め、 テストデータ(本試験)で最終評価する 3 段階の 役割分担がポイントです。
なぜ 3 つに分けるのか? 訓練データだけでハイパーパラメータを決めると 過学習する(モデルが訓練データを暗記)。 テストデータは「最終評価専用」で途中で見てはいけない(見ると テストへのリークが発生し、 本番性能を過大評価する)。 そこで 第 3 のデータとして検証データを切り出します。
用途 3 つ:
max_depth(3, 5, 7, 10, None)、 学習率(0.001, 0.01, 0.1)、 正則化強度(α=0.001〜10)など。 訓練で学習 → 検証で評価 → 最高性能の値を採用。SSDSE-B-2026 47 都道府県の例: 実在列 A1101(人口)や A1303 から A4101(出生数)を予測する場合、 訓練 27 県(約 60%)、 検証 10 県(約 20%)、 テスト 10 県(約 20%)に分ける。 検証データで RandomForest の max_depth=2,3,4,5,None を試し、 検証精度が最良の max_depth を採用。 テスト 10 県で最終評価する。
検証データへ「答えを合わせ続ける」と過学習する: 1 万通りのハイパーパラメータを試して検証データ最良を選ぶと、 検証データに 偶然合った設定を引き当てるリスクが上がる(多重比較問題)。 多数試行する場合は k 分割交差検証(k-fold CV)を併用し、 検証 fold を交代しながら平均評価する。 さらに「最終評価」用にテストデータを完全に隔離する。
分割比率の経験則: 標準は 60:20:20、 データ大量(10 万件超)なら 80:10:10。 SSDSE-B-2026 のように 47 件しかない場合は単一分割では不安定なので、 訓練 + 検証 を k=5 で交差検証し、 テスト 10 県だけ最終評価用に隔離する 2 段階構成が現実解。
🍰 まずはやさしく
データの分け方を決めるルールです。
正しく評価するために割合を決めます。
スマホのアプリ開発などの基準になります。
具体的な分け方の数字を見ていきましょう。
本概念は次のように記述されます(KaTeX で描画)。
英語名 Validation Data。 別称:Validation Set / Dev Set。
記号と意味を逐一突き合わせて読みます。 慣れないうちは式を「日本語で読む」ことが理解の近道です。
SSDSE-B の 47 都道府県データを 訓練 60% / 検証 20% / テスト 20% に分割し、 RandomForest の max_depth を検証データで選びます。
データ出典:SSDSE-B-2026(独立行政法人統計センター)。 47 都道府県 × 複数年(最新 2023)の社会統計データ。
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 | import pandas as pd import numpy as np from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import accuracy_score df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df[df['SSDSE-B-2026'] == 2023].reset_index(drop=True) df['ratio'] = df['A1303'] / df['A1101'] df['label'] = (df['A4101'] >= df['A4101'].median()).astype(int) X = df[['A1101','A1303']].values y = df['label'].values # 1段目: 訓練80% / テスト20% X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, random_state=0, stratify=y) # 2段目: 訓練の中で 訓練75% / 検証25% → 全体だと 60/20/20 X_train, X_val, y_train, y_val = train_test_split(X_tr, y_tr, test_size=0.25, random_state=0, stratify=y_tr) best_d, best_acc = None, -1 for d in [2, 3, 4, 5, None]: m = RandomForestClassifier(max_depth=d, random_state=0).fit(X_train, y_train) acc = accuracy_score(y_val, m.predict(X_val)) print(f'max_depth={d}: val_acc={acc:.3f}') if acc > best_acc: best_acc, best_d = acc, d # 最終評価 final = RandomForestClassifier(max_depth=best_d, random_state=0).fit(X_tr, y_tr) print(f'best_depth={best_d}, test_acc={accuracy_score(y_te, final.predict(X_te)):.3f}') |
実行結果の要約(sklearn で実測。 検証は 10 県のみのため精度は 0.1 刻みでしか動かず、 max_depth 間で差が出ない=小さな検証セットの限界を示す):
| 項目 | 値 |
|---|---|
| 訓練 | 27 県 (約 60%) |
| 検証 | 10 県 (約 20%) |
| テスト | 10 県 (約 20%) |
| max_depth=2 val | 0.800 (best) |
| max_depth=3 val | 0.800 |
| テスト精度 | 1.000 |
検証データの作り方には 2 つの分かれ道がある。 (1) train / val / test を何 % ずつにするか、 (2) hyperparameter 探索が走るときに leakage を起こさず汎化を測る nested cross-validation を使うか。 SSDSE-B-2026 (n=47) のような小サンプルでは、 単純な 6:2:2 分割は 各分割が 10 件未満になり、 分散が爆発する。 ここでは nested 5-fold CV が現実的な唯一解になる。
| 比率 / 戦略 | 想定 n | SSDSE-B-2026 で使えるか | 注意点 |
|---|---|---|---|
| 8:1:1 単純分割 | 10,000+ | × | val=5, test=5 件では信頼区間が ±20% を超える |
| 7:2:1 | 1,000+ | △ | test=5 が地域偏在しないよう stratify 必須 |
| 5-fold CV (val=test 兼用) | 30+ | ○ | hyperparameter 探索を含めると optimistic bias |
| Nested 5×5 CV | 30+ | ◎ | 外側 5-fold で test、 内側 5-fold で hyperparameter 選択 → leakage 0 |
| LOOCV | 10+ | ○ | 計算は 47 回、 高分散だが不偏推定に近い |
数式を言葉で読み解く: nested CV の汎化誤差 $\hat{E}_{\text{nested}} = \frac{1}{K}\sum_{k=1}^{K} L\bigl(f_{\theta^*_k}(\mathbf{x}^{(k)}_{\text{test}}), y^{(k)}_{\text{test}}\bigr)$ ── ここで $\theta^*_k$ は外側 fold $k$ の 内側 CV で選ばれた最良 hyperparameter。 外側 test data は内側探索を 一度も見ていないため、 hyperparameter 選択経由の leakage が遮断される。
🎯 このコードでやること: SSDSE-B-2026 で人口 → 出生数を予測する Ridge 回帰の正則化強度 $\alpha$ を、 nested 5×5 CV で leakage-free に選ぶ。
📥 入力: SSDSE-B-2026 の A1101 (人口) と A4101 (出生数) を含む 47 行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import pandas as pd from sklearn.linear_model import Ridge from sklearn.model_selection import KFold, GridSearchCV, cross_val_score df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df[df['SSDSE-B-2026'] == 2023] # 最新年で 47 県 X = df[['A1101']].values y = df['A4101'].values inner = KFold(n_splits=5, shuffle=True, random_state=0) outer = KFold(n_splits=5, shuffle=True, random_state=1) grid = GridSearchCV(Ridge(), {'alpha': [0.01, 0.1, 1, 10, 100]}, cv=inner, scoring='r2') scores = cross_val_score(grid, X, y, cv=outer, scoring='r2') print(f"nested CV R² mean = {scores.mean():.3f}, sd = {scores.std():.3f}") |
📤 実行結果:
💬 結果の読み方: 単純 5-fold は α 選択と test を同じ fold で使い回すため オプティミスティック・バイアスが乗り得るが、 ここでは人口 1 特徴量の線形モデルなので差はほぼ 0。 ハイパーパラメータ空間が広いモデル (RandomForest 等) や複雑なデータでは差が数 % に広がるため、 「nested CV か LOOCV を選ぶ」のが安全。
StandardScaler.fit_transform を先に当てると val/test の平均・分散が train に漏れる。 Pipeline 内に scaler を入れ、 各 fold で fit検証データは「過学習を検知する鏡」です。 学習曲線・残差・正則化曲線という3つの図で、 検証データが活きる場面を確認します。
3点の図は「過学習検知」「分布シフト診断」「ハイパーパラメータ選択」という検証データの3大用途を可視化しています。
合成 1000 件で train/val/test を計算する。
| set | 比 | 件数 |
|---|---|---|
| train | 0.6 | 600 |
| val | 0.2 | 200 |
| test | 0.2 | 200 |
1 2 3 4 5 | N = 1000 train = int(N * 0.6) val = int(N * 0.2) test = N - train - val print(f"train={train}, val={val}, test={test}") |
💬 手計算 (Step 1) と Python 出力が完全一致。
scikit-learn / pandas を使った最小実装パターン。 上の SSDSE-B 計算と同じスタイルですが、 ここでは「読み込み→前処理→学習→評価」のテンプレを 4 つのスニペットに分けます。
1 2 3 4 | import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df[df['SSDSE-B-2026'] == 2023].reset_index(drop=True) print(df.shape, df.columns.tolist()[:8]) |
1 2 3 | X = df[['A1101','A1303']].values y = df['A4101'].values print('X shape =', X.shape, ', y shape =', y.shape) |
1 2 3 4 5 6 | from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestRegressor X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.3, random_state=0) model = RandomForestRegressor(n_estimators=300, random_state=0).fit(X_tr, y_tr) print('R^2 (test) =', model.score(X_te, y_te)) |
1 2 3 4 5 6 | import matplotlib.pyplot as plt pred = model.predict(X_te) plt.scatter(y_te, pred) plt.plot([y_te.min(), y_te.max()], [y_te.min(), y_te.max()], 'r--') plt.xlabel('実測'); plt.ylabel('予測'); plt.title('「検証データ」関連モデルの予測精度') plt.tight_layout(); plt.savefig('out.png', dpi=150) |
※ 「検証データ」固有の本格コードは上の 🧮 SSDSE-B 実値計算 節を参照。
検証データ (validation data) は「訓練データで学習したモデルがどれだけ汎化するか」を測るための専用データセットで、 機械学習・統計学習の精度評価とハイパーパラメータ調整の要です。 しかし分割方法・サイズ・運用ルールを誤ると、 「検証スコアは高いのに本番で大失敗」という事態を招きます。 SSDSE-B-2026 を題材に、 適用条件・限界・誤解回避を整理します。
√(p(1-p)/n) 程度。 n=10 だと精度 10% でも標準誤差 9% と暴れます。 最低 200-500 件は確保したいですが、 SSDSE のような小標本では交差検証で代替します。検証データは「モデルの本当の実力」を測る唯一の手段ですが、 設計を誤ると逆に誤った安心感を生みます。 SSDSE-B-2026 のような小標本で「三分割 → CV → 別 hold-out」の規律を身につけ、 本番運用前に必ず分布シフト診断を組み込みましょう。
「訓練 80%、 検証 10%、 テスト 10%」のような経験則はよく聞きますが、 SSDSE-B-2026 のような小標本 (N=47) でこのまま適用すると検証 4-5 件・テスト 4-5 件となり、 評価精度が極端に低下します。 サンプル数 n、 検証スコアの標準誤差 SE は二値分類精度 p の場合 SE = √(p(1-p)/n) で近似でき、 SE を 0.05 以下に抑えたいなら最低 100 件が必要、 0.01 以下なら 2,500 件が必要です。 SSDSE のように N が小さいときは hold-out 検証ではなく 5-fold/10-fold CV や RepeatedKFold で「データを使い切る」設計が必須。 一方 N が 100 万を超える大規模データなら、 計算コストの観点から hold-out 比率を 1-2% に絞っても統計的に十分なケースがあります。 Andrew Ng は「データが膨大なときは検証もテストも 1% で 1 万件あれば足りる」と提案しています。 SSDSE と業務データを横断するときは、 まず N と必要精度から SE を逆算し、 そこから分割比を決めるのが筋の通った設計です。
教師あり学習における検証データは「予測精度の信頼区間」を提供するためのものですが、 因果推論や A/B テストの世界では「介入後の母集団から取得したサンプル」が同じ役割を果たします。 SSDSE-B-2026 のような観察データで予測モデルを作る場合は、 検証データに「介入前後の比較対象」を組み込むことで、 単なる予測精度を超えた「政策効果の検証」が可能になります。 例えば「ある県で出生支援政策を実施→その県だけ別 hold-out として残し、 全国モデルで予測した値との差分を測る」というアプローチは、 検証データを準実験 (quasi-experiment) として使う典型例です。 こうした使い方では、 検証データのサイズだけでなく「介入群と対照群の分布の同等性 (balance check)」も同時に確認します。 propensity score matching, SMD (Standardized Mean Difference), Love plot などのツールが定石です。 SSDSE 教材レベルでは概念的な接続として押さえ、 業務ではより厳密な因果推論ライブラリ (CausalML, DoWhy, EconML) を参照してください。
検証データの設計は「どの指標で評価するか」と表裏一体です。 回帰なら RMSE/MAE/MAPE/R²、 二値分類なら accuracy/precision/recall/F1/ROC-AUC/PR-AUC、 多クラスなら macro-F1/weighted-F1、 ランキングなら NDCG/MAP、 確率予測なら log-loss/Brier score など、 タスクごとに複数指標を併用します。 SSDSE-B-2026 で都道府県の総人口を予測するなら RMSE と R² の両方を見て「絶対誤差の大きさ」と「説明力の高さ」を分けて評価。 異常検知では precision-recall trade-off が支配的で、 ROC-AUC だけでは陽性率が低い場面の性能を見誤ります。 検証データのサイズが小さいと「平均値±信頼区間」だけでなく fold ごとの結果分布も併記し、 boxplot や violin plot で可視化するのが定石です。 ビジネス指標 (売上・コスト・顧客満足度) との対応を明示し、 「RMSE 1.0 改善 = 月次売上 100 万円改善」のような橋渡しを必ず作りましょう。
検証データは「結果を再現できる」状態で管理するのが MLOps の基本です。 具体的には、 (a) random_state を全 random/numpy/sklearn/torch で固定、 (b) データ前処理の version を git/DVC で追跡、 (c) 環境を Docker/conda env で固定、 (d) ハードウェア依存 (GPU 演算順序) もログ化、 という 4 点を押さえます。 SSDSE-B-2026 の取り込みスクリプトも、 同じ CSV を読み込めば必ず同じ train/val/test 分割になるよう設計し、 git commit hash と分割ファイル名を組み合わせて参照可能にします。 こうしておくと、 半年後に「あのモデル、 もう一度評価したい」と言われても 5 分で再現できます。
大学の演習や卒業研究で検証データを扱うときは、 学生ごとに同じ train/val/test 分割を共有する運用が大切です。 SSDSE-B-2026 を題材にしたゼミなら、 GitHub Classroom と DVC でデータ分割を版管理し、 全員が同じ random_state を使うことで「誰のモデルが本当に強いか」をフェアに比較できます。 学生個別に分割を変えると、 評価値の差が「モデルの差」ではなく「分割の運」になってしまう典型的な失敗を防げます。 教員側は、 採点用テストデータを別途隠して持ち、 提出物に対して 1 回だけ評価する運用にすると、 testing optimism を構造的に排除できます。
小標本の検証データを何度もハイパラ探索に使うと「検証スコアへの過適合 (overfitting to validation set)」が起こります。 これを防ぐ最良の方法はテストデータを完全に分離して触らないこと、 そして検証データに触る回数自体を log として残すことです。 SSDSE-B-2026 のような小規模教材でも、 ハイパラ探索の試行数を 50 以下に抑える、 別の hold-out (例: 別年度データ) を最終評価に使う、 報告時に「検証回数: 30 試行」と明記する、 といった習慣を付けると、 数値だけでなく評価過程の信頼性が高まります。 こうした規律はとくに kaggle や論文投稿のような「スコアが順位を決める場」で重要で、 Public LB に過適合させて Private LB で順位が大きく落ちる現象を防ぐ核心的なルールです。
検証データの「リーク」は最も発見しにくく、 かつ最も致命的な失敗です。 SSDSE-B-2026 のような小規模データでも次のチェックを習慣化しましょう。 (1) 前処理 (標準化・欠損補完・カテゴリエンコード) は Pipeline で fold/分割内に閉じる、 (2) 目的変数の関数 (例: 翌月平均) を特徴量に含めない、 (3) ID 列や時刻列を特徴量から除外、 (4) train/val/test の重複行を df.duplicated() で検査、 (5) target encoding は out-of-fold で計算、 (6) 時系列では未来の情報を特徴量にしない (cutoff time の厳守)、 (7) 画像/医療データは患者・撮影日でグループ化、 (8) 大規模では hash(id) % k で再現性ある分割を採用。 これらを CI に組み込んで「リーク検査が通らないと merge できない」運用にすると、 検証スコアの楽観バイアスを構造的に防げます。
実務では「検証データを 1 回作って終わり」ではなく、 モデルバージョンごと・データバージョンごとに検証データセットを版管理する MLOps の運用が広がっています。 DVC (Data Version Control) や MLflow Model Registry を使うと、 各モデルがどの検証データで何点を出したかが完全に追跡可能になります。 SSDSE-B-2026 を毎年更新するときも、 「2026 年版 train / 2026 年版 val / 2025 年版 hold-out」とラベルを付け、 モデル間比較を行う基盤を整えると、 翌年以降の改善議論がスムーズです。 また monitoring の観点では、 本番デプロイ後に新たな入力データの分布シフトを検知する「監視用検証データ」を別途用意し、 PSI (Population Stability Index) や KL divergence で日次・週次にチェックする運用が一般的です。 検証データは静的な「テスト用ファイル」ではなく、 ライフサイクル全体で更新・監査される動的な資産だと捉え直しましょう。
検証データを正しく分割しても、 評価指標がタスクと合っていなければ意味がありません。 SSDSE-B-2026 で都道府県の高齢化率を予測する回帰タスクなら、 RMSE / MAE / 決定係数 R² を併記すべきで、 例えば RMSE=2.3 ポイント、 MAE=1.8 ポイント、 R²=0.84 のように 3 つを並べて報告すると、 外れ値の影響度と全体当てはまりが同時に分かります。 一方、 「ある県を高齢化率上位 10 に入るか」を判定する分類タスクに切り替えると、 検証データ上での評価は accuracy だけでは不足で、 precision / recall / F1 / ROC-AUC / PR-AUC を全て見るのが標準です。 評価指標の選択を誤ると、 検証データ自体は適切でも「良いモデルを取り違える」結果になります。
検証データ分割は乱数依存なので、 そのままだと「先週のスコアと今週のスコアが違う理由は乱数か特徴量か」が切り分けられません。 scikit-learn では train_test_split(X, y, random_state=42) や KFold(n_splits=5, shuffle=True, random_state=42) のように random_state を必ず固定します。 さらに堅牢性を上げるには、 異なる random_state を 5〜10 個試して検証スコアの平均と標準偏差を報告する「乱数頑健性チェック」を行います。 SSDSE-B-2026 は 47 件しかないので、 分割の乱数で性能が ±2 ポイント揺れることが珍しくありません。 こうしたばらつきを認識してこそ、 「検証スコア 84.0 と 83.7 のモデルのうち、 どちらが本当に優れているか」の判断が可能になります。
検証データの件数が少ないと、 検証スコア自体の信頼区間が広くなり、 モデル比較の結論が不安定になります。 二項分類の accuracy の信頼区間幅は概ね 1/√(検証件数) に比例するので、 検証データ 47 件で accuracy 80% を観測した場合、 95% 信頼区間は約 [68%, 89%] と非常に広く、 「77% のモデルと有意に違うか」を判定するのは困難です。 こうしたケースでは検証 1 回ではなく、 5-fold CV を 10 回繰り返す Repeated K-Fold、 あるいは Nested CV による不偏推定を採用し、 報告時には平均だけでなく標準偏差・95% 信頼区間を明示することが必須です。 SSDSE-B-2026 のような小規模教育データではこの「小サンプル検証の不安定性」を意識的に学び、 報告の言い回しを「84.0 ± 1.2」のように揺らぎ付きで書く習慣を付けましょう。
random_state を固定する理由は? (答:分割の乱数を再現可能にし、 性能差が特徴量改良か乱数かを切り分けるため)6 問中 5 問以上正解で「検証データ運用の落とし穴を理解した」と自己評価できる。 5 問未満ならリーク防止チェックリストと信頼性の節を再読する。
検証データ分割は「とりあえず 8:2」で済ませてはいけません。 タスクの性質・データのサイズ・時系列性・グループ構造を踏まえ、 次の意思決定フローで分割方法を選びます。 (1) サンプル数が 1 万を超え、 i.i.d. を仮定できる:単純 hold-out (train 60% / val 20% / test 20%) で十分。 (2) サンプル数が 1000〜1 万、 i.i.d.:5-fold CV + 別途 hold-out test。 (3) サンプル数が 1000 未満、 i.i.d.:Nested CV か Repeated K-Fold で乱数頑健性を確保。 (4) 時系列データ:未来情報リークを防ぐため TimeSeriesSplit を使い、 過去で学習・未来で評価。 (5) 患者・店舗・撮影日などグループ構造あり:GroupKFold で同一グループが train/val をまたがないようにする。 SSDSE-B-2026 は 47 件の i.i.d. 想定なので、 5-fold CV を 10 回繰り返す Repeated K-Fold が標準解です。 この意思決定フローを意識すると「なぜこの分割を選んだか」をレポートに書ける段になり、 評価の妥当性が説明できるようになります。
陽性率 1% のような不均衡データを単純 K-fold で分割すると、 fold によっては陽性が 0 件になり評価不能になります。 これを避けるのが StratifiedKFold で、 各 fold での陽性比率を全体と同じに保ちます。 scikit-learn では StratifiedKFold(n_splits=5, shuffle=True, random_state=42) と書くだけ。 不均衡が極端 (1% 未満) なら、 さらに RepeatedStratifiedKFold で複数回繰り返してスコアを平均すると安定します。 SSDSE-B-2026 で「人口減少県 vs 増加県」のような二値分類を行う場合も、 必ず層化分割を採用しましょう。
「validation data」を中心とした関連概念マップ。
検証データ (val set) の中心から、 訓練データ・テストデータ と並列、 ハイパラ調整・Early Stopping・モデル選択 が下流に並ぶ。 SSDSE-B-2026 (47 県 × 12 年) を 60/20/20 で分けると val = 113 行で、 ここでの精度を見てハイパラを決め、 test には触れない、 が鉄則。
検証データは「学習中の調整役」で、 上流の分割設計と下流のチューニングをつなぐ。
SSDSE-B-2026 で人口予測モデルを LightGBM で組む場合、 val MAE が最小になる num_leaves を Optuna で 100 trial 探索 → test で 1 回だけ評価、 という pipeline が典型。
検証データをどう確保するかは、 データ量とタスクの性質で 4 通りに分岐する。
val を test 代わりに使い回す (最良 val 精度を最終報告) のは典型的な禁忌。 val は探索用、 test は 1 回限りの公正評価用、 と役割を厳密に分けるのが正しい運用。
合成データ(真の関数は 3 次多項式+ノイズ)を 訓練 60% ・ 検証 20% ・ テスト 20% に分割しました。 次数スライダーを動かすと、 多項式回帰の当てはめ曲線・訓練誤差・検証誤差がその場で再計算されます。 検証誤差が最小になる次数を選ぶのがモデル選択、 その後 テストで 1 回だけ最終性能を確かめるのが正しい手順です。 ここでの数値はブラウザ内で最小二乗法をその場計算した合成データで、 SSDSE の実測値ではありません。
検証データは、 パラメータ(回帰係数)を学ぶ訓練データと、 最終成績を測るテストデータのあいだにある「調整専用の中間データ」です。 モデルが自分で学べない設定値(次数・正則化強度・木の深さなど=ハイパーパラメータ)を、 訓練データを見ずに「未知データでの成績」で選ぶ役目を担います。 上の②のグラフで谷を探す行為そのものが、 検証データによるハイパーパラメータ選択です。
検証データを 1 つ固定すると、 その分割の「引き」で結果が揺れます。 交差検証(k-fold CV)は、 データを k 分割して「各回で 1 つを検証・残りで訓練」を k 回繰り返し、 検証誤差を平均します。 上の②の赤い曲線を「k 本の平均」にするイメージで、 谷の位置が安定します。 ただしハイパラ選択と最終評価に同じ CV を使い回すと、 やはり楽観バイアスが残ります。 これを断ち切るのが ネスト CV で、 外側 CV でテスト評価・内側 CV でハイパラ選択と役割を二重に分離します。 テストでの調整を避ける原則の詳細は データリーク、 複雑さと誤差の関係は バイアス・バリアンス、 選ぶ設定値そのものは ハイパーパラメータ調整 を参照してください。
ここは既存の解説を壊さずに追記した発展セクションです。 姉妹ページ(検証/訓練データ/交差検証)と重複しない独自角度として、 「検証スコアという数字を、どこまで信じてよいか」という一点に絞って掘り下げます。 結論を先に言うと──検証スコアの絶対値(水準)は当てにならないことが多いが、候補を並べ替える順位づけは比較的頼れる、これが検証データの実務的な使い方です。
検証データは「本番性能を測る物差し」ではなく、候補を並べ替える“ふるい”だと捉えると誤解が減ります。 模試に例えるなら、返ってきた絶対点はその日の運・問題の難易で大きくブレるけれど、 「国語より数学が得意」という科目間の順位は比較的安定します。 検証データも同じで、少数の検証サンプルだと R² や誤差の水準は本番から大きくズレ得る一方、 「どのモデル・どのハイパラが相対的に良いか」の順位は、水準ほどには壊れません。
これは data/raw/SSDSE-B-2026.csv の実測でも確認できます。 2023 年 47 都道府県(df[df['SSDSE-B-2026']==2023]、read_csv(encoding='cp932', skiprows=[1]))で A1101(総人口)を残り数値列から予測し、 train_test_split(random_state=42) で 60/20/20 に分割(訓練 28・検証 9・テスト 10 県)。 RandomForest の max_depth を 5 通り試した実測値が下表です。
| max_depth | 検証 R²(n=9) | テスト R²(n=10) |
|---|---|---|
| 2 | 0.6373 | 0.9175 |
| 3 | 0.6418 | 0.9215 |
| 4 | 0.6430 | 0.9234 |
| 5 | 0.6521 | 0.9282 |
| None | 0.6482 | 0.9275 |
ここが要点です。 検証 R² の水準(約 0.64〜0.65)は、テスト R²(約 0.92〜0.93)を約 0.28 も過小評価しています。 検証 9 県の顔ぶれ(random_state=42 の“引き”)がたまたま予測しにくい県を多く含んだためで、 これは実力ではなく分割ノイズです。 それでも順位は保たれ、検証で最良の max_depth=5 は、テストで最良の設定とも一致しました(テスト R²=0.9282)。 つまり「水準は信じるな、順位は使え」が実データで成立しています。
(1) 検証スコアの絶対値を「本番の実力」として報告する。 上表の通り、検証 R²=0.65 を成績として書くと本番(0.93)を大きく外します。 逆に検証が“当たり”の県を引けば過大評価もあり得ます。 n が小さいほど水準の信頼区間は広い。報告するなら順位・相対差、または交差検証の平均±分散にする。
(2) 選択回数を無管理に増やす=検証集合そのものへの過学習(勝者の呪い)。 同じ検証データで何百通りも試して最良を拾うと、 その検証集合固有のノイズに張り付いた設定を引き当てます。 これは検証データが事実上の第2のテストになり、最終評価用テストとの乖離を生みます(=データリークの一種)。
(3) 検証で選んだ設定の「検証スコア」を最終成績にする。 選択に使った集合での得点は、定義上楽観方向に偏る。 選択後は必ず、選択に一切使っていないテストデータで1度だけ確認する。
【架空データによるデモ:勝者の呪いを純ノイズで可視化】 以下は SSDSE ではなく、 目的変数と予測が完全に無相関(真の予測力=ゼロ)な架空データです(シード 20260614)。 検証 20 点・テスト 200 点を固定し、当てずっぽうの予測を 1000 通り生成して検証相関が最大の1つを選びます。
結果: 勝者の 検証相関 = 0.599(真値ゼロなのに高く見える=選び過ぎによる幻)、 同じ勝者の テスト相関 = −0.011(≒ゼロ、実力どおりに崩壊)。 検証集合を「何度も見て一番良いのを拾う」と、水準はいくらでも盛れてしまう、という架空実証です。
上の性質から、実務では次の3つを組み合わせます。
関連する背景として、複雑さを上げると訓練誤差は下がるが検証誤差は谷を過ぎて上がる関係は バイアス・バリアンス、 検証への張り付き自体は 過学習 の文脈で捉えられます。 なお、最終設定を全データで学習し直す運用は 再学習 を参照。
検証 訓練データ テストデータ ホールドアウト 交差検証 ネスト CV 検証によるモデル選択 ハイパーパラメータ調整 データリーク バイアス・バリアンス 過学習 再学習
※ 数値は data/raw/SSDSE-B-2026.csv(2023 年 47 都道府県, A1101)の実測、 および明記した架空ノイズデモに基づく。既存本文への追記のみで、他の記述は変更していません。