論文一覧に戻る 📚 用語集トップ 🗺 概念マップ
📚 用語解説
📚 用語解説
検証データ
Validation Data
ML基礎

🔖 キーワード索引

検証データ validation dev set ハイパーパラメータ モデル選択 k分割CV early stopping ホールドアウト stratify リーケージ 汎化 訓練/検証/テスト シャッフル 再現性

🔖 キーワード索引(追補)

🎨 直感で掴む 📐 分割式 🧮 実値で計算 🐍 train_test_split 🐍 KFold 🐍 Stratified 🐍 時系列分割 ⚠️ 落とし穴 🌐 関連手法 📚 グループ教材

💡 30 秒で分かる結論(追補)

📍 文脈ボックス:あなたが今見ているもの

この記事は 機械学習の基礎 の「データ分割」段階。 上位概念は 交差検証、 並列概念は 訓練・テスト分割 / ホールドアウト、 派生概念は 入れ子 CV / データリーク。 SSDSE-B-2026 を題材に「適切な分割比率」「リーク回避」を体感する。

🎨 直感で掴む:「模試」と「本試験」の比喩

モデル学習は「受験勉強」、 train は「過去問」、 validation は「模試」、 test は「本試験」。 過去問だけで勉強すると「答えを覚える」だけになる(過学習)。 模試で「弱点を見つけて勉強法を変える」(ハイパラ調整)。 本試験は当日 1 回しか受けられない(test set は最後に 1 回)。 もし「模試の点を見て勉強法を 100 回変える」と、 模試の問題に最適化しすぎて本試験では失敗する(validation set overfitting)。

役割英語用途頻度受験比喩
学習Training setパラメータ推定毎エポック使う過去問
検証Validation setハイパラ調整・モデル選択数十回〜数百回模試
テストTest set最終性能評価最後に 1 回だけ本試験

📐 分割の数式と Statistical Power

全データを $\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 を 60/20/20 分割

SSDSE-B-2026 単年(47 県、 2023 年)を 60/20/20 で分けると検証セット 10 件しかなく評価が不安定。 全 12 年(564 件)を使えば検証セット 113 件で実用範囲。 さらに 5-fold CV にすれば fold あたり 113 件 × 5 回で安定する。

データ規模分割trainvaltestval 上の SE
47 件 (1 年)60/20/20271010±0.16 (大)
47 件 (1 年)5-fold CV3710 ×5±0.07 (中)
564 件 (12 年)60/20/20338113113±0.05
564 件 (12 年)5-fold CV451〜452113 ×4・112 ×1±0.02 (小)
564 件 (12 年)LOOCV5631 ×564±0.04 (大、 計算重)
SSDSE-B-2026 全 12 年 = 564 件 60/20/20 分割: Train=338, Val=113, Test=113 5-fold: Fold 1〜4 = 113件, Fold 5 = 112件 (k=5 で平均化)

🐍 Python 実装

① sklearn.model_selection.train_test_split で 60/20/20

🎯 このコードでやること: 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}%)')

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

Total: 47 件 Train: 27 件 (57.4%) Val: 10 件 (21.3%) Test: 10 件 (21.3%)

💬 結果の読み方: 47 件を 60/20/20 にすると検証セットが 10 件しかない。 これは「モデル A の精度 0.85、 モデル B の精度 0.83、 どちらが良い?」を判定するには不十分(SE 大)。 SSDSE 1 年規模では k-fold CV が必須。 random_state=42 を固定すれば再現性が保てる。

② KFold で 5-fold 交差検証

🎯 このコードでやること: 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}')

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

Fold 1: train=37, val=10, R²=0.9907 Fold 2: train=37, val=10, R²=0.9947 Fold 3: train=38, val=9, R²=0.9515 Fold 4: train=38, val=9, R²=0.9892 Fold 5: train=38, val=9, R²=0.9730 5-fold 平均 R²: 0.9798 ± 0.0160

💬 結果の読み方: 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 で平均化されているので堅牢。

③ StratifiedKFold で分類タスクの層化

🎯 このコードでやること: 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}')

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

全体クラス比: {'large': 0.34, 'small': 0.34, 'medium': 0.319} Fold 1 val (10件): {'large': 0.4, 'small': 0.3, 'medium': 0.3} Fold 2 val (10件): {'small': 0.4, 'large': 0.3, 'medium': 0.3} Fold 3 val (9件): {'small': 0.333, 'medium': 0.333, 'large': 0.333} Fold 4 val (9件): {'medium': 0.333, 'large': 0.333, 'small': 0.333} Fold 5 val (9件): {'small': 0.333, 'medium': 0.333, 'large': 0.333}

💬 結果の読み方: 全 fold でクラス比率がほぼ 1:1:1 に揃った。 不均衡データ(少数クラス 5%)で通常 KFold を使うと、 ある fold には少数クラスが 0 件入ってしまい評価が壊れる。 Stratified が必須。 SSDSE では人口分布は連続値だが、 「3 分位ラベル化 → Stratified」で似た学習・検証バランスを保てる。

④ TimeSeriesSplit で時系列対応

🎯 このコードでやること: 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]})')

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

Fold 1: train=94件 (2012〜2013), val=94件 (2014〜2015) Fold 2: train=188件 (2012〜2015), val=94件 (2016〜2017) Fold 3: train=282件 (2012〜2017), val=94件 (2018〜2019) Fold 4: train=376件 (2012〜2019), val=94件 (2020〜2021) Fold 5: train=470件 (2012〜2021), val=94件 (2022〜2023)

💬 結果の読み方: train が「最初の数年」、 val が「次の数年」になり、 時間方向の順序が保たれる。 これにより「2024 年を予測する」という実運用シナリオに近い評価ができる。 KFold だと「2023 年で学習 → 2012 年で検証」が起きうるが、 時系列予測としては無意味(リーク)。

⚠️ 落とし穴(追補 5 件)

  1. データリーク (Data Leakage): 前処理(標準化、 欠損補完)を train+val 全体で実施してから分割すると、 val の情報が train に漏れる。 必ず「train で fit → val/test に transform」の順。 sklearn.pipeline.Pipeline + cross_val_score で自動化できる。
  2. 時系列の通常 split: TimeSeriesSplit を使わず KFold で時系列を分割すると「未来で学んで過去を予測」になる。 株価・需要予測では致命的。
  3. 不均衡分割: 少数クラス 5% のデータで通常 KFold を使うと、 fold によっては少数クラスが 0 件 → AUC が計算不能。 StratifiedKFold 必須。
  4. group leakage: 同じ「顧客 ID」「県」のサンプルが train と val 両方にあると、 同一個体の情報が漏れる。 GroupKFold で「顧客単位 / 県単位」で分割する。
  5. val set overfitting: val スコアを見ながらハイパラを 100 回調整すると、 val に過適合する。 これを防ぐには「ハイパラ調整は train 内 CV、 最終評価は別の test set」の入れ子 CV (nested CV) を使う。
領域手法関係
基本分割Hold-out / train_test_split単純な 1 回切り
交差検証k-fold CV / LOOCV複数回平均で堅牢
特殊分割Stratified / Group / TimeSeriesデータ特性に応じて
階層化Nested CVハイパラ調整と性能評価を分離
リスクデータリーク / 過学習不適切分割で発生

🔗 関連用語(前提・並列・発展)

前提: 訓練データ / テストデータ / ホールドアウト
並列: 交差検証 / ブートストラップ / 早期終了
発展: 入れ子 CV / ハイパラ調整 / データリーク / 汎化性能

📚 関連グループ教材

📐 補足: Nested 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 倍だが、 真の汎化性能を見るには必須。

🐍 Python 実装 (補足): Nested CV with GridSearchCV

🎯 このコードでやること: 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}')

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

Nested CV R² (外側 5-fold): [0.8120 0.9924 0.9903 0.9778 0.9960] 平均: 0.9537 ± 0.0711 全データ grid search の best CV score: 0.9122 差分 (楽観バイアス): -0.0415

💬 結果の読み方: この実測では外側 Fold 1 の R² が 0.81 と低く、 nested CV 平均は 0.9537 ± 0.0711。 差分がマイナスになったのは、 47 件の小標本では fold 構成のばらつき(分散)が楽観バイアスより大きく、 バイアスの符号すら安定しないため。 ハイパラ空間が広い深層学習やテキスト分類では 3〜5% の楽観バイアスが観測される(Cawley & Talbot 2010)ため、 「報告するスコアは nested CV 由来であるべき」という原則自体は変わらない。 小標本ではまず分散(fold 間ばらつき)を疑うこと。

🐍 Python 実装 (補足 2): Pipeline でリーク防止

🎯 このコードでやること: 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}')

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

NG (リーク): R² = 0.9723 ± 0.0297 OK (リーク防止): R² = 0.9722 ± 0.0303 差分: 0.0001

💬 結果の読み方: SSDSE では差が 0.0001 と小さいが、 これは「scale のパラメータ (mean, std)」が train と全体でほぼ同じだから。 サンプル数が少ない / 分布が偏る場合、 差は 0.05 以上になることもある。 Pipeline は 常に使うべき習慣。 SimpleImputer、 PCA、 SelectKBest なども必ず Pipeline 内に置く。

📐 補足: GroupKFold と「県単位」分割

SSDSE-B-2026 全 12 年データ (564 件) を通常 KFold で分けると、 同じ県の 2023 年データが train、 2022 年データが val に入ることがある。 「同じ県の異なる年」は強く相関するため、 これは group leakage。 GroupKFold で「県単位」で分割すれば防げる。

📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行) 都道府県 A1101(総人口) A4101(出生数) E1101(幼稚園数) 北海道 5,092,000 24,430 331 東京都 14,086,000 86,348 959 沖縄県 1,468,000 12,549 151 …(全 47 行)
 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)}')

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

Fold 1: train=444件 (37県), val=120件 (10県), 重複県=0 Fold 2: train=444件 (37県), val=120件 (10県), 重複県=0 Fold 3: train=456件 (38県), val=108件 (9県), 重複県=0 Fold 4: train=456件 (38県), val=108件 (9県), 重複県=0 Fold 5: train=456件 (38県), val=108件 (9県), 重複県=0

重複県 0 で「同じ県の異なる年」が train と val に入らない。 「未知の県の出生数を予測する」という設定では GroupKFold が必須。 顧客 ID、 患者 ID、 セッション ID でも同じ原則。

🗺 概念マップ:データ分割戦略の選び方

状況推奨手法理由
N > 10000、 IID単純な 60/20/20 holdoutval 2000 件で SE 小、 計算軽量
N < 1000、 IID5-fold or 10-fold CV分散小、 holdout だと信頼性低
N < 100LOOCV or Stratified k-fold (k 大)バイアス低減優先
不均衡分類 (少数 5%)StratifiedKFold各 fold で少数クラスを保証
時系列予測TimeSeriesSplit / blocked CV未来→過去のリーク防止
パネル / クラスタデータGroupKFold同一個体の漏出防止
ハイパラ最適化Nested CV調整と評価を分離
確率推定 / 校正StratifiedKFold + CalibrationBrier / Reliability の正確評価

📐 補足: バリデーション戦略の数式比較

各戦略の「バイアス・分散・計算量」を比較する:

手法学習回数バイアス分散計算量
Holdout (80/20)1中(train 8 割で過小評価可能)大(1 回だけ)最小
k=5 CV5
k=10 CV10小(train 9 割で適切)10×
LOOCV (k=N)N最小(train N-1)大(fold が高度相関)N× (重)
Repeated k-foldk×R最小k×R (重)
Bootstrap (.632)BB× (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 分割リーダーボード過適合警戒

⚠️ よくある誤り 5 連発

  1. 誤解 1: 「validation と test は同じ意味」→ 別物。 validation はモデル選択用、 test は最終評価用。 混同すると「自分の選んだモデルが本当に良い」とは言えなくなる。
  2. 誤解 2: 「k=2 でも CV だから OK」→ k=2 では各 fold で半分しか train に使えず、 OLS 等は性能が落ちる。 最低 k=5、 理想は k=10。
  3. 誤解 3: 「random_state を毎回変えて 100 回試して、 平均を取る」→ 結果がブレないことを確認するなら OK だが、 「最良の random_state を選んで報告」は誤った楽観評価。
  4. 誤解 4: 「scaler は最初に全データで fit してよい」→ NG。 これは典型的なリーク。 Pipeline で fold ごとに fit せよ。
  5. 誤解 5: 「test を見ずに val だけでハイパラ調整したから OK」→ 部分的に正しい。 val で多数回試すと val set overfitting が起きる。 「val 1 回 / test 1 回」が理想、 多数試行なら nested CV。

📚 確認チェックリスト

📐 補足: 検証データのサイズ設計の計算式

精度 $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% 以上の精度差しか確実に検出できない。

🐍 Python 実装 (補足 3): RepeatedKFold で分散を実測

🎯 このコードでやること: 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}')

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

5-fold × 10回 = 50 個の R² スコア 平均: 0.9772 標準偏差: 0.0291 95% CI (パーセンタイル): [0.8940, 0.9982] 最小: 0.8332, 最大: 0.9984

💬 結果の読み方: 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 との連携

検証データの新しい使い方として、 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% カバレッジが安定する。 検証データが「ハイパラ調整」だけでなく「不確実性定量化」にも使えることを示す例。

📚 さらに深く学ぶには

🧮 実値 (補足 2): 5-fold CV と Holdout の比較

SSDSE-B-2026 47 件で「LinearRegression」を 100 通りの random_state で評価し、 (1) 単一 holdout 80/20 と (2) 5-fold CV の結果分布を比較した:

評価法平均 R²標準偏差95% 範囲最悪値
単一 holdout (80/20)0.96410.0521[0.812, 0.999]0.759
5-fold CV0.96740.0200[0.914, 0.988]0.897
10-fold CV0.92450.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 が現実的

🐍 Python 実装 (補足 4): カスタム CV クラスを書く

🎯 このコードでやること: 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}')

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

年ブロック CV: R² = [0.9919 0.9842 0.9805] 平均: 0.9856 ± 0.0047

💬 結果の読み方: 業務シナリオに沿った「3 年学んで翌年予測」の精度を直接測れる。 標準 sklearn には無い CV ロジックも 20 行で書ける。 BaseCrossValidator は要件適応の汎用性が高い。 各社の業務要件 (「過去 6 ヶ月で学習、 直近 1 ヶ月で検証」等) に合わせて自作する習慣が望ましい。

📐 補足: Cross-Validation の歴史

📐 補足: モデル比較の統計的検定

「モデル A の CV スコア 0.85、 モデル B が 0.83」 → 本当に A が良いか? 検証データのスコア差は確率変数なので、 統計検定で評価:

🐍 Python 実装 (補足 5): モデル比較の有意差検定

🎯 このコードでやること: 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: 有意差なし、 偶然の範囲')

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

LinearRegression: 0.9226 ± 0.1070 Ridge (α=10): 0.9226 ± 0.1070 Paired t-test: t = -1.2593, p = 0.2396 Wilcoxon: W = 21.0000, p = 0.5566 → 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 を採用」のように述べる方が誠実。

🌐 補足: Production 化での validation

モデルがプロダクションに乗ったあとの「継続的バリデーション」も重要。 Concept Drift / Data Drift が起きると CV で測った精度は維持されない。

📐 補足: 検証スコアの解釈フローチャート

状況解釈次の手
train 高、 val 低過学習正則化、 データ拡張、 モデル単純化
train 低、 val 低過小学習 (underfitting)モデル複雑化、 特徴量追加、 学習時間延長
train ≈ val、 共に高理想的test 評価へ進む
fold 間の分散大データ不安定 or k 小k を増やす、 RepeatedKFold
val 高、 test 低val set overfittingNested CV、 ハイパラ試行回数削減
時系列で val 低分布シフト最近データに重み付け、 オンライン学習

🔬 数式 (補足): CV のバイアス・分散分解

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 回切りなので、 リハーサル + 予行演習で「想定外」への耐性を作る必要がある。

🌐 補足: 検証データ vs テストデータ vs ベンチマークデータ

種類使用頻度用途
Validation数十回〜数百回ハイパラ調整、 モデル選択CV の fold
Test (Holdout)最後に 1 回最終性能の報告Kaggle Private LB
Benchmark業界横断他研究との公正比較ImageNet, GLUE, SuperGLUE
External Holdout他組織から提供独立性検証医療 AI の他施設データ
Production Sample継続的本番デプロイ後の精度モニタA/B test サンプル
Calibration1 回確率予測の校正Platt scaling 用

📚 補足: 用語の細かい使い分け

📐 補足: CV のサイズと推奨 k 早見表

N推奨 k理由
50 以下LOOCV (k=N)バイアス最小化優先
50〜50010-fold CVバイアス・分散 OK
500〜100005-fold CV計算量と精度のバランス
10000〜1000003-fold or holdout 80/10/10CV の必要性低下
100000 以上単一 holdout 90/5/5val/test だけで SE 十分小

⚠️ 落とし穴 (補足 2): 「ハイパラ調整後にスコアを test で評価」も慎重に

「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 条件が破られて評価が楽観的になる。

🌐 補足: Kaggle における validation 戦略

💡 30秒で分かる結論

🍰 まずはやさしく

モデルを調整するための練習問題です。

一番良い設定を見つけるために使います。

部活で練習メニューを試す感覚です。

まずは結論から短くまとめます。

検証データ(Validation Data):ハイパーパラメータ調整・モデル選択に使う、 訓練データから切り分けた評価用データ

📍 あなたが今見ているもの

🍰 まずはやさしく

検証データの解説ページです。

機械学習の基本を学ぶために使います。

都道府県のデータを使って学びます。

おすすめの読み方を紹介します。

このページは「検証データ(Validation Data)」の用語解説です。 機械学習の基礎カテゴリにおける重要概念で、 機械学習の基礎 グループ教材の中で繰り返し登場します。 数式・実コード・落とし穴を 1 ページに集約し、 SSDSE-B-2026 都道府県データ(47 件 × 112 列)を題材に 手を動かしながら理解できるよう構成しています。

別称:Validation Set / Dev Set。 まず 💡 30秒結論 で全体像を、 次に 🎨 直感📐 数式🧮 実値🐍 Python の順で読むのがおすすめ。

🎨 直感で掴む

🍰 まずはやさしく

モデル選びのための模試のようなものです。

勉強のやり方が正しいか確かめます。

テスト前に模試で弱点を探す感覚です。

なぜデータを分けるのかを解説します。

検証データ(validation data)は「モデル選びの試験会場」です。 訓練データ(教科書)で勉強し、 検証データ(模試)で「どの参考書(モデル)が良いか」「何時間勉強すれば足りるか(ハイパーパラメータ)」を決め、 テストデータ(本試験)で最終評価する 3 段階の 役割分担がポイントです。

なぜ 3 つに分けるのか? 訓練データだけでハイパーパラメータを決めると 過学習する(モデルが訓練データを暗記)。 テストデータは「最終評価専用」で途中で見てはいけない(見ると テストへのリークが発生し、 本番性能を過大評価する)。 そこで 第 3 のデータとして検証データを切り出します。

用途 3 つ:

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 で描画)。

$$\text{訓練データ}\;:\;\text{検証データ}\;:\;\text{テストデータ} \;=\; 60\%\;:\;20\%\;:\;20\%\quad (\text{典型例})$$

英語名 Validation Data。 別称:Validation Set / Dev Set。

🔬 数式を言葉で読み解く

記号と意味を逐一突き合わせて読みます。 慣れないうちは式を「日本語で読む」ことが理解の近道です。

🧮 SSDSE-B 実値で計算してみる

SSDSE-B の 47 都道府県データを 訓練 60% / 検証 20% / テスト 20% に分割し、 RandomForest の max_depth を検証データで選びます。

データ出典:SSDSE-B-2026(独立行政法人統計センター)。 47 都道府県 × 複数年(最新 2023)の社会統計データ。

📥 入力例(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
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 val0.800 (best)
max_depth=3 val0.800
テスト精度1.000

🧮 検証データの「比率設計」と「nested CV」── SSDSE-B-2026 で具体的に考える

検証データの作り方には 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:11,000+test=5 が地域偏在しないよう stratify 必須
5-fold CV (val=test 兼用)30+hyperparameter 探索を含めると optimistic bias
Nested 5×5 CV30+外側 5-fold で test、 内側 5-fold で hyperparameter 選択 → leakage 0
LOOCV10+計算は 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 行。

SSDSE-B-2026 Code Prefecture A1101(人口) A4101(出生数) 2023 R01000 北海道 5092000 24430 2023 R13000 東京都 14086000 86348 2023 R27000 大阪府 8763000 55292 ... ... ... (全 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}")

📤 実行結果:

nested CV R² mean = 0.976, sd = 0.024 (全データ grid search の best CV も R² ≈ 0.974 で、 この 1 特徴量・線形モデルでは楽観バイアスはごく小さい)

💬 結果の読み方: 単純 5-fold は α 選択と test を同じ fold で使い回すため オプティミスティック・バイアスが乗り得るが、 ここでは人口 1 特徴量の線形モデルなので差はほぼ 0。 ハイパーパラメータ空間が広いモデル (RandomForest 等) や複雑なデータでは差が数 % に広がるため、 「nested CV か LOOCV を選ぶ」のが安全。

⚠️ validation-data 固有の leakage パターン

🖼 視覚で確認する:検証データの役割を図で押さえる

検証データは「過学習を検知する鏡」です。 学習曲線・残差・正則化曲線という3つの図で、 検証データが活きる場面を確認します。

単純回帰のフィット
図1: 単純な回帰モデルでも、 訓練誤差だけで判断すると過学習に気づけない。 検証データで MSE を別に測ることで初めて「汎化性能」が見える。
残差プロット
図2: 検証データに対する残差プロット。 訓練と検証で残差パターンが大きく異なれば、 分布シフトや leakage を疑う。
正則化曲線
図3: 正則化強度 λ を変えたときの検証誤差曲線。 検証データを使って λ を選ぶプロセスがハイパーパラメータ調整の中核。

3点の図は「過学習検知」「分布シフト診断」「ハイパーパラメータ選択」という検証データの3大用途を可視化しています。

🧮 数式に値を入れて手で計算する: 6:2:2 分割

合成 1000 件で train/val/test を計算する。

Step 1: 件数

set件数
train0.6600
val0.2200
test0.2200

Step 2: 検算

合計 = 1000 ✓ val/test は別目的: - val: ハイパラ調整 - test: 最終評価

🐍 Python で再現

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}")

📤 実行結果

train=600, val=200, test=200

💬 手計算 (Step 1) と Python 出力が完全一致。

🐍 Python 実装

scikit-learn / pandas を使った最小実装パターン。 上の SSDSE-B 計算と同じスタイルですが、 ここでは「読み込み→前処理→学習→評価」のテンプレを 4 つのスニペットに分けます。

① データ読み込み & 概観

📥 入力例(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
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 実値計算 節を参照。

⚠️ よくある落とし穴(5 つ)

❌ 検証データを学習に使ってしまう
fit する前に分割するのは常識だが、 スケーラーを全データで fit するのもリーク。 Pipeline + CV で防ぐ。
❌ 検証データに合わせすぎる
ハイパーパラメータを大量に試すと検証セット過学習。 試行回数が多いときは k 分割 CV + ネスト CV を。
❌ シャッフルなし時系列で分割
時系列は未来→過去のリーク注意。 TimeSeriesSplit を使い、 検証は必ず訓練より「後」の期間に。
❌ クラス不均衡で stratify を忘れる
層化なしだと検証セットに陽性が 0 件のケースが発生。 stratify=y を必ず指定。
❌ サンプル数が少ないとき固定分割
47 県のような小データはホールドアウト 1 回では不安定。 5-fold CV や Leave-One-Out を検討。

⚠️ 条件・限界・誤解回避(検証データ)

検証データ (validation data) は「訓練データで学習したモデルがどれだけ汎化するか」を測るための専用データセットで、 機械学習・統計学習の精度評価とハイパーパラメータ調整の要です。 しかし分割方法・サイズ・運用ルールを誤ると、 「検証スコアは高いのに本番で大失敗」という事態を招きます。 SSDSE-B-2026 を題材に、 適用条件・限界・誤解回避を整理します。

適用条件

  1. 訓練・検証・テストの三分割を原則とする: 訓練データでパラメータを学習し、 検証データでハイパーパラメータを選択し、 テストデータで最終評価する三段構えが基本。 検証とテストを混同すると、 テストスコアが楽観的になります。 SSDSE-B-2026 (N=47) なら 70/15/15% 分割、 または 5-fold CV + 別 hold-out が現実解。
  2. 分割の独立性と無作為性: 検証データは訓練データと統計的に独立であるべき。 時系列なら時間で区切る、 グループ構造があるなら GroupShuffleSplit、 クラス不均衡があるなら StratifiedShuffleSplit を使う。 SSDSE-B では地方区分を groupby して、 同じ地方が両側に入らないようにすると地理的相関の影響を抑えられます。
  3. 適切な検証データサイズ: 検証誤差の標準誤差は √(p(1-p)/n) 程度。 n=10 だと精度 10% でも標準誤差 9% と暴れます。 最低 200-500 件は確保したいですが、 SSDSE のような小標本では交差検証で代替します。
  4. 分布の整合性: 検証データの特徴量・目的変数分布が訓練データと同じであることが前提。 訓練と検証で分布が違うなら、 共変量シフト・ラベルシフト対策が必要 (importance weighting など)。
  5. 前処理 leakage の防止: 標準化・欠損補完・特徴量選択を訓練データ全体で先に fit すると、 検証データの情報が漏れます。 Pipeline 経由で fold/分割内で fit_transform を完結させる。

限界

  1. 固定 hold-out の variance: 1 回の分割で出した検証スコアは分散が大きく、 別の random_state では順位が逆転することも。 信頼性を上げるには複数 seed で繰り返すか CV を採用。
  2. 探索回数による過剰最適化: 検証データを何度も使ってハイパラを調整すると、 検証データに「フィット」してしまう。 これを防ぐため、 最終評価は触っていないテストデータで行う。
  3. 分布シフトの検知限界: 検証データも訓練と同じ母集団から取ると、 本番運用での分布変化は検出できない。 SSDSE-B-2026 で評価しても 2030 年運用時の有用性は別途検証が必要。
  4. 小標本の検出力不足: 検証データが小さいと、 モデル間の真の差を検出できず「どっちもどっち」と判定しがち。 検出力分析で必要サンプル数を試算しておく。
  5. クラス不均衡時の指標選択: 不均衡データでは accuracy だけ見ると常に多数派を選んで高得点。 ROC-AUC, PR-AUC, F1 など複数指標を併用する。

誤解回避

  1. 「検証スコアが高い = 良いモデル」は誤り: 検証データが偏っていれば高スコアは見せかけ。 訓練スコアとの差・fold 別ばらつき・残差プロット・特徴重要度を合わせて見ます。
  2. 「検証データはランダムに分けるだけで良い」は誤り: 時系列・グループ構造があるならランダム分割は leakage を生みます。 データ特性に応じた専用 splitter を使う必要があります。
  3. 「検証は 1 回でいい」は誤り: 小標本では分割依存性が大きく、 複数回・複数 seed で検証する RepeatedKFold が信頼性を高めます。
  4. 「検証データを増やせば訓練データが減って性能が落ちる」は条件付きで誤り: 学習曲線が頭打ちの場合、 訓練を減らしても性能は変わらず、 検証信頼性だけ上がる。 学習曲線を描いて判断する。
  5. 「テストデータも検証として使える」は誤り: テストはモデル選択後の最終確認のみ。 ハイパラ調整に使うと testing optimism (テスト過学習) が発生します。
  6. 「Public LB スコア = テスト性能」は誤り: Kaggle 等で Public LB は一部のテストデータのみ。 Private LB との乖離が起きるのは Public に過適合させた結果。

典型ワークフロー

  1. データ理解と分布確認: SSDSE-B-2026 を読み込み、 目的変数の分布・グループ構造・時系列性を確認。 ヒストグラム・散布図・df.describe() を出力。
  2. 分割戦略の決定: 単純無作為で良いか、 stratify が必要か、 group/time で分けるかを決める。 random_state を固定して再現性確保。
  3. 三分割の実施: train_test_split で 70/15/15% に分け、 最初の 70% を訓練、 次の 15% を検証、 残り 15% をテストとして退避。
  4. Pipeline 設計: scaler → 特徴量変換 → 推定器を sklearn.Pipeline でまとめ、 fit を訓練データのみに適用。
  5. ハイパラ探索: GridSearchCV/RandomizedSearchCV/Optuna で訓練+検証データを使ってハイパラ選択。 cv 引数で分割方式を明示。
  6. 診断プロット: 訓練 vs 検証の learning curve、 残差プロット、 confusion matrix を出力。 過学習・分布シフトの兆候を視覚的に確認。
  7. 最終評価: 退避していたテストデータで 1 回だけ評価。 95% 信頼区間 (bootstrap) も併記。
  8. レポート化: 分割戦略・ハイパラ・テスト指標・データ統計をまとめ、 再現可能なコードと共に保存。

ケーススタディ

検証データは「モデルの本当の実力」を測る唯一の手段ですが、 設計を誤ると逆に誤った安心感を生みます。 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 で順位が大きく落ちる現象を防ぐ核心的なルールです。

検証データのリーク (data leakage) を防ぐ実務チェックリスト

検証データの「リーク」は最も発見しにくく、 かつ最も致命的な失敗です。 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 できない」運用にすると、 検証スコアの楽観バイアスを構造的に防げます。

MLOps における検証データの管理

実務では「検証データを 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」のように揺らぎ付きで書く習慣を付けましょう。

📝 理解度チェック

  1. 検証データと訓練データを使い分ける目的は? (答:ハイパーパラメータ調整・モデル選択を「未学習データ」で行い、 過学習を検出するため)
  2. 検証データとテストデータの違いは? (答:検証はモデル選択の参考に何度でも使えるが、 テストは最終評価で 1 回のみ使う)
  3. SSDSE-B-2026(47 件)を train/val/test に分けるとき推奨される比率は? (答:件数が少ないので単純分割は避け、 K-fold CV や Nested CV を採用)
  4. 検証データのリークが起きる典型シナリオを 3 つ挙げよ。 (答:fold 外で標準化、 未来情報を含む特徴量、 ID 重複)
  5. 検証スコアの 95% 信頼区間を見るべきなのはなぜ? (答:小サンプルでは点推定の差が乱数の範囲に収まることがあるため)
  6. 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 通りに分岐する。

  1. データ豊富 (n > 10000)? Yes → 単純 hold-out (60/20/20)。 SSDSE-B-2026 の 564 行では不足気味なので CV 併用が無難
  2. データ少量 (n < 1000)? Yes → val を分けず 5-fold CV を val 代替に。 SSDSE-B-2026 の 47 県 1 年ぶんなら CV 必須
  3. 時系列? Yes → TimeSeriesSplit、 時間順 walk-forward で val を取る
  4. ハイパラ選定 + 公正評価 を両立? Yes → Nested CV。 外側 CV で test、 内側 CV で val

val を test 代わりに使い回す (最良 val 精度を最終報告) のは典型的な禁忌。 val は探索用、 test は 1 回限りの公正評価用、 と役割を厳密に分けるのが正しい運用。

🎮 触って理解する

合成データ(真の関数は 3 次多項式+ノイズ)を 訓練 60% ・ 検証 20% ・ テスト 20% に分割しました。 次数スライダーを動かすと、 多項式回帰の当てはめ曲線・訓練誤差検証誤差がその場で再計算されます。 検証誤差が最小になる次数を選ぶのがモデル選択、 その後 テストで 1 回だけ最終性能を確かめるのが正しい手順です。 ここでの数値はブラウザ内で最小二乗法をその場計算した合成データで、 SSDSE の実測値ではありません。

① 当てはめ(3 分割データ+回帰曲線)
訓練 検証 テスト 当てはめ曲線
② 次数ごとの誤差(クリック/タップで次数を選択)
訓練誤差 検証誤差 検証誤差が最小の次数


訓練 RMSE(現在の次数)
検証 RMSE(現在の次数)
検証誤差が最小の次数
次数を動かして、 検証誤差(赤)が最小になる次数を探してください。

🧭 この操作から読み取れること

🎨 直感:検証データは「調整用の中間データ」

検証データは、 パラメータ(回帰係数)を学ぶ訓練データと、 最終成績を測るテストデータのあいだにある「調整専用の中間データ」です。 モデルが自分で学べない設定値(次数・正則化強度・木の深さなど=ハイパーパラメータ)を、 訓練データを見ずに「未知データでの成績」で選ぶ役目を担います。 上の②のグラフで谷を探す行為そのものが、 検証データによるハイパーパラメータ選択です。

⚠️ よくある落とし穴:テストでの調整=リーク

🚀 発展:交差検証とネスト CV

検証データを 1 つ固定すると、 その分割の「引き」で結果が揺れます。 交差検証(k-fold CV)は、 データを k 分割して「各回で 1 つを検証・残りで訓練」を k 回繰り返し、 検証誤差を平均します。 上の②の赤い曲線を「k 本の平均」にするイメージで、 谷の位置が安定します。 ただしハイパラ選択と最終評価に同じ CV を使い回すと、 やはり楽観バイアスが残ります。 これを断ち切るのが ネスト CV で、 外側 CV でテスト評価・内側 CV でハイパラ選択と役割を二重に分離します。 テストでの調整を避ける原則の詳細は データリーク、 複雑さと誤差の関係は バイアス・バリアンス、 選ぶ設定値そのものは ハイパーパラメータ調整 を参照してください。

🔭 深掘り:検証スコアの「水準」は疑い、「順位」を使う

ここは既存の解説を壊さずに追記した発展セクションです。 姉妹ページ(検証訓練データ交差検証)と重複しない独自角度として、 「検証スコアという数字を、どこまで信じてよいか」という一点に絞って掘り下げます。 結論を先に言うと──検証スコアの絶対値(水準)は当てにならないことが多いが、候補を並べ替える順位づけは比較的頼れる、これが検証データの実務的な使い方です。

🎨 直感

検証データは「本番性能を測る物差し」ではなく、候補を並べ替える“ふるい”だと捉えると誤解が減ります。 模試に例えるなら、返ってきた絶対点はその日の運・問題の難易で大きくブレるけれど、 「国語より数学が得意」という科目間の順位は比較的安定します。 検証データも同じで、少数の検証サンプルだと R² や誤差の水準は本番から大きくズレ得る一方、 「どのモデル・どのハイパラが相対的に良いか」の順位は、水準ほどには壊れません。

覚え方: 検証スコアは argmax を取るための道具であって、 value をそのまま報告するための道具ではない。 「いくつだったか」より「どれが勝ったか」を使う。

これは 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)
20.63730.9175
30.64180.9215
40.64300.9234
50.65210.9282
None0.64820.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つを組み合わせます。

関連する背景として、複雑さを上げると訓練誤差は下がるが検証誤差は谷を過ぎて上がる関係は バイアス・バリアンス、 検証への張り付き自体は 過学習 の文脈で捉えられます。 なお、最終設定を全データで学習し直す運用は 再学習 を参照。

🔗 関連ページ

※ 数値は data/raw/SSDSE-B-2026.csv(2023 年 47 都道府県, A1101)の実測、 および明記した架空ノイズデモに基づく。既存本文への追記のみで、他の記述は変更していません。