論文一覧に戻る 📚 用語集トップ 🗺 概念マップ
📚 用語解説
📚 用語解説
バリデーション
Validation
ML基礎

🔖 キーワード索引

Validation検証HoldoutK-Fold CVStratified CVNested CVTime Series CVEarly Stopping汎化性能GroupKFoldLeave-One-OutRepeatedKFoldTimeSeriesSplitPipeline とリーク楽観バイアスfold 間のばらつき
💡 30秒結論 | 📍 文脈 | 🎨 直感 | 📐 数式 | 🔬 数式を言葉で読み解く | 🧮 実値計算 | 🐍 Python 実装 | ⚠️ 落とし穴 | 🌐 関連手法 | 🔗 関連用語 | 📚 グループ教材 | 🎮 触って理解する

💡 30秒で分かる結論

🍰 まずはやさしく

正解を確かめるためのテストです。

モデルが正しいか判断するために使います。

スマホのアプリを試す感覚に似ています。

まずは結論と直感的な意味を読みましょう。

モデル選択のための検証手続き

📍 文脈 — どこで使う概念か

🍰 まずはやさしく

モデルを選ぶための準備ステップです。

最適な設定を見つけるために使います。

部活の練習で本番を想定するようなものです。

この言葉がどう使われるかを見ていきましょう。

バリデーション(検証)は 学習済みモデルを Train と Test の間で評価し、 ハイパラ調整やモデル選択に使う中間ステップ。 Train で学び、 Val で選び、 Test で最終判定、 という 3 段構えが現代 ML の標準。

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

このページは、 モデルを作った「後」に、 その成績をどう測れば本番での成績に近い値になるかを扱う。 交差検証・ホールドアウトといった個々の分割法の手順はそれぞれのページに譲り、 ここでは「データの形(標本サイズ・時系列・同じ県の繰り返し・クラスの偏り)からどの分割法を選ぶか」と「出てきたスコアをどう読むか・どう報告するか」を、 SSDSE-B-2026 の 47 都道府県 × 12 年度のデータで確かめる。

🎨 直感で掴む

🍰 まずはやさしく

未知のデータへの強さを測る仕組みです。

練習問題だけに慣れるのを防ぐために使います。

テスト勉強で予想問題を解く感覚です。

データの量に合わせて選ぶ方法を学びます。

バリデーション (validation) は「モデルの未知データへの汎化性能を、 訓練時に見せなかったデータで測る」プロセス。 単に訓練データの精度 (in-sample) を測ると過学習を見逃すため、 train/validation の分離が必須。 SSDSE-B-2026 の 47 都道府県のような小標本では、 ホールドアウトより 5-fold CV または LOO で「全データを評価に使い回す」のが定石。

本ページでは バリデーション を、 「ホールドアウト → K-fold CV → 層化 K-fold → LOO → TimeSeriesSplit」の選択軸で整理し、 SSDSE-B-2026 の N=47 ケースでの実装を示す。 厳密な定義より、 まず標本サイズ N と時系列性で何を選ぶかを理解することを優先する。

🎨 直感で掴む — 具体例で理解する

Validation がないと「Test を見ながらモデルを調整する」ことになり、 Test 過適合が起きる。 そこで Train を Train + Val に分け、 ハイパラやモデル選択は Val で完結させる。 データが小さいと Val の不確実性が大きいので、 K-Fold CV で k 回平均を取る。 時系列データは未来情報リークを避けるため Time Series Split を使う。

📐 定義

🍰 まずはやさしく

モデル選択のための検証手続きのことです。

分析の標準的な道具として使います。

買い物で商品の質を確かめるようなものです。

詳しい定義や計算の方法を確認しましょう。

モデル選択のための検証手続き

英語名 Validation。

📐 数式・定義

バリデーションを数式 / 形式定義で表す:

$$\text{CV-Score} = \frac{1}{K}\sum_{k=1}^K \text{Metric}\!\big(\mathcal{D}_k^{\text{val}}, f^{(-k)}\big)$$

K-Fold 交差検証スコア:データを K 分割し、 各 fold を Val、 残りを Train として K 個のモデルを学習し平均する。

📐 k-fold CV の数式を言葉で読み解く

k-fold CV の汎化誤差推定量は次のように書ける。

$$\widehat{\text{Err}}_{\text{CV}} = \frac{1}{k}\sum_{j=1}^{k} \frac{1}{|D_j|}\sum_{(x_i,y_i)\in D_j} L(y_i, \hat{f}^{(-j)}(x_i))$$

数式を言葉で読み解く:

バイアス・分散分解で見ると、 k=n(LOOCV)はバイアス最小だが分散大、 k=5 は分散小だがバイアス中程度。 経験則として k=5 または k=10 がスイートスポット。

🔬 数式を言葉で読み解く

上の数式に出てきた記号を 1 つずつ解説します。 数式が出てくる試験問題(統計検定・G 検定・基本情報)では、 各記号の意味を答えられるかが分岐点:

記号意味
$K$Fold 数(5 or 10 が典型)
$\mathcal{D}_k^{\text{val}}$$k$ 番目の検証 fold
$f^{(-k)}$fold $k$ を除いて学習したモデル
MetricRMSE・F1 など

🧮 SSDSE-B 実値計算 — 都道府県データで手を動かす

SSDSE-B-2026 で 「総人口 → 出生数」回帰の 5-Fold CVを実行し、 各 fold の R² と平均を出す。

使用データ:SSDSE-B-2026.csv(独立行政法人 統計センター提供、 47 都道府県 × 100 超の社会経済指標)。 出典

📥 入力例(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
import pandas as pd
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import KFold, cross_val_score
import numpy as np

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
df = df.rename(columns={df.columns[2]: 'pref'})
df = df[df['SSDSE-B-2026'] == 2023]      # 2023 年度の 47 都道府県

X = df[['A1101', 'A1303']].values
y = df['A4101'].values

kf = KFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(LinearRegression(), X, y, cv=kf, scoring='r2')
print(f'各 fold R²: {scores.round(3)}')
print(f'平均 R²   : {scores.mean():.3f} ± {scores.std():.3f}')
📤 実行例(実測) 各 fold R²: [0.982 0.995 0.969 0.994 0.98 ] 平均 R² : 0.984 ± 0.010

💬 5 つの fold の R² は 0.969〜0.995 で、平均 0.984 ± 0.010。どの fold でも 0.97 を下回らないので、総人口と 65 歳以上人口で出生数を当てる線形モデルは、どの 9〜10 県を検証に回しても安定している。最も低い 0.969 は 3 番目の fold で、kf.split で中身を見ると北海道が入っている。北海道は人口規模の割に出生数が少なく、この 2 変数の直線から大きく外れる県なので、fold ごとの差は検証に回った県の顔ぶれで説明できる。

▲ 上記コードはそのまま実行可能。 CP932 エンコーディング・skiprows=1(英語ヘッダ行をスキップ)・列名の英数字コード(A1101 = 総人口 など)に注意。

🧮 SSDSE-B-2026 で 5-fold CV を回す

このコードでやること:SSDSE-B-2026 の都道府県データ(2023 年・n=47)を使い、 「総人口(A1101)」を予測する線形回帰モデルの汎化性能を cross_val_score で 5-fold CV により評価する。

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

SSDSE-B-2026 Code Prefecture A1101 A1301 A4101 A4103 0 2023 R01000 北海道 5092000 514000 24430 1.06 1 2023 R02000 青森県 1184000 118000 5696 1.23 2 2023 R03000 岩手県 1163000 120000 5432 1.16 ... 46 2023 R47000 沖縄県 1468000 236000 12549 1.60
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.linear_model import LinearRegression
from sklearn.model_selection import KFold, cross_val_score
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

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)  # 2023 年・47 都道府県

# 説明変数:15歳未満人口、 出生数、 合計特殊出生率 → 目的変数:総人口
X = df[['A1301', 'A4101', 'A4103']]
y = df['A1101']

# 人口・出生数(〜10^5)と出生率(〜1)は桁が違うので、標準化してから回帰する
model = make_pipeline(StandardScaler(), LinearRegression())
kf = KFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(model, X, y, cv=kf, scoring='r2')
print("各 fold R²:", scores.round(3))
print(f"平均 R² = {scores.mean():.3f} ± {scores.std():.3f}")

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

各 fold R²: [0.983 0.997 0.988 0.997 0.985] 平均 R² = 0.990 ± 0.006

💬 結果の読み方:R² ≈ 0.990 と非常に高く、 5 fold も 0.983〜0.997 に収まってばらつきは小さい(±0.006)。 → 15 歳未満人口・出生数・合計特殊出生率の 3 変数で総人口がほぼ説明できる、 安定したモデルだと判断できる。 hold-out の 1 値ではなく 5 つの値の分布を見ることで「たまたまよく当たった」を排除できる。

🧮 数式に値を入れて手で計算する: K-fold CV の評価

合成 5-fold CV の精度を平均する。

Step 1: fold 別精度

f = [0.85, 0.87, 0.84, 0.86, 0.83] 平均 = (0.85+0.87+0.84+0.86+0.83)/5 = 4.25/5 = 0.85 偏差 = [0, +0.02, −0.01, +0.01, −0.02] 偏差平方和 = 0 + 0.0004 + 0.0001 + 0.0001 + 0.0004 = 0.0010 SD(不偏, ddof=1)= √(0.0010/4) = √0.00025 ≈ 0.0158

Step 2: 95% CI

SE = 0.0158/√5 = 0.0158/2.236 ≈ 0.00707 CI = 0.85 ± 1.96·0.00707 = 0.85 ± 0.0139 = [0.836, 0.864]

🐍 Python で再現

1
2
3
4
5
6
7
import numpy as np
folds = np.array([0.85, 0.87, 0.84, 0.86, 0.83])
mean = folds.mean()
sd = folds.std(ddof=1)
SE = sd/np.sqrt(len(folds))
print(f"平均: {mean}, SD: {sd:.3f}")
print(f"95% CI: [{mean - 1.96*SE:.3f}, {mean + 1.96*SE:.3f}]")

📤 実行結果

平均: 0.85, SD: 0.016 95% CI: [0.836, 0.864]

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

🐍 Python 実装

この章では、 分割法ごとに「何を学習に使い、 何で評価したか」がスコアにどう効くかを、 同じ SSDSE-B-2026 のデータで並べて確かめる。 ホールドアウト 1 回 → 層化 K-fold → Nested CV → TimeSeriesSplit → Pipeline の順に進む。

🐍 ホールドアウト 1 回(37 県で学習・10 県で評価)

📥 入力例(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
# ── この抜粋で使うデータを用意します(英字の項目コードで読み込み)──
import pandas as pd

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', header=0)
df = df[df['Code'].astype(str).str.match(r'^R\d{5}$', na=False)].copy()
for _c in df.columns[3:]:
    df[_c] = pd.to_numeric(df[_c], errors='coerce')
df = df[df['SSDSE-B-2026'] == df['SSDSE-B-2026'].max()]

# この抜粋で使う df_jp を用意する
import pandas as pd

df_jp = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
df_jp = df_jp[df_jp['SSDSE-B-2026'] == 2023]   # 2023 年の 47 都道府県

from sklearn.linear_model import LinearRegression
from sklearn.metrics import r2_score, mean_squared_error
from sklearn.model_selection import train_test_split
import numpy as np

X = df_jp[['A1101', 'A1303']].fillna(0).values
y = df_jp['A4101'].values
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, random_state=42)
m = LinearRegression().fit(X_tr, y_tr)
pred = m.predict(X_te)
print(f'R²   = {r2_score(y_te, pred):.3f}')
print(f'RMSE = {np.sqrt(mean_squared_error(y_te, pred)):.2f}')
📤 実行例(実測) R² = 0.982 RMSE = 3438.97

💬 こちらは df_jp を skiprows=[1] で読み直してから同じ 2 変数モデルを 37 県で学習し、残る 10 県で評価した。R² = 0.982 は先の 5-Fold CV の平均 0.984 とほぼ同じだが、RMSE 3,439 人の 1 回分だけでは、5-Fold CV の fold 間の幅(R² で 0.969〜0.995)のどこに当たるかは分からない。ホールドアウトは速い代わりに分け方の運に左右されるので、47 県程度の小さなデータでは交差検証の平均と幅を主に報告する。

🐍 Stratified k-fold で分類を評価(fold ごとの正例率をそろえる)

このコードでやること:SSDSE-B-2026 から「合計特殊出生率(A4103)が全国平均以下か」を二値分類し、 各 fold で正例比率を揃える Stratified k-fold で評価する。

📥 入力データ:A4103(合計特殊出生率)を平均で二値化したラベル。 正例率は約 50%(実測 49%=23/47)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import StratifiedKFold, cross_val_score

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
df = df[df['SSDSE-B-2026'] == 2023]  # 2023 年・47 都道府県
X = df[['A1101', 'A1301', 'A4101']]
y = (df['A4103'] < df['A4103'].mean()).astype(int)

skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
model = LogisticRegression(max_iter=1000)
scores = cross_val_score(model, X, y, cv=skf, scoring='accuracy')
print(f"Stratified 5-fold accuracy: {scores.mean():.3f} ± {scores.std():.3f}")

📤 実行例:

Stratified 5-fold accuracy: 0.831 ± 0.085

💬 結果の読み方:accuracy 83% は単純多数決(約 51%)より高いが、 標準偏差 ±8.5% と大きい → サンプル数 47 と小さいことが原因。 通常の KFold だと fold によって正例比率が偏り評価が不安定になるので、 不均衡データでは Stratified 一択。

🐍 Nested CV — ハイパラ調整と性能評価を分離

このコードでやること:内側 CV でハイパラを選び、 外側 CV でモデル性能を評価する Nested CV を SSDSE-B-2026 で実行する。 同じデータでチューニングと評価を兼ねると 楽観バイアスが乗るのを防ぐ。

📥 入力データ:SSDSE-B-2026、 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
import pandas as pd
from sklearn.linear_model import Ridge
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import GridSearchCV, cross_validate, KFold

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
X = df[['A1101', 'A1301', 'A4200']]
y = df['A4101']

# 桁の違う説明変数に Ridge をかけるので、標準化してから罰則を効かせる
model = make_pipeline(StandardScaler(), Ridge())
inner = KFold(n_splits=3, shuffle=True, random_state=0)
outer = KFold(n_splits=5, shuffle=True, random_state=1)
param_grid = {'ridge__alpha': [0.01, 0.1, 1.0, 10.0, 100.0]}
gs = GridSearchCV(model, param_grid, cv=inner, scoring='r2')
res = cross_validate(gs, X, y, cv=outer, scoring='r2', return_estimator=True)
nested_scores = res['test_score']
print("外側 fold ごとに選ばれた alpha:",
      [est.best_params_['ridge__alpha'] for est in res['estimator']])
print(f"Nested CV R² = {nested_scores.mean():.4f} ± {nested_scores.std():.4f}")

# 比較: 全データで選んだ alpha の内側 CV スコア(同じデータで選んで評価するので楽観的)
gs.fit(X, y)
print(f"GridSearchCV best_score_ = {gs.best_score_:.4f}  (alpha={gs.best_params_['ridge__alpha']})")

📤 実行例:

外側 fold ごとに選ばれた alpha: [0.1, 0.1, 0.1, 0.1, 0.1] Nested CV R² = 0.9916 ± 0.0004 GridSearchCV best_score_ = 0.9918 (alpha=0.1)

💬 結果の読み方:5 つの外側 fold すべてで内側 CV が alpha = 0.1 を選び、 Nested CV の R² は 0.9916 ± 0.0004。 同じデータで選んで評価した GridSearchCV の best_score_ 0.9918 のほうがわずかに高く、 これが「選んだデータで評価する」ことの楽観バイアスにあたる。 ここでは差が 0.0002 と小さいが、 候補のハイパラが多く標本が少ないほど開く。 なお標準化せずに Ridge にかけると、 人口(〜10^6)に比べて罰則が効かず 5 つの alpha がすべて同じスコアになり、 探索そのものが意味を失う。

🐍 TimeSeriesSplit — 時系列データの正しい分割

このコードでやること:SSDSE-B-2026 を複数年データとして扱い、 TimeSeriesSplit で「過去→未来」の分割を行う。 通常の KFold と比較して、 リーク防止の重要性を確認する。

📥 入力データ:SSDSE-B-2026 を SSDSE-B-2026 列(年)でソート、 X は人口関連 3 変数、 y は出生数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import pandas as pd
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import TimeSeriesSplit, KFold, cross_val_score

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
df_sorted = df.sort_values('SSDSE-B-2026', kind='stable')  # 年順
X = df_sorted[['A1101', 'A1301', 'A4200']]
y = df_sorted['A4101']

tscv = TimeSeriesSplit(n_splits=5)
for i, (tr, vl) in enumerate(tscv.split(X)):
    yrs = df_sorted['SSDSE-B-2026'].iloc[vl]
    print(f"Fold {i}: train {len(tr)} 件, val {len(vl)} 件 (検証 {yrs.min()}–{yrs.max()} 年度)")

scores = cross_val_score(LinearRegression(), X, y, cv=tscv, scoring='r2')
print("各 fold R²:", scores.round(3))
print(f"TimeSeriesSplit R² = {scores.mean():.4f} ± {scores.std():.4f}")

# 比較: 年を無視して無作為に 5 分割した場合
kf = KFold(n_splits=5, shuffle=True, random_state=42)
kf_scores = cross_val_score(LinearRegression(), X, y, cv=kf, scoring='r2')
print(f"KFold(shuffle) R² = {kf_scores.mean():.4f} ± {kf_scores.std():.4f}")

📤 実行例:

Fold 0: train 94 件, val 94 件 (検証 2014–2015 年度) Fold 1: train 188 件, val 94 件 (検証 2016–2017 年度) Fold 2: train 282 件, val 94 件 (検証 2018–2019 年度) Fold 3: train 376 件, val 94 件 (検証 2020–2021 年度) Fold 4: train 470 件, val 94 件 (検証 2022–2023 年度) 各 fold R²: [0.995 0.994 0.987 0.981 0.988] TimeSeriesSplit R² = 0.9890 ± 0.0050 KFold(shuffle) R² = 0.9919 ± 0.0003

💬 結果の読み方:fold が進むごとに train が 94 件(2 年度分)ずつ増える expanding window で、 検証は常に学習より後の 2 年度。 R² は 0.995 から 2020–2021 年度の 0.981 まで下がり、 平均 0.9890 ± 0.0050。 同じデータを年を無視して無作為に分けた KFold は 0.9919 ± 0.0003 と高く揃うが、 これは同じ県の前後の年度が学習側にも入り、 未来の値を見て当てている分を含む。 過去から未来を当てる性能として報告するのは TimeSeriesSplit 側の値で、 fold ごとの落ち込み(コロナ期の 2020–2021 年度が最低)も一緒に見る。

📊 主要 CV 手法の比較表

手法バイアス分散計算量用途
Hold-out中高O(1)大規模・素早く回したい
5-fold CV小〜中小O(5)標準的選択
10-fold CV小中O(10)中小規模データ
LOOCV最小大O(n)超小規模(n < 50)
Stratified k-fold小小O(k)不均衡分類
TimeSeriesSplit中中O(k)時系列
GroupKFold小小O(k)グループ構造あり
Nested CV最小小O(k×k)ハイパラ調整 + 性能評価
Bootstrap (.632+)小小O(B)不確実性区間が欲しい時

🐍 Pipeline で Leakage を防ぐ

このコードでやること:標準化 + 線形回帰を Pipeline にまとめ、 CV の各 fold で正しく fit するようにする。 Pipeline なしと有りの結果を比較する。

📥 入力データ:SSDSE-B-2026、 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
import pandas as pd
from sklearn.linear_model import Ridge
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.model_selection import cross_val_score, KFold

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
X = df[['A1101', 'A1301', 'A4200']]
y = df['A4101']
kf = KFold(n_splits=5, shuffle=True, random_state=42)

# Pipeline なし:CV の外で全データに scaler を fit(検証 fold の平均・分散が混ざる)
X_leak = StandardScaler().fit_transform(X)
leak = cross_val_score(Ridge(alpha=1.0), X_leak, y, cv=kf, scoring='r2')

# Pipeline 化:fold ごとに scaler を訓練側だけで再 fit する
pipe = Pipeline([
    ('scaler', StandardScaler()),
    ('ridge', Ridge(alpha=1.0))
])
scores = cross_val_score(pipe, X, y, cv=kf, scoring='r2')
print(f"Pipeline なし CV R² = {leak.mean():.4f} ± {leak.std():.4f}")
print(f"Pipeline   CV R² = {scores.mean():.4f} ± {scores.std():.4f}")
print(f"fold ごとの差(なし − あり): {(leak - scores).round(5)}")

📤 実行例:

Pipeline なし CV R² = 0.9915 ± 0.0012 Pipeline CV R² = 0.9915 ± 0.0012 fold ごとの差(なし − あり): [-6.e-05 3.e-05 -3.e-05 3.e-05 -4.e-05]

💬 結果の読み方:CV の外で全データに scaler を fit した「Pipeline なし」と、 fold ごとに訓練側だけで fit する Pipeline は、 どちらも R² = 0.9915 ± 0.0012 で、 fold ごとの差も 10^−5 程度しかない。 564 行もあると検証 fold を混ぜても平均・分散がほとんど変わらないためで、 標準化だけのリークは小さい。 ただし特徴量選択・SMOTE・目的変数を使うエンコーディングのように検証 fold の y まで使う前処理では差が大きく開くので、 前処理を Pipeline に入れる習慣はそのまま守る。

🔍 深掘り:CV スコアの不確実性と統計的検定

2 つのモデルの CV スコアを比較する際、 「平均値だけ」で勝敗を決めるのは危険。 fold ごとのスコアは相関しているため、 通常の t 検定の前提が成り立たない。 推奨:

Kaggle 等で「CV スコアが 0.001 上がった」と喜ぶ前に、 fold 標準偏差を見て差が有意かを必ず確認すること。

⚠️ よくある落とし穴

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

「バリデーション」を実務・試験で扱うときに頻発する典型的なミスです。

❌ Val と Test を混同
Val は調整用、 Test は最終評価用。 役割を混ぜると過大評価。
❌ 時系列を Random CV
時系列データは未来リーク。 TimeSeriesSplit を使う。
❌ Stratify 忘れ
不均衡分類で Random KFold だと少数派が偏在。 StratifiedKFold を必須。
❌ 単一 Holdout で結論
データ少量だと Holdout 1 回の結果は揺れる。 CV で平均をとる。
❌ CV を Test で再検証しない
CV スコアが良くても本番分布が違うと失敗。 最終 Test で確認。

⚠️ Data Leakage の典型パターン 5 種

  1. 正規化を CV 前に適用:StandardScaler.fit_transform(X) を CV の外でやると、 テスト fold の平均・分散が train に漏れる。 → Pipeline に scaler を組み込み、 fold ごとに fit すれば防げる。
  2. 特徴量選択を CV 前に:相関の高い変数だけ残して CV すると、 「テスト側を覗いた選択」になる。 → 特徴量選択も Pipeline に含める。
  3. 時系列を shuffle:未来→過去の予測になり、 楽観評価。 → TimeSeriesSplit を使う。
  4. 同一個体が train/val に分割:医療データで「同じ患者の別検査」が train と val に分かれると、 個人特徴を覚えてしまう。 → GroupKFold で患者 ID をグループ化。
  5. ターゲット由来の特徴量:「将来の y で作った変数」を X に入れる。 → 特徴量定義時点で「予測時に入手可能か」を必ず確認。

📌 CV スコアが現実より極端に高い時は、 まず Data Leakage を疑え。 CV と本番の成績が大きく食い違うときは、 分割前の前処理・特徴量の作り方・同じ個体や将来の値が学習側に入っていないかを最初に点検する。

🗓 バリデーション手法の歴史

年出来事提唱者
1968Hold-out 法の体系化Lachenbruch & Mickey
1974Cross-validation の理論整備Stone, M.
1979Bootstrap の提案Efron, B.
1995k-fold CV と LOOCV の比較研究Kohavi, R.
2006Nested CV による楽観バイアス研究Varma & Simon
2016scikit-learn 0.18 で TimeSeriesSplit 標準化scikit-learn team

❓ よくある質問

Q1. k は 5 と 10 のどちらが良い?

A. データが小さい(n < 500)なら 10-fold、 中規模(500-5000)なら 5-fold で十分。 bias-variance のトレードオフで k=10 は分散がやや大きくなるが、 多くの実証研究で k=5 と k=10 はほぼ同じ結論を出す。

Q2. LOOCV は使うべき?

A. n が極小(< 50)でなければ非推奨。 LOOCV は 分散が大きい(fold 間の相関が高いため)。 線形回帰には解析解があり計算量問題は無いが、 通常は 10-fold で十分。

Q3. Validation と Test の違いは?

A. Validation はモデル選択(ハイパラ調整・特徴量選択など)に使うデータ、 Test は最終評価のみに使うデータ。 Test を見てチューニングしてはいけない(リーク)。 Kaggle の public/private LB の関係に近い。

Q4. shuffle=True と shuffle=False の使い分け?

A. IID なら True、 時系列・順序のあるデータなら False。 とくに SSDSE-B-2026 のような複数年パネルでは year を考慮して時系列分割する必要がある。

Q5. CV スコアが test スコアと大きく違う場合は?

A. (1) data leakage、 (2) train/test の分布差(covariate shift)、 (3) サンプリングバイアス、 (4) ハイパラ過剰調整、 のいずれか。 まず Pipeline 化と分布の確認から。

🎮 触って理解する — 検証設計シミュレータ

個々の手法(交差検証・ホールドアウト)の中身は各ページに譲り、 ここでは「検証の全体設計」= データの条件からどの分割法を選ぶかを体感する。 下のスライダとスイッチで 標本サイズ N・時系列性・クラス不均衡 を切り替えると、 ①フローチャートの推奨経路、 ②分割の見取り図、 ③学習回数・fold サイズ が同時に更新される。 本ページ「🌳 手法選択フロー」のルールをそのまま動かせるようにしたもの。

N=47:SSDSE-B-2026 の 47 都道府県と同じ小標本
時系列データ?
クラス不均衡?
分割数 K

① 意思決定フローチャート(推奨経路がハイライト)

① 時系列データ? Yes TimeSeriesSplittrain は常に val より過去 No ② N ≥ 10,000? Yes Holdout(train:val = 8:2)大標本なら 1 回の分割で十分安定 No ③ N < 100? Yes LOO / 反復 K-fold全データを検証に使い回す No ④ クラス不均衡? Yes Stratified K-fold各 fold のクラス比を保持 No K-fold(shuffle=True)標準の交差検証

② 分割の見取り図(横棒 = データ N 件、フォールドに触れると内訳表示)

Train Validation 未使用(その fold では使わない)
フォールドにマウス / 指を乗せると内訳を表示
学習するモデル数
—
Train / Val 件数(代表 fold)
—
検証に 1 度でも使う割合
—

💡 直感 — 検証設計は「模擬試験の設計」

本番の入試(Test)を解いて対策したら実力は測れない。 だから模擬試験(Validation)を別に用意し、 模試の作り方(分割法)はデータの性質で決める——これが検証の全体設計。 上のフローチャートの分岐は 3 つだけ:時間の向き(時系列性)・データの量(N)・ラベルの偏り(不均衡)。 この 3 条件を確認せずに train_test_split を書き始めるのが、 初学者の最も多い事故パターン。

⚠️ シミュレータで体感できる落とし穴

🚀 発展 — フローチャートの先へ

このフローチャートは 1 段目の設計。 実務ではさらに、 ①Nested CV:ハイパーパラメータ調整を内側ループ、 性能評価を外側ループに分けて「調整による楽観バイアス」を除く、 ②GroupKFold:同一個体(同じ患者・同じ都道府県の複数年)が train と val に跨がるのを防ぐ、 ③反復 CV(RepeatedKFold):乱数シードを変えて CV を繰り返し、 分割運の影響を平均化する——を組み合わせる。 分割の内部動作は 交差検証・ホールドアウト・訓練・テスト分割、 時系列の性質は 時系列データ、 検証が守ろうとしている敵は 過学習 の各ページで深掘りできる。

🗺 概念マップ

関連概念を視覚的に整理した概念マップ。

validation 教訓 Pipeline 内 平均と標準偏差の両方 全データで再学習 スコアの安定性 分割の妥当性

このマップは validation の意思決定の流れを表す。 中央に「validation」を置き、「分割の妥当性 (層化が必要か、 時系列性があるか)」「スコアの安定性 (CV fold 間の標準偏差)」「全データで再学習 (本番モデルの構築)」「ハイパーパラメータ選択 (グリッドサーチ・ベイズ最適化との接続)」の 4 軸へ枝が伸びる。 SSDSE-B-2026 の場合、 47 都道府県という小標本では 5-fold CV と LOO の差が大きく、 分割設計が validation 結果を左右する点に注意したい。

🔗 隣接手法への橋渡し

バリデーションは「モデル評価」を担う層であり、 前後の手法とリレー的に接続する。

SSDSE-B-2026 (47 都道府県) のように小標本ではホールドアウト不可、 5-fold CV か LOO が必須となる。 学習曲線と組み合わせ、 「データを増やせば改善するのか、 モデル変更が必要なのか」を判断する。

🌳 手法選択フロー

バリデーション手法は標本サイズと時系列性で決まる。 以下の 3 段階で選択する。

  1. 時系列性があるか? Yes → TimeSeriesSplit (時系列用 CV) (未来情報漏洩防止)、 No → 次へ
  2. 標本サイズ N は? N>=10000 → 単純な train/test 分割、 100<=N<10000 → 5-fold か 10-fold CV、 N<100 → LOO または反復 CV
  3. クラス不均衡か? Yes → 層化 K-fold (sampling) でクラス比保持、 No → 通常の K-fold

SSDSE-B-2026 (N=47) のケースでは「No → 100 未満 → LOO または 5-fold 反復」が選ばれる。 fold 間の標準偏差が平均の 20% を超える場合、 分割設計か特徴量を見直す。

🌱 解説の深化 — 「CV スコアそのもの」の不確実性を疑う

ここまでは「どう分割するか」を扱ってきた。 この節では一歩引いて、 算出された CV スコア自体がどれほど信用できる数値なのかを、 SSDSE-B-2026(2023 年・47 都道府県)の実測で確かめる。 モデル・変数は本文の実値計算と同じ(説明変数 A1301 15歳未満人口・A4101 出生数・A4103 合計特殊出生率 → 目的変数 A1101 総人口、 線形回帰)。

🎨 直感 — CV スコアは「測定値」ではなく「推定値」

5-fold CV の平均 R² は、 モデルの汎化性能という見えない真値を、 たまたま引いた 1 通りの分割で推定した値にすぎない。 分割は random_state(シード)で決まるので、 シードを変えれば同じデータ・同じモデルでもスコアは動く。 体温計で 1 回測った 36.8℃ をそのまま信じず「測るたびに少し揺れる」と知っておくのと同じで、 CV スコアには標本誤差ならぬ「分割誤差」が乗っている。 N=47 の小標本ではこの揺れが無視できない大きさになる。

実測:本文と同じモデルで random_state を 0〜9 の 10 通りに変えて 5-fold CV を回すと、 平均 R² は 0.974(seed=6)〜 0.994(seed=9)まで 0.021 の幅で動いた(10 シードの平均 0.987、 シード間 SD 0.007)。 本文の seed=42 の値 0.990 は、 この分布のほぼ中央に落ちる「たまたま素直な 1 標本」だったことになる。

⚠️ 落とし穴(重要) — fold 平均に埋もれる外れ値と「シード選び」

🔬 発展 — 揺れを測って報告する道具立て

🔗 関連ページ