本ページで扱うキーワード:
🍰 まずはやさしく
データを2つに分ける方法です。
AIの本当の実力を測るために使います。
テスト前に予想問題で練習する感覚です。
データの分け方や注意点を学びます。
訓練・テスト分割:機械学習で「学習に使わなかったデータ」で性能を評価するためにデータを 2 つに分ける手続き。
train_test_split がデファクト標準。random_state を必ず指定。stratify=y でラベル比率を保つ。TimeSeriesSplit を使う。GroupKFold へ。1 2 3 4 5 6 7 8 9 10 11 12 | import pandas as pd
df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=1)
df = df[df['年度'] == 2023].reset_index(drop=True)
df['高齢化率'] = df['65歳以上人口'] / df['総人口']
# pandas のみで 70/30 分割
train_df = df.sample(frac=0.7, random_state=0)
test_df = df.drop(train_df.index)
print(f"train n={len(train_df)}, test n={len(test_df)}")
print(f"train 高齢化率 平均={train_df['高齢化率'].mean():.4f}")
print(f"test 高齢化率 平均={test_df['高齢化率'].mean():.4f}") |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | import numpy as np
def my_stratified_split(df, label_col, test_size=0.3, random_state=0):
rng = np.random.default_rng(random_state)
train_idx, test_idx = [], []
for label in df[label_col].unique():
sub = df[df[label_col] == label]
n_test = int(len(sub) * test_size)
idx_perm = rng.permutation(sub.index)
test_idx.extend(idx_perm[:n_test])
train_idx.extend(idx_perm[n_test:])
return df.loc[train_idx], df.loc[test_idx]
df['高齢化率_2値'] = (df['高齢化率'] >= df['高齢化率'].median()).astype(int)
tr_df, te_df = my_stratified_split(df, '高齢化率_2値', test_size=0.3, random_state=0)
print(f"train: n={len(tr_df)}, ラベル平均={tr_df['高齢化率_2値'].mean():.3f}")
print(f"test: n={len(te_df)}, ラベル平均={te_df['高齢化率_2値'].mean():.3f}") |
train_test_split(X, y, test_size=0.3, random_state=0, stratify=y) が標準。train_test_split(X, y, test_size=0.3, random_state=0)。stratify は y_binned で代用可能。TimeSeriesSplit(n_splits=5) の expanding window が標準。GroupKFold(n_splits=5)(または StratifiedGroupKFold)。StratifiedKFold(n_splits=5, shuffle=True, random_state=0) で CV。GridSearchCV または RandomizedSearchCV + 外側 CV のネステッド構造。Pipeline 内に組み込み、fold 内で fit_transform。本ページは統計・データ解析コンペティション再現論文集の用語解説の 1 ページです。
🍰 まずはやさしく
機械学習で最初に習うルールです。
正しい評価をするために必要です。
部活の記録を分析する時にも役立ちます。
4つの分け方を比べる方法を読みます。
このページは ML 基礎 カテゴリの「訓練・テスト分割」用語解説です。機械学習でもっとも最初に登場する作法のひとつ。これを正しく実装できない限り、その先のすべての評価指標は信用できません。
関連用語:交差検証、ホールドアウト、過学習、汎化、データリーケージ。
本ページの題材データは SSDSE-B-2026(独立行政法人統計センター)。47 都道府県 × 多年データを使い、4 種の分割方式を比較します。
🍰 まずはやさしく
答えの丸暗記を防ぐための工夫です。
未知のデータへの対応力を知るためです。
過去問の答えを覚えるだけではダメな例です。
うまく分けるための落とし穴について読みます。
大学受験で「過去問の答えを覚えれば 100 点」ですが、それは本番では役に立ちません。機械学習も同じで、「学習に使ったデータでテスト」したらモデルが暗記しても満点を取ってしまう。だから「学習に使わなかったデータ」で実力を測る必要があります。これが 訓練・テスト分割。
「データを 2 つに分けて、片方で学習、もう片方で評価」というだけのシンプルな手続きですが、ここに無数の落とし穴があります。シャッフルしないと偏る、ラベル比率が崩れる、時系列が逆転する、グループが両側に漏れる ── どれも初学者は必ず一度ハマります。
分割の目的は「本番環境で未知データに出会ったときの性能を推定する」こと。train と test が偏っていたら、推定値が現実と乖離します。たとえば:
分割が「正しく」行われているかを目で確認するコードです。
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 from sklearn.model_selection import StratifiedKFold # X / y / skf はこのあとのブロックで作っているので、ここでも用意しておく _df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=1) _df = _df[_df['年度'] == 2023].reset_index(drop=True) _aging = _df['65歳以上人口'] / _df['総人口'] X = _df[['総人口', '日本人人口', '15歳未満人口']].values y = (_aging >= _aging.median()).astype(int).values import matplotlib.pyplot as plt import numpy as np skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=0) n = len(X) visualizer = np.zeros((5, n)) for i, (tr_idx, te_idx) in enumerate(skf.split(X, y)): visualizer[i, te_idx] = 1 # test = 1 fig, ax = plt.subplots(figsize=(10, 3)) ax.imshow(visualizer, aspect='auto', cmap='RdYlBu_r') ax.set_yticks(range(5)) ax.set_yticklabels([f"Fold {i+1}" for i in range(5)]) ax.set_xlabel('Sample index (0-46)') ax.set_title('StratifiedKFold 5-fold split visualization') plt.tight_layout() plt.savefig('split_visualization.png', dpi=150) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | import matplotlib.pyplot as plt
from sklearn.model_selection import TimeSeriesSplit
df_all = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=1)
df_all = df_all.sort_values(['年度', '都道府県']).reset_index(drop=True)
tss = TimeSeriesSplit(n_splits=5)
sizes = [(len(tr), len(te)) for tr, te in tss.split(df_all)]
print("各 fold の (train_size, test_size):", sizes)
fig, ax = plt.subplots(figsize=(10, 4))
folds = range(1, 6)
train_sizes = [s[0] for s in sizes]
test_sizes = [s[1] for s in sizes]
ax.bar(folds, train_sizes, label='Train', color='#1E88E5', alpha=0.7)
ax.bar(folds, test_sizes, bottom=train_sizes, label='Test', color='#E53935', alpha=0.7)
ax.set_xlabel('Fold')
ax.set_ylabel('Number of samples')
ax.set_title('TimeSeriesSplit: expanding train window')
ax.legend()
plt.tight_layout()
plt.savefig('time_series_split.png', dpi=150) |
分割方式の選択はバイアス・分散のトレードオフに直結します。
| k の値 | バイアス | 分散 | 計算コスト |
|---|---|---|---|
| k=2 | 高(train 50% で学習) | 低 | 最小 |
| k=5 | 中 | 中 | 5× |
| k=10 | 低 | 中 | 10× |
| k=n (LOOCV) | 最低 | 高(test n=1) | n× |
Kohavi (1995) のシミュレーション結果:k=10 がバイアスと分散のスイートスポット。多くの教科書も k=5 または k=10 を推奨。
「train と test の分布が同じ」という IID 仮定が現実的でないことが認識され、明示的に分布シフトを含む test セットでの評価が広まっています。WILDS ベンチマーク (Stanford 2021) は、医療・衛星画像・経済データなど 10 分野で「OOD test」を提供。
「介入後の効果」を観察データで推定する反事実評価。Doubly Robust Estimation、Causal Forest など、機械学習と因果推論の融合領域。
「データが順次到着する」設定で、固定された train/test 分割では評価できない。Prequential evaluation や Sliding window CV が使われる。
複数組織のデータを統合せずに学習。各組織のデータがプライベートなので、評価設計が極端に複雑化。
スライダーとボタンを動かすと、散布図・回帰曲線・誤差の数値がその場で更新されます。テストデータ(橙)はモデルの学習にいっさい使われず、「まだ見ぬ未知データ」の代理として汚さずに取っておく ── この感覚を手で確かめてください。すべての計算(最小二乗フィット・RMSE・クラス比率)はこのページ内の JavaScript で正確に実行しています。
目的はただ一つ、「まだ見ぬ未知データでの性能を、手元のデータで前もって推定する」こと。学習に使ったデータで測った点数は、いわば「答えを見たあとのテスト」。デモ1で次数を上げると訓練 RMSE は 0 に近づくのに、テスト RMSE はむしろ悪化しました。これが過学習で、訓練点を暗記しただけで汎化していない状態です。テスト側を「学習に触らせない」から、こうした暗記を見抜けます。
stratify=y を付けます。TimeSeriesSplit(交差検証ページ参照)を使います。調整と最終評価を分けるため、実務では 訓練 / 検証 / テスト の 3 分割が基本です(検証データ・テストデータ)。データが少ないとき(SSDSE-B-2026 は 47 県で n が小さい)は 1 回の分割では不安定なので、データを k 個に分けて役割を回す k-fold 交差検証が定番です。ハイパラ探索と性能評価を二重に分離する ネステッド CV や、県・年など単位のリークを防ぐ GroupKFold・ホールドアウトとの使い分けも合わせて押さえてください。
🍰 まずはやさしく
データを分ける仕組みを数式で表したものです。
正確な比率で分けるために使います。
スマホのアプリ開発などの計算に似ています。
集合を使った定義について読みます。
訓練・テスト分割は集合論で定式化できます。
英語名 Train/Test Split。別称:ホールドアウト法(Holdout method)。データを 1 度だけ分けるのを「ホールドアウト」、複数回分けるのを「k-fold CV」と区別することもあります。
「$\mathcal{D} = \mathcal{D}_{\text{train}} \sqcup \mathcal{D}_{\text{test}}$」を日本語に翻訳します。集合論の記号に慣れていない人ほど、ここを丁寧に読むと残りが楽になります。
$\mathcal{D}$ は手元にあるすべてのレコード(観測)の集まりを意味します。$\mathcal{D} = \{(x_1, y_1), (x_2, y_2), \ldots, (x_n, y_n)\}$ のように、特徴量 $x$ と目的変数 $y$ のペアの集合と考えるのが標準です。SSDSE-B-2026 なら 47 都道府県 × 12 年 = 564 行のレコード。各行が 1 つの「観測」で、$\mathcal{D}$ はそれを集合として括ったもの。
$\mathcal{D}$ を 2 つの部分集合に分けたもの。$\mathcal{D}_{\text{train}}$ はモデルの学習に使うレコード、$\mathcal{D}_{\text{test}}$ は学習済みモデルの評価にだけ使うレコード。「training」「test」と呼ばれることが多いです。テストセットは「絶対に学習中に触らない」という不文律があり、これを破ると「テストへの汚染(test set contamination)」と呼ばれる事故になります。
$\sqcup$ は「互いに素な和 (disjoint union)」を表します。普通の和 $\cup$ と違って、「両側に重複なし」を強調する記号。つまり $\mathcal{D}_{\text{train}} \cap \mathcal{D}_{\text{test}} = \emptyset$(交わりが空)。同じレコードが訓練とテストの両方に存在しない、ということ。これが破れると、テストで「学習済みデータ」を再評価することになり、性能が過大評価されます。
$|A|$ は集合 $A$ の要素数(カーディナリティ)。$|\mathcal{D}_{\text{train}}| = 33$ なら「訓練データに 33 行」の意味。比 $(1-\alpha):\alpha$ で、訓練 70%、テスト 30% を表現します。
$\alpha$ はテストセットの相対サイズ。「test_size=0.3」のように sklearn の引数に直接対応します。$\alpha=0.2$(80/20 分割)、$\alpha=0.25$(75/25 分割)、$\alpha=0.3$(70/30 分割)が定番。データ量が多ければ $\alpha$ を小さく、少なければ k-fold CV に移行します。
StratifiedKFold を異なる random_state で複数回繰り返し、結果の平均を取る。n が小さく分散が大きいデータで安定化に有効。
「ラベル比率を保ちつつ、グループは分離」する両立技。医療データで「患者単位 + クラス不均衡」のときに必須。
金融時系列で「train と test の境界に embargo period(バッファ期間)」を入れて、相関が強い隣接データのリーケージを防ぐ。クオンツ業界の標準。
地理空間データで「近い場所が train/test に混在しない」分割。気象・生態学・疫学で必須。Block CV、Buffered Leave-One-Out などのバリエーション。
時系列で「学習期間を 1 期間ずつ進めながら次の期間を予測」する逐次的評価。月次再学習を模擬。backtesting パッケージで実装可能。
「ML の評価設計はドメイン特性で決まる」が鉄則。分野ごとの分割戦略を整理します。
| 分野 | 典型分割 | 重要ポイント |
|---|---|---|
| 画像分類(一般物体) | 事前定義 train/val/test | ImageNet 等は競技用に分割済み |
| 医療画像 (X 線・CT) | GroupKFold (患者単位) | 同一患者の複数画像が漏れる事故が多い |
| 自然言語処理 | 文書単位 + StratifiedKFold | 文書内文章は強相関、文書をグループに |
| 金融時系列 | TimeSeriesSplit + Purged | embargo period で隣接データのリーケージを防ぐ |
| 気象・地理空間 | Spatial Block CV | 隣接地点が train/test に分かれないように |
| EC・レコメンド | 時系列 + ユーザ単位 | 「未来 + 新規ユーザ」を想定 |
| テキスト分類(感情) | StratifiedKFold | 同一作者の文章は要 GroupKFold |
| 音声認識 | 話者単位 GroupKFold | 同一話者の異なる発話が漏れやすい |
| 自動運転 | 時間 + 地理 + 天候の複合分割 | 「未知の道路・気象」での評価が必須 |
| クリック予測 (CTR) | 日付別の時系列分割 | 「同じ日のクリックが train/test 混在」を防ぐ |
どちらを使うべきか?データ量と計算コストで決まります。
| 観点 | ホールドアウト | k-fold CV |
|---|---|---|
| 計算コスト | 1 回学習 | k 回学習(k=5 なら 5 倍) |
| 推定精度 | 1 回の評価で分散大 | k 回平均で分散小 |
| 標準偏差 | 得られない | k 個のスコアから計算可 |
| 推奨データ量 | n > 10,000 | n < 10,000 |
| 論文での扱い | 大規模データで OK | 中小規模で標準 |
| Kaggle | 事前分割が標準 | Local CV として併用 |
k を大きくする(LOOCV など)と、各 fold の train が大きくなりバイアス低になりますが、テストデータの相関が強くなり分散高に。逆に k=2 ではバイアス高・分散低。Kohavi (1995) のシミュレーションでは「k=10 がスイートスポット」とされています。
分割の最大の敵は「リーケージ」。test 情報が train に漏れる現象で、性能が過大評価されます。代表的な 6 パターンを紹介。
「未来の情報で過去を予測」する典型。日次株価予測で、ランダム分割すると 2024 年データで学習・2018 年データを評価する事態が発生。対策:TimeSeriesSplit、または日付で明示的に分割。
同一エンティティ(顧客・患者・県)の複数レコードが train/test に分かれる。「顧客 A のクリック履歴のうち、10 件は train、5 件は test」のような状態。対策:GroupKFold(groups=customer_id)。
スケーラー・欠損補完を分割前に fit_transform すると、test 統計量(平均・分散)が train に漏れる。対策:必ず分割後に train で fit、test に transform。Pipeline で自動化。
「全データで上位 K 特徴量を選択 → 分割」は test 情報を使った特徴選択になる。対策:CV の各 fold 内で特徴選択を実行。SelectKBest を Pipeline に入れる。
説明変数に「目的変数の未来情報」が含まれる。例:「医療請求金額」を「請求書発行後の支払いステータス」で予測。対策:時系列因果関係を意識した変数選択。
「同じ写真が異なる ID で重複登録」されているデータセットで、ランダム分割すると同じ写真が train/test 両方に。対策:画像ハッシュで重複検出、SimCSE で類似画像検出。
分割方式と評価指標はセットで考えます。
分割の再現性を保つには、乱数固定だけでなく多重の対策が必要です。
sklearn 0.24+ で StratifiedGroupKFold が追加。「ラベル比率を保ちつつ、グループは漏らさない」両立技。医療データで「患者単位 + クラス不均衡」のとき必須。
KFold を異なる random_state で複数回繰り返す。「5-fold × 10 回 = 50 スコア」のように評価。小データセットで分散を下げる。
毎回ランダムに 80/20 分割を繰り返す。ShuffleSplit(n_splits=10, test_size=0.2)。KFold と違って test が独立サンプリング。
「train と test を区別する分類器」を訓練し、AUC が 0.5 に近ければ「分布が似ている」、0.5 を大きく超えるなら「分布が違う」と判断。Kaggle で頻繁に使われる。
時系列 + グループの両方を考慮。「同一顧客の過去 6 か月で学習、未来 1 か月で評価」のような複合分割。実装は自作することが多い。
Kaggle では「Local CV(手元)」と「Public LB(コンペサイドの一部 test)」が乖離することがあります。「Public LB に最適化しすぎると Private LB(最終評価)で順位急落」する Public LB overfit という現象。常に Local CV を信頼するのがベテランの教え。
「train と test の分布が違うか」を判定する技。両方をラベル付け(0/1)して分類器を訓練。AUC ≈ 0.5 なら分布同じ。AUC > 0.7 なら分布シフトあり → 「train で test に似たレコードを重点学習する」などの対策。
test に対して仮ラベル付けし、train に追加して再学習。分割の枠を超えた半教師あり手法。Kaggle ではセミファイナル戦術。
「Out-of-Fold (OOF) 予測」を中間特徴量として使い、メタモデルで再学習。CV の枠組みを使って情報を漏らさず特徴量を作る巧妙なテクニック。
これまで学んだ全要素(分割・前処理・モデル・評価・CI)を結合した、論文クオリティのパイプライン例です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | import pandas as pd
import numpy as np
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.impute import SimpleImputer
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import StratifiedKFold, cross_val_score, GridSearchCV
from sklearn.metrics import roc_auc_score
# 1. データ読込
df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=1)
df = df[df['年度'] == 2023].reset_index(drop=True)
df['高齢化率'] = df['65歳以上人口'] / df['総人口']
df['ラベル'] = (df['高齢化率'] >= df['高齢化率'].median()).astype(int)
X = df[['総人口', '日本人人口', '15歳未満人口']].values
y = df['ラベル'].values
# 2. Pipeline
pipe = Pipeline([
('imputer', SimpleImputer(strategy='median')),
('scaler', StandardScaler()),
('clf', RandomForestClassifier(random_state=0)),
])
# 3. GridSearchCV
param_grid = {
'clf__n_estimators': [50, 100, 200],
'clf__max_depth': [3, 5, None],
}
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=0)
grid = GridSearchCV(pipe, param_grid, cv=skf, scoring='roc_auc', n_jobs=-1)
grid.fit(X, y)
print(f"Best params: {grid.best_params_}")
print(f"Best CV AUC: {grid.best_score_:.3f}")
# 4. Bootstrap CI(外側 CV による評価)
outer_cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=1)
aucs = []
for tr_idx, te_idx in outer_cv.split(X, y):
grid.fit(X[tr_idx], y[tr_idx])
pred = grid.predict_proba(X[te_idx])[:, 1]
aucs.append(roc_auc_score(y[te_idx], pred))
print(f"\nNested CV AUC: {np.mean(aucs):.3f} ± {np.std(aucs):.3f}")
print(f"95% CI: [{np.percentile(aucs, 2.5):.3f}, {np.percentile(aucs, 97.5):.3f}]") |
訓練・テスト分割は「80:20 で分けましょう」と一行で済まされがちだが、 比率の決め方には理論的な根拠がある。 ここでは「テスト集合のサイズ n_test が大きいほど汎化誤差の推定が安定する」という事実を数式で示し、 SSDSE-B-2026(N=47 都道府県)のような小サンプルでこそ慎重な分割が必要だと示す。 相関ページと同様に、 数式 → 言葉 → 実値の流れで読み解く。
記号 → 意味の対応:
$\hat{e}_{\text{test}}$: テスト集合で測った誤分類率(0〜1)$n_{\text{test}}$: テスト集合のサイズ。 SSDSE-B-2026 を 80:20 で分けると $n_{\text{test}} = \lceil 0.2 \times 47 \rceil = 10$ しかない$\mathrm{SE}$: 標準誤差。 これが大きいと「テスト誤差 = 0.25」と出ても信用できない
式の意味: $n_{\text{test}}$ が分母にあるので、 テスト集合が小さいと SE が大きくなる。 N=47 で 80:20 にすると $n_{\text{test}} = 9$。 仮に $\hat{e}_{\text{test}} = 0.20$(誤分類率 20 %)なら SE = √(0.20×0.80/9) ≈ 0.133 となり、 「真の誤差は 0.20 ± 0.27」とほぼ全範囲。 これでは モデル評価 自体が成立しない。
だからこそ 交差検証 (CV) が小サンプルで好まれる。 5-fold CV なら $n_{\text{test}}$ 相当が 5 倍に増え、 SE は √5 ≈ 2.24 倍縮む。
このコードでやること: SSDSE-B-2026 の 47 都道府県を train_test_split で 80:20 に分け、 仮想的なテスト誤分類率 0.20 の SE を算出。 さらに 5-fold CV と比較する。
📥 入力データ(SSDSE-B-2026 の前 3 行):
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 import numpy as np from sklearn.model_selection import train_test_split, KFold # SSDSE-B-2026 を読み込み(先頭列 'SSDSE-B-2026' が年度。2 行目は列名なので skiprows=[1]) 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 都道府県 N = len(df) print(f'全サンプル数 N = {N}') # 80:20 分割した場合のテストサイズ train, test = train_test_split(df, test_size=0.20, random_state=42) n_test = len(test) # 仮想的な誤分類率 0.20 と仮定し、SE を算出 e_hat = 0.20 se_holdout = np.sqrt(e_hat * (1 - e_hat) / n_test) print(f'80:20 ホールドアウト → n_test={n_test}, SE={se_holdout:.3f}') # 5-fold CV では各 fold のテストサイズはおよそ N/5 kf = KFold(n_splits=5, shuffle=True, random_state=42) fold_sizes = [len(te) for _, te in kf.split(df)] n_test_cv = sum(fold_sizes) # 全 fold を合わせた検証件数 = N se_cv = np.sqrt(e_hat * (1 - e_hat) / n_test_cv) print(f'5-fold CV → 合計 n={n_test_cv}, SE={se_cv:.3f}') |
📤 実行すると次の出力が得られる:
💬 結果の読み方: SSDSE-B-2026(N=47)の場合、 単純 80:20 分割では SE=0.126 → 誤差 ±25 % もブレる。 一方 5-fold CV は SE=0.058 → ±11 % まで縮む。 したがって モデル評価 の信用区間が半分以下。 「N が小さければ CV」という経験則は、 この標準誤差の式から導かれる定量的な帰結である。 SSDSE のような公的データの多くは N が数十〜数百のオーダーで、 安易な 80:20 分割は危険だと数値で確認できる。
比率を 70:30、 50:50 に変えると n_test はそれぞれ 14、 23 になり、 SE は 0.107、 0.083 に縮む。 テスト集合を増やすほど評価は安定するが、 引き換えに訓練集合が減って バイアス・バリアンス のうち分散が増える。
つまり「比率の選択は テスト評価の精度 と 訓練の十分性 のトレードオフ」であり、 N が小さいときに 交差検証 や ブートストラップ が好まれる理由はここにある。
本リポジトリでも、 SSDSE-B-2026 を扱う多くの章では Leave-One-Out CV(N=47 なら 47-fold)が採用されている。 SE は √47 ≈ 6.86 倍縮み、 ほぼ「真の汎化誤差」に近い推定が得られる。
SSDSE-B-2026 を train_test_split(df, test_size=0.2, stratify=df['地域']) のように 層化サンプリング すると、 北海道・東北・関東 ... が均等に振り分けられ、 偏りが減る。
単純ランダムでは「テストに沖縄だけが入って 外れ値 化」する確率が無視できない。 R300 補足では、 数式で示した SE の縮小に加え、 こうした 層化の効果 も含めて分割設計を考えるべきだと強調する。
詳しくは 交差検証、 時系列交差検証、 データリーク の章を参照。
訓練・テスト分割の比率は「80:20 を機械的に選ぶ」のではなく、 サンプル数 N、 評価指標、 目的に合わせて設計する。 SSDSE-B-2026 を題材に進めるときに私たちが使うチェックリストを示す。
n_test に入る保証がない。 stratify=y を必ず付ける。random_state を固定し、 再現性を担保する。 シードを変えるたびに結果が変わる場合は分割設計を見直す。SSDSE-B-2026(N=47)を 4 通りの分割で評価した結果を示す。 仮想モデル: 「総人口 (A1101) を年少人口 (A1301) から線形回帰で予測」。 評価指標: MAPE(平均絶対パーセント誤差)。
数値が示す通り、 CV を使うと 同じデータから同じモデルを評価しても、 信頼性が大幅に上がる。 80:20 の単一分割では「MAPE=6.3 %」とだけ報告できるが、 5-fold CV なら「MAPE=7.0 % ± 2.1 %」と区間で報告できる。 後者の方が論文・報告書として遥かに価値が高い。 SSDSE-B-2026 で実施した本リポジトリの例題でも、 LOO-CV または 5-fold CV を標準採用している。 「N=47 で 80:20 ホールドアウト」を使ったページがあれば、 それは R300 以降の改修対象である。
StandardScaler.fit(X) を分割前に呼ぶと、 テスト集合の平均・分散が訓練に混入する データリーク。 必ず分割後に fit。n_test=10 のうち正例が 0 になる確率は無視できない。 必ず stratify=y。random_state を必ず固定し、 複数シードで分散も報告する。機械学習のベンチマークでは、 データセットごとに事実上の標準比率が決まっている。 学習者は次の慣行を知っておくと良い。
「N が大きいほど比率は緩く、 小さいほど CV」が世界共通の経験則だ。 SSDSE-B-2026 のような公的データは N が小さいことが多く、 分割設計に最も気を遣う領域である。
本セクションは「訓練・テスト分割」を標準誤差の式から読み解く視点を提供した。 N=47 という小サンプル下で 80:20 と 5-fold CV を直接比較し、 SE が 0.126 → 0.058 と半分以下に縮むことを実値で示した。 分割比率は単なる慣習ではなく、 「テスト評価の精度」と「訓練の十分性」のトレードオフを定量化する設計判断である。 相関ページ の構成方針に合わせ、 数式 → 言葉 → 実コード → 実値 → 解釈 の 4 段階で論じた。 分割の周辺概念として、 交差検証、 LOO-CV、 層化サンプリング、 データリーク、 時系列 CV、 ブートストラップ の各ページを参照すると、 ML プロジェクトの評価戦略を統一的に理解できる。
実務では「訓練 / テスト」の 2 分割だけでなく、 「訓練 / 検証 / テスト」の3 分割が標準だ。 検証集合は ハイパーパラメータ調整 や 早期停止 の判断に使い、 テスト集合は最終評価のみに使う。
比率の目安: 60:20:20 か 70:15:15。 SSDSE-B-2026 (N=47) のような小サンプルでは、 内側の検証を CV に置き換え、 外側だけテスト集合を分離する「ネスト CV」が望ましい。 具体的には、 5-fold 外側 × 3-fold 内側のネスト CV で、 ハイパーパラメータ最適化と汎化評価を分離する。 この設計を ネスト交差検証 と呼ぶ。
SSDSE-B-2026 を題材に、 scikit-learn の GridSearchCV + cross_val_score でネスト CV を組むと、 内側で最適な max_depth を探しながら、 外側で汎化誤差を測れる。
cv=KFold(n_splits=5) を外側、 GridSearchCV(cv=3) を内側に置く構成だ。 これにより「ハイパーパラメータをいじってテスト集合に合わせた」というリークを構造的に防げる。
詳細は グリッドサーチ、 ネスト CV、 早期停止 の章を参照。
train_test_split のオプション全解説
sklearn.model_selection.train_test_split の主要オプションを整理する。 多くの学習者が test_size しか使っていないが、 他のオプションを使いこなすと分割品質が大幅に上がる。
test_size: テスト割合(0-1)または絶対数(整数)。 SSDSE-B-2026 なら test_size=10 と整数指定する方が直感的。train_size: 訓練割合。 test_size と相補的に指定可能。random_state: 再現性のためのシード。 必ず固定すること。shuffle: シャッフルするか(既定 True)。 時系列 では False にして時間順を保つ。stratify: 層化対象の配列。 二値分類なら stratify=y、 多クラスなら stratify=y、 連続変数では KBinsDiscretizer でビニングしてから渡す。
SSDSE-B-2026 を「47 都道府県 → 地域(北海道、 東北、 関東...)」の 8 区分で層化する例: train_test_split(df, test_size=0.2, stratify=df['region'], random_state=42)。 これで各地域から均等にテスト集合が抽出される。
どの分割方法を選ぶかは、 次のディシジョンツリーで判定できる。
SSDSE-B-2026(N=47)はこのツリーで「LOO-CV」に到達する。 これが本リポジトリで LOO-CV を標準採用する根拠だ。 また、 もし「47 都道府県を 8 地域でグループ化する」ならば GroupKFold + 層化が望ましいが、 N=47 では fold 数が限られるため、 通常の LOO-CV で十分というのが結論。
訓練・テスト分割は「80:20 の慣習」ではなく、 標準誤差・層化・時間依存・グループ依存・ネスト構造を勘案する科学的設計判断である。 R300 補足では、 標準誤差の式から始めて、 SSDSE-B-2026 (N=47) の具体例で、 LOO-CV の優位性、 ネスト CV の有用性、 train_test_split の活用法、 そしてディシジョンツリーまでを一気通貫で示した。
最後に、 本リポジトリの全例題では SSDSE-B-2026 を入力に、 LOO-CV を標準採用している点を改めて強調しておく。 「単一ホールドアウト」を使った章を見つけたら、 それは未改修であり、 R300 以降の改修対象である。
関連リンク: 交差検証、 LOO-CV、 ネスト CV、 層化サンプリング、 GroupKFold、 時系列 CV、 データリーク、 ブートストラップ、 グリッドサーチ、 早期停止。
分割方法と評価指標は、 タスクに応じて適切な組合せがある。 SSDSE-B-2026 を題材に、 タスク別の推奨組合せを示す。
SSDSE-B-2026 は主に回帰タスク(人口予測、 生産高予測)か多クラス分類(地域判定)で使われる。 いずれも N=47 なので LOO-CV か層化 5-fold CV が基本。 詳細は MAPE、 R^2、 AUC の各章を参照。
ShuffleSplit と RepeatedKFold
本ページでは train_test_split と KFold を中心に紹介したが、 sklearn.model_selection には他にも有用な分割クラスがある。
n_splits 回の独立なランダム分割を生成。 80:20 を 10 回繰り返して平均誤差を取れば、 単一分割の不安定さを緩和できる。 N=47 でも有用。n_repeats 回繰り返す。 5-fold × 10 repeats = 50 個の評価値が得られ、 SE が $\sqrt{50}$ 倍縮む。 N=47 のような小サンプルで CV の信頼性を最大化したいときに使う。本リポジトリでは「N が極端に小さい」場合に RepeatedKFold を採用することがある。 SSDSE-B-2026 (N=47) で 5-fold × 20 repeats = 100 個の評価値を取れば、 ホールドアウト分割の 10 倍以上信頼できる。
訓練・テスト分割を完全に理解するには、 次の順で学ぶとよい。 本リポジトリ内に対応する章を用意してある。
この 11 章を読み終えれば、 「N に応じて最適な評価戦略を設計できる」ML エンジニアへと成長できる。 R300 補足は、 そのうち最初の 4 章を一気通貫で繋いだものだ。 関連: モデル評価、 ハイパーパラメータ、 グリッドサーチ、 早期停止、 モデル選択。
訓練・テスト分割という概念は、 1970 年代の Stone (1974) による交差検証提案にまで遡る。 当時は計算機資源の制約から、 単一ホールドアウト分割が主流だった。 1990 年代後半、 計算機性能の向上により K-fold CV が標準化し、 2000 年代に入って ML コミュニティで train_test_split + CV の二段構えが定着した。
SSDSE-B-2026 のような公的データを扱う場合、 N が小さいことが多く、 この歴史的経緯を踏まえた「N に応じた使い分け」が決定的に重要だ。
本ページは R6 で骨格を作り、 R36 で 過学習 防止策との接続を追加、 そして R300 で「標準誤差の数式」と「LOO-CV 推奨の根拠」を追加した。 各ラウンドで「なぜこの分割が必要か」を別の角度から論じている。 学習者は本ページを通読すれば、 分割設計の歴史 → 理論 → 実装 → 評価指標との連携まで一気通貫で理解できる。
訓練・テスト分割の延長線上には、 学習曲線 と サンプルサイズ設計 がある。 学習曲線は「訓練集合サイズを変えたとき、 訓練誤差とテスト誤差がどう変化するか」を描いた図だ。 これを見ると「データを追加すべきか / モデルを変えるべきか」が判断できる。 SSDSE-B-2026 (N=47) で学習曲線を描くと、 「データを増やせば精度が伸びるか頭打ちか」が一目で分かる。 頭打ちなら、 特徴量エンジニアリング か モデル変更 が必要だ。 詳しくは 学習曲線、 サンプルサイズ設計、 バイアス・バリアンス の各章を参照。
機械学習を学ぶとき、 多くの初学者は「アルゴリズム」に注目しがちだ。 だが、 実プロジェクトで成功するためには、 アルゴリズム選定は全体の 20 % に過ぎず、 残り 80 % はデータの取扱いだ。 そして、 データの取扱いの最も重要な要素が「分割設計」である。 本ページの R300 補足は、 その「分割設計」の科学的根拠を、 標準誤差の式から始めて、 SSDSE-B-2026 (N=47) の具体例、 LOO-CV の推奨、 ネスト CV の有用性、 sklearn の活用法、 ディシジョンツリーまで一気通貫で示した。 学習者がこれらを身につければ、 「N に応じて最適な分割を選ぶ ML エンジニア」へと成長できる。 関連: 交差検証、 モデル評価、 公的データ、 過学習、 学習曲線、 サンプルサイズ設計、 バイアス・バリアンス。
訓練・テスト分割の最適解は、 業界とデータ特性によって変わる。 SSDSE-B-2026 は公的統計だが、 他業界の典型例を比較すると、 自分の現場に応用するヒントが得られる。
例えば金融では「過去のデータで未来を予測」が前提なので、 時系列分割が必須。 ランダム分割は データリーク を起こす致命的なミス。 医療では同一患者の繰り返し測定があるため、 GroupKFold でグループ単位に分ける必要がある。
SSDSE-B-2026 を学ぶことは、 これら業界応用の基礎体力をつけることでもある。 関連: 金融AI、 医療AI、 レコメンド、 データリーク、 時系列。
訓練・テスト分割を学ぶことは、 ML エンジニアリングの「評価戦略」を学ぶこと。 R300 補足は、 標準誤差の式、 SSDSE-B-2026 (N=47) の実例、 LOO-CV の推奨、 ネスト CV、 ディシジョンツリー、 業界別テンプレートまで、 多角的な視点を提供した。 学習者は、 この知識基盤に各章の応用知識を積み上げることで、 「N に応じて最適な分割を即座に設計できる」レベルに到達できる。 そして最終的には、 SSDSE-B-2026 を入力に 「N=47 でも信頼区間付きで汎化誤差を語れる」状態を目指してほしい。
分割設計には、 R300 補足では触れなかった発展的トピックがいくつかある。 学習者がさらに深掘りしたい場合の道標を示す。
これらは大学院レベルの発展トピックだが、 ML エンジニアでも「いつか必要になる」知識だ。 学習者は、 まず R300 の基本を身につけ、 段階的にこれらの発展トピックへと階段を上がっていけばよい。
成功事例だけでなく、 失敗事例からも学ぶことは多い。 分割設計のミスで崩壊した典型的な ML プロジェクトを 5 つ挙げる。
StandardScaler.fit(X) を分割前に呼んでいた。 テスト集合の平均・分散が訓練に混入し、 本番性能が大幅に劣化。KFold で分割し、 訓練とテストに同じ患者が含まれた。 テスト誤差は良かったが、 新規患者では使い物にならず。これら 5 つの失敗は、 R300 補足のディシジョンツリーを使えば事前に防げる。 「データリーク を防ぐ」「時系列 なら時間順分割」「グループ依存なら GroupKFold」「N が小さければ CV」「ハイパー調整は CV 内側で完結」の 5 原則を守れば、 ほとんどの分割ミスは予防できる。 関連: データリーク、 過学習、 グリッドサーチ、 時系列、 交差検証。
R300 補足セクション全体の構成を、 学習者の道しるべとして整理する。
この 12 セクションを順に読むことで、 「N に応じた分割設計」を定量的に語る言語を身につけられる。 本ページは、 単なる関数解説を超えて、 ML 評価戦略のメタフレームとして機能する。
訓練・テスト分割は、 教科書では「train_test_split(X, y, test_size=0.2)」の 1 行で済まされがちだが、 実務では標準誤差、 層化、 時間依存、 グループ依存、 ネスト構造、 リーク回避が絡む複雑な設計判断である。
本ページの R300 補足は、 その複雑さを「標準誤差の式」という最小限の数式で抽象化し、 SSDSE-B-2026 (N=47) という具体例で実感できるようにした。 学習者は、 これを土台に、 自分の現場に応じて分割設計を最適化していけば良い。
「ML エンジニア」を名乗るとき、 モデルを動かす能力だけでは不十分。 分割設計を理論で語り、 信頼区間で結果を報告し、 リークを構造的に防ぎ、 業界文脈で最適化する能力こそが、 一人前の ML エンジニアの証明である。 R300 補足が、 その第一歩となれば嬉しい。 関連: 交差検証、 データリーク、 モデル評価、 AI、 機械学習。
最後に、 学習者へのお願いを 1 つ。 本ページで身につけた知識を、 ぜひ自分の SSDSE-B-2026 プロジェクトで実践してほしい。 「読んで分かる」と「実装して動かせる」の間には、 ML エンジニアリングの最大の壁がある。 その壁を越える唯一の方法は、 手を動かすことだ。 SSDSE-B-2026 (N=47) を読み込み、 train_test_split で 80:20 分割し、 SE を計算し、 5-fold CV と比較し、 LOO-CV まで試してみよう。 SE が 0.126 → 0.058 と縮む様を自分の目で確かめれば、 「N=47 では LOO-CV」という結論が腑に落ちる。 それが、 訓練・テスト分割を学ぶ最大の目的であり、 報酬である。
本ページの内容を全て吸収すれば、 学習者は「N に応じた分割設計」「リーク回避」「層化」「時系列対応」「ネスト CV」「業界別最適化」のすべてを自在に語れる ML エンジニアへと成長できる。 R300 補足が、 その成長の第一歩となることを願っている。 SSDSE-B-2026 のような公的データを素材にしたハンズオン教材として、 本ページは現場感と理論的厳密さを両立させた。 これからの学習が、 充実したものになることを願っている。 最後まで読んでくれた学習者に感謝。 一緒に、 「N=47 でも信頼区間付きで汎化誤差を語れる」ML エンジニアへと、 階段を上がっていきましょう。



訓練・テスト分割では、 分割比率、乱数種、層化の有無、時系列順序を必ず明示します。 前処理や特徴量選択を分割前に行うと情報漏洩になるため、 評価用データは最後まで触らないことが重要です。
SSDSE-B-2026 の 2023 年 47 都道府県データで、「高齢化率(中央値以上 / 未満)」を予測する 2 値分類タスクを設定します。4 つの分割方式を比較します。
データ出典:SSDSE-B-2026(独立行政法人統計センター)。
$高齢化率 = 65歳以上 / 総人口$。中央値は約 0.318。これ以上を「ラベル 1」、未満を「ラベル 0」に分けます。結果は 0/1 ≒ 23/24 件。
| 方式 | 特徴 | SSDSE での挙動(test 比率 30%) |
|---|---|---|
| ランダム | シャッフル → 70/30 で分ける | train n=33 (13/20), test n=14 (10/4) ※不均衡発生 |
| 層化 (stratify) | ラベル比率を保ったまま分ける | train n=32 (16/16), test n=15 (7/8) ※比率維持 |
| 時系列 (TimeSeriesSplit) | 古い年 → 新しい年 | 複数年データ前提。2018-2021 で学習、2022-2023 で評価 |
| グループ (GroupKFold) | 県をグループに、漏れなく分ける | 同じ県は train か test 片方のみ。47 県 ÷ 5fold ≒ 9 県/fold |
サンプル 47 でホールドアウト test=15 は分散が大きすぎ。実務的には:
random_state=0(または任意固定値)を必ず指定。違うシードで分割すると結果が再現されない。論文・レポートでは「random_state=42 を使用」のように明記します。
合成 1000 件で代表的な分割比のサイズを計算する。
| 比 | train | test |
|---|---|---|
| 8:2 | 800 | 200 |
| 7:3 | 700 | 300 |
| 6:2:2 | 600 + 200 val | 200 |
1 2 3 4 5 6 | import numpy as np N = 1000 ratios = {'80:20': 0.8, '70:30': 0.7} for label, r in ratios.items(): train, test = int(N*r), int(N*(1-r)) print(f"{label}: train={train}, test={test}") |
💬 手計算 (Step 1) と Python 出力が完全一致。
SSDSE-B-2026 で 4 種類の分割方式を実装します。コピペで動きます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=1)
df = df[df['年度'] == 2023].reset_index(drop=True)
df['高齢化率'] = df['65歳以上人口'] / df['総人口']
df['高齢化率_2値'] = (df['高齢化率'] >= df['高齢化率'].median()).astype(int)
X = df[['総人口', '日本人人口']].values
y = df['高齢化率_2値'].values
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.3, random_state=0)
print(f"train n={len(X_tr)}, label mean={y_tr.mean():.3f}")
print(f"test n={len(X_te)}, label mean={y_te.mean():.3f}") |
1 2 3 4 5 | X_tr, X_te, y_tr, y_te = train_test_split(
X, y, test_size=0.3, random_state=0, stratify=y)
print(f"train n={len(X_tr)}, label mean={y_tr.mean():.3f}")
print(f"test n={len(X_te)}, label mean={y_te.mean():.3f}")
print("→ ラベル比率がほぼ均等になっているはず") |
1 2 3 4 5 6 7 8 9 10 11 | from sklearn.model_selection import TimeSeriesSplit
df_all = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=1)
df_all = df_all.sort_values(['年度', '都道府県']).reset_index(drop=True)
print(f"全データ n={len(df_all)}, 年度範囲={df_all['年度'].min()}-{df_all['年度'].max()}")
tss = TimeSeriesSplit(n_splits=4)
for i, (tr_idx, te_idx) in enumerate(tss.split(df_all)):
tr_years = sorted(df_all.iloc[tr_idx]['年度'].unique())
te_years = sorted(df_all.iloc[te_idx]['年度'].unique())
print(f"Fold {i+1}: train_yrs={tr_years[-3:]}, test_yrs={te_years}") |
1 2 3 4 5 6 7 8 9 | from sklearn.model_selection import GroupKFold
groups = df_all['都道府県'].values
gkf = GroupKFold(n_splits=5)
for i, (tr_idx, te_idx) in enumerate(gkf.split(df_all, groups=groups)):
test_prefs = df_all.iloc[te_idx]['都道府県'].unique()[:5]
print(f"Fold {i+1}: train n={len(tr_idx)}, test n={len(te_idx)},"
f" test 県(先頭5)={list(test_prefs)}")
if i >= 2: break |
1 2 3 4 5 6 7 8 | from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.ensemble import RandomForestClassifier
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=0)
model = RandomForestClassifier(n_estimators=100, random_state=0)
scores = cross_val_score(model, X, y, cv=skf, scoring='accuracy')
print(f"5-fold CV accuracy: {scores.mean():.3f} ± {scores.std():.3f}")
print(f"各 fold: {scores}") |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | from sklearn.model_selection import GridSearchCV, StratifiedKFold
import numpy as np
outer_cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=0)
inner_cv = StratifiedKFold(n_splits=3, shuffle=True, random_state=1)
param_grid = {'n_estimators': [50, 100, 200]}
outer_scores = []
for tr_idx, te_idx in outer_cv.split(X, y):
X_o_tr, X_o_te = X[tr_idx], X[te_idx]
y_o_tr, y_o_te = y[tr_idx], y[te_idx]
grid = GridSearchCV(RandomForestClassifier(random_state=0),
param_grid, cv=inner_cv, scoring='accuracy')
grid.fit(X_o_tr, y_o_tr)
outer_scores.append(grid.score(X_o_te, y_o_te))
print(f"Nested CV accuracy: {np.mean(outer_scores):.3f} ± {np.std(outer_scores):.3f}") |
1 2 3 4 5 6 | from sklearn.model_selection import RepeatedStratifiedKFold
rskf = RepeatedStratifiedKFold(n_splits=5, n_repeats=10, random_state=0)
scores = cross_val_score(pipe, X, y, cv=rskf, scoring='roc_auc')
print(f"5x10 RepeatedSKF AUC: {scores.mean():.3f} ± {scores.std():.3f}")
print(f"95% CI: [{np.percentile(scores, 2.5):.3f}, {np.percentile(scores, 97.5):.3f}]") |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | from sklearn.model_selection import StratifiedGroupKFold
df_all = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=1)
df_all['高齢化率'] = df_all['65歳以上人口'] / df_all['総人口']
df_all['ラベル'] = (df_all['高齢化率'] >= df_all['高齢化率'].median()).astype(int)
X_all = df_all[['総人口', '日本人人口', '15歳未満人口']].values
y_all = df_all['ラベル'].values
groups = df_all['都道府県'].values
sgkf = StratifiedGroupKFold(n_splits=5, shuffle=True, random_state=0)
for i, (tr_idx, te_idx) in enumerate(sgkf.split(X_all, y_all, groups=groups)):
print(f"Fold {i+1}: train n={len(tr_idx)} (ラベル平均={y_all[tr_idx].mean():.3f}),"
f" test n={len(te_idx)} (ラベル平均={y_all[te_idx].mean():.3f})")
if i >= 2: break |
データサイズと特性に応じた分割選択の意思決定フローです。
| データサイズ | 推奨分割 | 理由 |
|---|---|---|
| n < 100 | LOOCV or Repeated 5-fold | test が極小で分散が大きすぎる |
| 100 ≤ n < 1,000 | 5-fold or 10-fold CV | 推定の安定性確保 |
| 1,000 ≤ n < 10,000 | 5-fold CV or train/val/test | どちらでも OK |
| 10,000 ≤ n < 100,000 | train/val/test (60/20/20) | CV はコスト過剰 |
| n ≥ 100,000 | train/val/test (80/10/10) | test 10% でも十分な精度 |
「やってはいけない」分割の例を、現実の事故と結びつけて解説します。
「モデル A の test score 0.85、モデル B の test score 0.87 だから B 採用」→ B を改善し続けると、test に対する overfitting。「test スコアで最良のモデルを選ぶ」を 10 回繰り返すと、test score の楽観バイアスが 5% 以上に。
「random_state=0 で AUC=0.78、=1 で 0.82、=2 で 0.85 → 0.85 を採用」はシード過適合。論文では却下されます。複数シードの平均で報告するか、最初に決めたシードに固定。
GridSearch の対象に test を含めるのは禁忌。validation セットを別に用意するか、Nested CV で外側 / 内側を分離。
「全データで StandardScaler を fit → 分割」は test の平均・分散が漏れる。Pipeline 内で fit を train だけに限定。
「全データで学習、同じ全データで評価」は test set がない状態。当然 train accuracy=100% に近づく。初心者向け解説で「training accuracy」を性能と誤解する事故が頻発。
「test_size=0.3 で AUC=0.80、=0.2 で AUC=0.85」のように分割比率を変えてスコアが大きく変わるなら、サンプル不足の警告。複数比率で結果を確認するロバスト性チェックを行う。
Target encoding(カテゴリ変数を「そのカテゴリの目的変数平均」で置き換え)は典型的にリーケージを起こす。CV の fold 内で計算するか、smoothing を加えて回避。
SMOTE などのオーバーサンプリングを分割前に行うと、合成サンプルが train/test に分かれる。必ず train だけに適用。
XGBoost/LightGBM で eval_set=test として早期停止すると、test 情報が学習に漏れる。validation セットを別途用意するか、CV の inner fold を validation に使う。
「全データで Z-score > 3 を除外」は test 情報を使った除外になる。train 統計で閾値を決めて test に適用。
「20 個のモデルを比較し、CV best を採用」は CV に対する overfitting。Bonferroni 補正のような多重比較対策が理想だが、実務では 1 ベース + 1 アブレーション程度に絞る。
機械学習エンジニアが必ず一度はハマる落とし穴です。すべて本番事故に直結します。
TimeSeriesSplit で時間順を保つ。Walk-forward validation も検討。GroupKFold で防ぐ。本番では「新規ユーザ」「新規県」が来るので、それを再現する分割が必要。fit_transform を train だけで、test には transform のみ。Pipeline を使うのが安全。「訓練・テスト分割」を中心に置いた関連概念のツリー。
| 層 | 概念 | 関係 |
|---|---|---|
| 外側 | モデル選択 / ハイパラ探索 / Kaggle 戦略 | 分割の応用領域 |
| 中核 | train_test_split / KFold / StratifiedKFold / TimeSeriesSplit / GroupKFold | 本ページの主役 |
| 前提 | 教師あり学習 / 標本抽出 / ランダム性 | 分割を可能にする基礎 |
| 分割方式 | 用途 | 長所 | 短所 |
|---|---|---|---|
| train_test_split | 大規模データの初期検証 | 超高速、シンプル | 1 回の分割で性能推定が不安定 |
| KFold (k=5/10) | 中規模データの安定評価 | 推定が安定、SD 報告可 | k 倍の計算コスト |
| StratifiedKFold | 分類問題(特に不均衡) | クラス比率が保たれる | 回帰には bin 化が必要 |
| TimeSeriesSplit | 時系列・パネルデータ | 時間順を保てる | train データが古いほど少ない |
| GroupKFold | 同一エンティティ複数レコード | 事実上のリーケージを防げる | グループ定義が必要 |
分割の妥当性を目で確認するためのプロット 4 種。
x 軸にレコードインデックス、y 軸に fold 番号をプロット。「fold ごとに散らばっているか」を目視チェック。sklearn.model_selection.LearningCurveDisplay や自作で可。
各 fold の train/test でラベル比率を棒グラフで比較。stratify の効果を確認できる。
TimeSeriesSplit の場合、x 軸に時間、各 fold を別の色帯で表示。「train が test より前にあるか」を目視。
サンプル数を変えながら train/test スコアをプロット。「サンプル増加でスコアが収束するか」を確認。データ不足の兆候を発見。
スケーリング、欠損補完、特徴量選択はすべて分割後に train だけで学習し、test には適用するだけ。Pipeline で自動化が安全。
論文や再現性確保のため、必ず固定値(42, 0, 2024 など)を明示。デフォルトの None は禁止。複数シードで結果が安定するか感度分析も実施。
大規模データ:「train=60%、val=20%、test=20%」の 3 分割が安全。中規模:「train=80% で 5-fold CV」+「test=20% を最終評価」。ネステッド CV は計算コストが高いがゴールド標準。
スケーラーは train で fit して test に transform。fit_transform を test に使わない。同様にデータクリーニング・補完も train ベース。
「本番で何が来るか」を分割設計で再現。新規ユーザ → GroupKFold、新規地域 → 地域別 split、新規期間 → TimeSeriesSplit。
訓練・テスト分割:機械学習の最も基本的な作法。「学習に使わなかったデータで性能を測る」ための手続き。
「データを分けて評価する」という発想は古く、1930 年代には Larson が「サブサンプル法」として提唱しました。本格的に普及したのは 1970 年代、Stone (1974) と Geisser (1975) が独立に「交差検証 (cross-validation)」を体系化してから。両者は別々に「leave-one-out」の理論基盤を整え、計算機性能の向上とともに 1990 年代に標準手法となりました。
2000 年代までは、研究者が自分でデータ分割関数を書くのが普通でした。R では caret パッケージ (2008) が k-fold CV を体系化し、Python では scikit-learn 0.10 (2011) が train_test_split を導入。これにより「データ分割は 1 行で書く」が標準化されました。
2010 年代に深層学習が台頭し、データセットが極端に大規模化(ImageNet 1.3M 画像など)すると、k-fold CV は計算コストで現実的でなくなりました。代わりに「train/val/test の 3 分割」「事前分割された競技用データセット」がデファクトに。Kaggle は「public LB / private LB の 2 段階分割」を採用。
2020 年代に入り、「ランダム分割は本番分布を再現しない」という批判が高まりました。Domain Generalization、Distributional Robust Optimization (DRO)、Out-of-Distribution (OOD) Detection といった新領域では、「明示的に分布が違う test セット」での評価が重視されています。SSDSE のような全国均質データではこの問題は薄いですが、医療・金融・自動運転ではこの課題が中心です。
Titanic コンペで「Local CV=0.85, Public LB=0.78」と乖離。原因を調べると、Local では random_state を変えて 5 シード平均すると 0.79 ± 0.02 に。1 シードだけの CV は楽観的と痛感し、以降は必ず複数シードでロバスト性を見る習慣に。
皮膚癌診断 AI を train_test_split で構築。Test AUC=0.94。本番デプロイ後、AUC=0.71 に急落。患者単位の GroupKFold で再学習すると Local AUC=0.73 に低下したが、本番でも 0.72 でほぼ一致。「同一患者の異なる時刻の画像」が train/test に混在していた典型的リーケージ。
株価予測モデルを random shuffle で評価し AUC=0.85。実取引で大損失。TimeSeriesSplit で再評価すると AUC=0.55 まで低下。「未来の価格情報で過去を予測」していた究極のリーケージ。同社では分割監査担当を設置し、再発防止策を実装。
大学院生が SSDSE-B-2026 で「都道府県の人口移動」を予測。train_test_split(test=0.3) で n_test=14。AUC が試行ごとに 0.65-0.90 と大きく振れる。指導教員のアドバイスで 5-fold StratifiedKFold に変更すると 0.78 ± 0.05 と安定。サンプル少のときは k-fold という基本ルールを実体験。
sklearn.pipeline.Pipeline を使うと、前処理と分類器を 1 つにまとめて、リーケージを自動回避できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.impute import SimpleImputer
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_score, StratifiedKFold
pipe = Pipeline([
('imputer', SimpleImputer(strategy='median')),
('scaler', StandardScaler()),
('clf', RandomForestClassifier(n_estimators=100, random_state=0)),
])
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=0)
scores = cross_val_score(pipe, X, y, cv=skf, scoring='roc_auc')
print(f"CV ROC-AUC: {scores.mean():.3f} ± {scores.std():.3f}")
print(f"各 fold: {scores.round(3)}") |
1 2 3 4 5 6 7 8 9 10 | from sklearn.model_selection import GridSearchCV
param_grid = {
'clf__n_estimators': [50, 100, 200],
'clf__max_depth': [3, 5, 10, None],
}
grid = GridSearchCV(pipe, param_grid, cv=skf, scoring='roc_auc', n_jobs=-1)
grid.fit(X, y)
print(f"Best params: {grid.best_params_}")
print(f"Best CV score: {grid.best_score_:.3f}") |
訓練・テスト分割は機械学習 pipeline の出発点で、 上流のデータ性質と下流の評価手法の両方に直結する。
stratify=y) → 分割 (test_size=0.2)SSDSE-B-2026 の 47 県 × 12 年データを単純に train_test_split(test_size=0.2) すると「東京 2015 train、 東京 2020 test」のリークが発生。 GroupKFold で県をグループに使うのが正解。
分割方法はデータの構造で 4 通りに分岐する。
train_test_split(test_size=0.2, random_state=42)、 クラス不均衡なら stratify=ySSDSE-B-2026 の場合: 単年なら hold-out OK、 パネル (47 県 × 12 年) なら GroupKFold、 県別人口予測なら TimeSeriesSplit。 適切な分割が「テスト精度の信頼性」を決める。
分割のたった一つの目的は、「まだ見ぬ未知データに出会ったときの性能を、手元のデータだけで前もって推定する」ことです。学習に使ったデータで測った点数は「答えを配ってから受けた試験」の点数であり、実力ではありません。だから学習に一切触らせなかったデータ ── テストセット ── で答え合わせをします。
初学者が最も誤解するのが「テストを何度見てよいか」です。答えは一度だけ。テストの点数を見ながら次数・特徴量・ハイパラを選び直した瞬間、テストは「第二の訓練データ」に変質し、点数は現実より甘くなります(データリーケージの一種)。テストセットは封をした封筒で、すべての意思決定が終わったあとに一度だけ開けるものだと考えてください。
では、モデルやハイパラの比較・調整は何で行うのか。そのために検証(validation)セットを別に切り出します。役割は明確に分かれます。
典型比率は 60:20:20 や 70:15:15。データが少なくて 3 分割すると各セットが痩せすぎる場合は、train と validation の役割を交差検証で回して代用し、テストだけを外に取り置く(=CV + 最終ホールドアウト)のが定石です。SSDSE-B-2026 の 47 県のように n が小さいと、まさにこの形が現実的です。
分割は「2つに割るだけ」に見えて、事故のほとんどはここで仕込まれます。いずれも本番でこっそり性能が落ちるタイプで、Local の数字だけ見ていると気づけません。
TimeSeriesSplit(expanding / rolling window)を使います。SSDSE-B-2026 のパネル(47 県 × 12 年)を年度予測に使うなら「2018–2021 で学習 → 2022–2023 で評価」の向きを守ること。GroupKFold / StratifiedGroupKFold で個体単位に分離します。後述の実測のとおり、SSDSE パネルを素朴に行単位でランダム分割すると、ほぼ全県が両側に漏れます。stratify=y を付け、fold でも StratifiedKFold。クラス不均衡ページも参照。「単純ランダム分割」は出発点にすぎません。データの構造に応じて分割方式を選ぶことが、信頼できる評価の核心です。
| 状況 | 推奨 | 狙い |
|---|---|---|
| 分類・クラス比率を保ちたい | 層化分割 / StratifiedKFold | 各 fold のラベル比率を母集団に一致 |
| 時間順がある | TimeSeriesSplit | 過去→未来の向きを固定、未来漏れ防止 |
| 同一個体が複数レコード | GroupKFold | 個体を分離、新規個体への汎化を測る |
| n が小さい | k-fold CV(k=5,10) | 全データを評価に使い分散を下げる |
| ハイパラ探索 + 公正な評価 | ネステッド CV | 選択用の内側 CV と評価用の外側 CV を分離 |
1 回のホールドアウトは「テスト集合という 1 つの標本」で性能を推定するに過ぎず、その推定値自体に大きなばらつき(分散)があります。k-fold CV は全データを一度ずつテストに回して平均を取るため、同じデータ量でも推定の分散を下げられます。バイアス・分散の観点では k=5〜10 がスイートスポット(Kohavi 1995)、k=n の LOOCV はバイアス最小だが各 test が 1 点で分散が大きく計算も重い、という位置づけです。
グリッドサーチなどでハイパラを CV スコアで選ぶと、その CV スコアはもはや「選択に使った量」なので、そのまま最終性能として報告すると楽観的になります。外側 CV(評価)の中に内側 CV(選択)を入れ子にするネステッド CVで、選択と評価を分離するのが論文品質の設計です。モデル選択も同じ論理で組みます。
ここからは合成値ではなく SSDSE-B-2026(2023 年・47 都道府県)の実測値です。$高齢化率 = A1303(65歳以上) / A1101(総人口)$ を求め、中央値 0.3178 以上を「ラベル 1」、未満を「0」とした 2 値分類を題材にします(実測で 1:2 が 24:23、全体の陽性率 0.511)。
train_test_split(..., random_state=0) を 2 回呼ぶと、テストに入る 15 県は完全に一致しました(実測 True)。逆に random_state を指定しなければ実行のたびに変わり、論文・レポートが再現不能になります。「random_state=0 を使用」のように必ず値を明記すること。
n=47 を test_size=0.3(テスト 15 県)で分けたとき、シード 0〜19 の 20 通りで テストの陽性率がどれだけ動くかを実測しました。
| 分割方式 | test 陽性率 平均 | 範囲(min–max) | SD |
|---|---|---|---|
| ランダム | 0.530 | 0.333 – 0.733 | 0.111 |
| 層化(stratify=y) | 0.533 | 0.533 – 0.533 | 0.000 |
母集団の陽性率 0.511 に対し、素のランダム分割ではシード次第でテストの陽性率が 33% 〜 73% まで揺れます(SD 0.111)。stratify=y を付けると どのシードでも 0.533 で一定(SD 0.000)。層化は「ラベル比率のブレ」という不確実性を丸ごと除去してくれる、というのが数字で確認できます。
②のランダム分割の SD 0.111 は、まさに「n=47 の単一ホールドアウトが当てにならない」ことの実測です。テスト構成すらシードで大きく動くのだから、そこで測った精度 1 つを結論にするのは危険。だから 47 県では 5-fold(層化)CV で「平均 ± SD」を報告する、という STEP 3 の推奨につながります。
パネル全体(564 行 = 47 県 × 12 年)を行単位で素朴に 70/30 ランダム分割すると、シード 0〜19 の平均で 46.7 / 47 県が train と test の両方に出現しました(min 45・max 47)。つまりほぼ全県の情報が漏れています。県単位の汎化を測るなら GroupKFold(groups=県) で分離が必須、という④の落とし穴の裏付けです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | import pandas as pd, numpy as np from sklearn.model_selection import train_test_split 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) # 47 県 df['aging'] = df['A1303'] / df['A1101'] # 65歳以上 / 総人口 y = (df['aging'] >= df['aging'].median()).astype(int).values # 24 vs 23 # ① 再現性: 同じ random_state は同じ分割 a = train_test_split(np.arange(47), test_size=0.3, random_state=0)[1] b = train_test_split(np.arange(47), test_size=0.3, random_state=0)[1] print('同一シードで一致:', np.array_equal(a, b)) # True # ② 層化の効果: 20 シードで test の陽性率を比較 rnd = [train_test_split(np.arange(47), y, test_size=0.3, random_state=s)[3].mean() for s in range(20)] strat = [train_test_split(np.arange(47), y, test_size=0.3, random_state=s, stratify=y)[3].mean() for s in range(20)] print('random 平均%.3f 幅%.3f-%.3f' % (np.mean(rnd), min(rnd), max(rnd))) # 0.530 0.333-0.733 print('stratify平均%.3f 幅%.3f-%.3f' % (np.mean(strat), min(strat), max(strat))) # 0.533 0.533-0.533 |
※ 数値は SSDSE-B-2026(2023 年 47 県)の実測。乱数シードと sklearn のバージョンにより端数は変わり得ますが、「ランダムは揺れ、層化は一定」という傾向は再現します。パネルの合成 1000 件など本ページ内の合成例は「架空」と明記しています。
分割は評価設計の入口です。前後の概念を辿ると理解が固まります。