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

🔖 キーワード索引

テストデータTest DataHoldout汎化評価リーケージTrain/Val/TestStratify再現性未知データ

本ページは テストデータ(Test Data)を 12 のセクションで多角的に解説します。 上のチップは検索・関連語の手がかりです。 以下のリンクで各セクションに直接ジャンプできます:

💡 30秒結論 | 📍 文脈 | 🎨 直感 | 📐 数式 | 🔬 数式を言葉で読み解く | 🧮 実値計算 | 🐍 Python 実装 | ⚠️ 落とし穴 | 🌐 関連手法 | 🔗 関連用語 | 📚 グループ教材

💡 30秒で分かる結論

🍰 まずはやさしく

最後に実力をはかるためのデータです。

正しく性能を評価するために使います。

テスト前に解く模擬試験のようなものです。

まずは結論を短く確認しましょう。

最終評価に使うデータ

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

🍰 まずはやさしく

学習に使わない未知のデータのことです。

本番で通用するかを確認するために使います。

部活の練習試合で実力を試す感覚です。

どのような場面で使うかを見ていきましょう。

テストデータは 学習にも検証にも使われず、 最終的な汎化性能評価のみに使う未知データ。 通常 Train / Validation / Test の 3 分割で確保し、 Test は最後の 1 回だけ触るのが鉄則。 Test を何度も見るとリーケージで Test スコアが過剰になる。

🔎 補足: テストデータの設計・隔離・運用

テストデータ (test set) は機械学習の評価品質を左右する最重要部品の一つだが、 「訓練データの余りを test に回す」「期間で適当に切る」など軽視されがちな扱いが多い。 ここでは SSDSE-B-2026 を題材に、 テストデータの設計・隔離・運用を 12 観点で整理する。

1. テストデータの 3 大要件

2. train/val/test の役割分担

training は重み最適化、 validation はハイパーパラメータと early stopping、 test は最終評価。 test を validation のように繰り返し見ると、 暗黙のうちにモデル選択が test に依存して評価が水増しされる (test set chasing) ので、 「最終リリース前 1 回だけ評価」が原則。

3. SSDSE-B-2026 での具体的分割例

SSDSE-B-2026 は 47 都道府県 × 経済・人口・産業の 100 変数程度。 サンプル数が極端に少ないので「7:2:1 分割」は 47 × 0.1 = 4.7 件しか test に残らず、 性能評価が不安定。 この場合は「分割するのではなく、 47 件で学習しつつ leave-one-out CV (LOO-CV) で評価する」「外部の別年データ (SSDSE-B-2025 等) を test として確保する」「stratified 5-fold で 9-10 件 / fold の test を回す」のいずれかを採る。

4. 時系列データの場合

SSDSE-B-2026 のような cross-sectional データなら無作為分割で OK だが、 時系列なら 必ず時間順分割 にする。 訓練データの最新時点より後を test にしないと、 「未来情報が訓練に混入してリーケージ」が起きる。 株価予測・需要予測ではこの罠が頻発する。

5. グループ単位の分割

同一ユーザー・同一店舗・同一患者のサンプルが複数あるデータでは、 グループ単位で互いに素にする (GroupKFold)。 同じユーザーのサンプルが train と test に混ざると、 モデルは「そのユーザーの特徴」を覚えるだけで本来の汎化を学べない。

6. test set の「凍結」と運用

test データは PR レビューが終わったら「凍結ディレクトリ」に移動し、 通常の開発フローからアクセスできないようにする。 git submodule や暗号化、 別リポジトリでの管理が現場の解。 「うっかり EDA で test を見てしまった」という事故は珍しくない。

7. データドリフトと test の鮮度

運用が長期になると本番分布と test 分布が乖離する (data drift)。 6 ヶ月〜1 年ごとに新しい test を確保し、 古い test と並列で評価することで「ドリフトのスピード」を計測する。 ドリフトが大きければ再学習や model refresh のシグナルになる。

8. 公開ベンチマーク test set との関係

公開済の test set (ImageNet, MNIST, GLUE, SSDSE 等) は何百万回も評価された結果、 「コミュニティ全体がその test に過剰適合」する現象 (benchmark leakage) が知られている。 公開 test の数値だけで意思決定せず、 自前 hold-out を必ず併用する。

9. test data の質を測る指標

10. label leakage の典型 6 パターン

  1. train と test で同じレコードが重複
  2. 特徴量に target を導く未来情報が混入 (例: 「請求が支払い済か」を特徴量に入れて延滞予測)
  3. preprocessing (StandardScaler.fit) を train + test に対してまとめてかける
  4. SMOTE を test にも適用
  5. SQL JOIN で test レコードのキーから target を取得
  6. cross-validation を「ランダム」にしてしまい、 同じ患者のサンプルが train/test に混入

11. test の選び方: hold-out vs CV vs nested CV

大規模データ (n > 10,000) なら hold-out 1 つで十分。 中規模 (1,000 < n < 10,000) では 5-fold CV を併用。 小規模 (n < 1,000) では LOO-CV または stratified 5-fold で、 ハイパーパラメータ調整時は nested CV で「内側 CV でチューニング、 外側 CV で最終評価」を分ける。

13. ケーススタディ: SSDSE-B-2026 で人口減少予測モデル

「人口減少率が 5% 以上か否か」を 2 値分類するモデルを SSDSE-B-2026 で構築するとき、 47 都道府県を 7:3 で訓練 33 件、 test 14 件に分割すると少数派が 1-2 件しか test に残らないので評価がノイズに支配される。 そこで stratified 5-fold + LOO-CV のハイブリッド評価を採用し、 さらに SSDSE-B-2025 (1 年前データ) を外部 test として加える。 外部 test との性能差が小さければモデルは時間方向に安定、 大きければデータドリフトが疑われる。 こうした多段評価で初めて、 small data の test 設計が実務に耐える。

15. 追加 FAQ: 現場で頻出する 12 の疑問

Q1. test の割合は何 % が正解か? 一般的には 20-30 % だが、 サンプル数で判断する。 n=100 なら 30 件、 n=10,000 なら 1,000 件で十分。 評価指標の標準誤差が業務上の意思決定閾値を下回る規模を確保するのが本質。

Q2. test を seed 固定で複数生成して平均を取ってよいか? 取ってよいが、 これは hold-out ではなく Monte Carlo cross-validation と呼ばれる手法になる。 通常の k-fold CV と異なり fold が重なるため、 標準誤差の見積もりは慎重に。

Q3. test サンプルに NaN がある場合は除外してよいか? 一律除外すると分布が歪む。 NaN を残し、 モデル側で imputation する設計が正攻法。 ただし train で imputer を fit し、 test には transform のみ適用すること。

Q4. test で性能が高すぎる場合は? data leakage を疑う。 特徴量に target に近い情報が混入していないか、 train と test に同じレコードが含まれていないか確認する。 性能が「異常に良い」ときは喜ぶ前に検証する。

Q5. test 用に正解ラベルを別ファイルで管理すべきか? 推奨。 特徴量 X_test.csv と正解 y_test.csv を分離し、 y_test.csv は暗号化または read-only にする。 開発中に y_test を見る誘惑が物理的にできない構成にする。

Q6. test の評価指標は 1 つに絞るべきか? 主要指標 1 つに加え、 補助指標を 3-5 個並べる。 例: 精度を主、 再現率・F1・AUC・PR-AUC を副。 主要指標だけだと「全部正解と予測」のような縮退モデルに気づけない。

Q7. train と test の特徴量分布が異なる場合は? covariate shift と呼ぶ。 importance weighting や domain adaptation で補正できるが、 まずは「なぜ違うのか」をデータ収集プロセスから調査するのが先。

Q8. test 結果が悪い場合の修正フロー? test 結果を見てモデルを修正するのは禁じ手 (test に過剰適合する)。 train+val でデバッグし、 test は最終 1 回のみ評価。 何度も評価したい場合は新しい test を確保する。

Q9. nested CV はいつ必要か? ハイパーパラメータ調整を伴うときに、 外側 CV で得る性能が「真の汎化性能」に近づく。 計算コストが k^2 倍になるので、 大規模データでは hold-out 評価で代替することが多い。

Q10. test を後から増やしてよいか? 増やすのは可だが、 「過去の test 結果」との互換性は失われる。 「v1 test (n=100)」「v2 test (n=200)」のようにバージョン管理し、 報告書に明記する。

Q11. 公開コンペの test はどう扱うか? public test と private test に分かれている場合、 public のスコアで過剰適合し private で順位が下がる現象が頻発する。 自前 hold-out で性能を確認してから submission する。

Q12. 質的データの test 設計は? アンケートやインタビューでも train/test の概念は有効。 異なる回答者群、 異なる地域、 異なる時期のサンプルを test に充て、 「モデル (= 解釈や仮説) が新しい人にも適用できるか」を確認する。

16. 用語小辞典: test 周辺の必修ワード 16 件

17. 実例: SSDSE-B-2026 で 5-fold CV を回すコード雛形

SSDSE-B-2026 の 47 件で stratified 5-fold を回す典型的なコード設計を示す。 fold ごとに 9-10 件の test が残り、 5 回の平均性能を最終指標として報告する。

from sklearn.model_selection import StratifiedKFold import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', skiprows=[1], encoding='cp932') y = (df['人口減少率'] >= 0.05).astype(int) # 仮の target X = df.drop(columns=['人口減少率', 'コード', '都道府県']) skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) for fold, (train_idx, test_idx) in enumerate(skf.split(X, y)): print(f'fold {fold}: train={len(train_idx)} test={len(test_idx)}') # ここで model.fit(X.iloc[train_idx], y.iloc[train_idx]) # ここで score = model.score(X.iloc[test_idx], y.iloc[test_idx])

このコードは 5-fold で 47 件を約 37:10 に分割し、 5 回繰り返して平均性能を評価する。 SSDSE-B-2026 のような small data の典型運用パターンとして覚えておきたい。

🔎 拡張補足: テストデータ運用のさらなる深堀り

1. 試験データセット管理のベストプラクティス

テストデータは「設計時に作る」のではなく「運用しながら育てる」ものだ。 SSDSE-B-2026 のような公開統計データを使う場合でも、 新しい都道府県データが毎年更新されるため、 古いテストセットの代表性が時間とともに低下する。 対策として、 (1) テストデータのバージョン管理 (Git LFS, DVC)、 (2) サンプル数・属性分布の年次レビュー、 (3) 重要属性ごとの代表サンプル保持、 (4) outlier・boundary case の保管庫構築、 (5) ドメインエキスパートによる質的検証、 の 5 軸で運用する。 特に (4) は重要で、 「珍しいが起こりうるケース」を意図的に保持することで、 モデルの未知ケース耐性 (robustness) を継続評価できる。 例えば SSDSE-B-2026 では「沖縄県の高齢化率の特異性」「東京都の人口密度の極端さ」「秋田県の人口減少速度」など、 各都道府県の特徴的属性をテストセットに必ず含める設計が望ましい。

2. テストデータと公平性 (fairness) 検証

分類モデルの公平性検証では、 テストデータが「保護属性」 (人種、 性別、 年齢、 地域 etc.) に対して均衡なサンプルを含むことが極めて重要だ。 SSDSE-B-2026 で「人口減少リスク予測」モデルを評価する際は、 「都市部・地方」「人口大・小」「産業構成 (1 次・2 次・3 次)」のすべての象限から少なくとも 5 サンプル以上を含めるよう設計する。 sklearn の stratified_train_test_split はクラスバランスを保つが、 多軸保護属性での均衡を保証しないため、 手動でサンプリングを設計する必要がある場合が多い。 さらに、 disparate impact (異なる集団間での誤り率格差) を計測するため、 各集団 (都市部 vs 地方 etc.) ごとに precision/recall/F1 を別々に集計し、 その差が業務的に許容できる範囲かを評価する。 例: 都市部の人口減少リスク予測 F1=0.85, 地方 F1=0.65 なら、 地方への過小予測リスクとして要監視。

3. ベンチマークデータセットの危険な過信

ImageNet, GLUE, SuperGLUE などの著名ベンチマークは何百万回も評価され、 結果として「コミュニティ全体がこれらに過剰適合 (overfitting to benchmark)」する現象が知られている。 ImageNet で SOTA を更新したモデルが、 実世界画像で性能が落ちる例は枚挙にいとまがない。 SSDSE のような公開統計データも、 「教育例として広く使われる」ことで類似の現象が起こり得る。 対策: 自前の hold-out (時系列で分離)、 ドメイン特化テスト (秋田だけの限定モデル) の併用。 また「ベンチマークでの順位」と「実運用での価値」は別物だと割り切ること。 NeurIPS や ICML の論文でも、 単一ベンチマーク SOTA だけで主張するものは査読で減点される傾向が強まっている。 複数ベンチマーク + 自前のドメインテスト + ablation study の三点セットが新標準だ。

4. テスト時拡張 (Test-Time Augmentation, TTA)

画像認識では、 推論時に入力画像を複数の augmentation (flip, crop, color jitter) で変換し、 アンサンブル予測を取る TTA が標準化している。 統計データでは類似の手法として、 「予測時の特徴量摂動 (noise injection)」「ブートストラップによる予測区間推定」「特徴量変換 (log, sqrt) の併用」がある。 SSDSE で人口減少率予測する際、 入力データを 5% の標準偏差で摂動して 100 通り予測し、 平均と分散を取れば堅牢な予測値が得られる。 TTA の副次効果として、 予測の不確実性を定量化できる点も重要だ。 同じ入力に対し、 摂動を加えた予測の標準偏差が大きい点 = モデルが自信を持てない領域、 と解釈できる。 これを「decision deferral」 (人間判断への委譲) のトリガーに使うことで、 自動化と人間判断のハイブリッド運用が実現する。

5. test-train データの重複検出 (Data Contamination)

LLM 評価で深刻化している問題: GPT-4 が GLUE テストデータを学習中に見ていた可能性。 対策として、 (1) MinHash, SimHash による重複検出、 (2) substring overlap 解析、 (3) 厳格な時系列カットオフ (テストデータは訓練終了後にリリースされたものに限定)、 (4) "test on truly held-out" を明示する。 SSDSE のような公開統計データも、 LLM の事前学習に含まれている可能性があるため、 LLM ベース予測には注意が必要。 contamination の検出には Anthropic, OpenAI も真剣に取り組んでおり、 「training data 中に test set の文字列が含まれるかを n-gram で検索する」「test set のランダム並べ替えで perplexity が変わらないかを確認する」などの手法が研究されている。 教育用 SSDSE 利用では、 「LLM に SSDSE-B-2026 の都道府県値を答えさせる」検証で contamination 度を簡易確認できる。

6. テストデータの再現可能性

「同じテストデータで同じ評価が出る」は研究の基本前提だが、 (1) 乱数 seed の固定、 (2) 環境依存性の排除 (Docker, conda env)、 (3) ハードウェア差 (CPU vs GPU, float 精度) の管理、 (4) 並列処理での順序依存 を考慮する必要がある。 SSDSE-B-2026 を使う研究では、 requirements.txt + random_state=42 + pd.read_csv('SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) のような明示的記述が必須。 さらに踏み込むと、 PyTorch の torch.use_deterministic_algorithms(True)、 NumPy の np.random.default_rng(seed) 利用、 並列処理での n_jobs=1 固定、 結果ファイルの SHA256 ハッシュ記録、 などを CI に組み込むことで完全な再現性を担保できる。 研究の信頼性は再現性と直結するため、 ここを軽視するとレビューで大きく減点される。

7. ドリフトとテストデータの陳腐化

SSDSE-B-2026 で予測モデルを構築し、 SSDSE-B-2027 が公開されたとき、 (1) covariate shift (説明変数分布の変化)、 (2) prior shift (目的変数分布の変化)、 (3) concept drift (目的-説明関係の変化) の 3 形態でドリフトを定量化する。 detection 手法: Kolmogorov-Smirnov 検定、 Wasserstein 距離、 Maximum Mean Discrepancy (MMD)。 ドリフトが閾値を超えたら再学習・モデル更新の引き金とする。 実運用では、 KS 検定の p < 0.01 を「即時アラート」、 MMD > 0.1 を「監視継続」、 Wasserstein 距離が train 標準偏差の 2 倍を超えたら「強制再学習」など、 業務影響の重みづけで多段階アラートを設定する。 ドリフト監視は「事前検知 = 障害予防」であり、 MLOps の中核機能の 1 つとして位置づけられている。

8. テストデータ準備のチェックリスト 7 件

9. SSDSE-B-2026 を使った擬似テスト設計例

「47 都道府県の高齢化率を産業構成・人口・教育費から予測」というタスクで、 (1) train 70%, val 15%, test 15% に都道府県単位で stratified 分割、 (2) test seed=42 固定、 (3) 地域ブロック (北海道・東北・関東 etc.) で fold を切ったクロスバリデーション、 (4) 評価指標は MAE + R² + 95% 信頼区間、 という設計が標準的。 サンプル数 47 と少ないため、 LOO-CV (leave-one-out cross-validation) も併用すると安定する。 さらに、 各地域ブロック別の予測誤差を箱ひげ図で可視化し、 「九州地方だけ系統的に過小予測」のような地理的バイアスを検出する手順も入れる。 SSDSE-B-2026 のような小規模データでは、 単一の test set ではなく、 ブートストラップ 1000 回での予測分布全体を評価対象とする方法も推奨される。

10. テストデータと業務要件の対応

機械学習の test 評価は「アルゴリズムの良さ」を測るが、 業務的に重要なのは「ビジネス指標の改善」だ。 テストデータ上で AUC=0.95 でも、 「false positive 1 件あたりのコスト」を考慮すると AUC=0.85 のモデルの方が業務的に優位なケースが多々ある。 SSDSE-B-2026 で「人口減少リスク予測」を扱うなら、 false positive (リスクと誤判定) のコスト = 不要な対策費用、 false negative (リスクを見逃し) のコスト = 自治体の財政破綻リスク、 を金額換算してから評価指標を設計する。 「AUC が高い = 良いモデル」ではなく、 「業務コストが最小 = 良いモデル」という視点に切り替えるのが、 データサイエンティストとしての成熟度を分ける分水嶺だ。

🎨 直感で掴む

🍰 まずはやさしく

本番初日の客さんのようなデータです。

予測の精度が十分か確かめるために使います。

スマホアプリを公開する前の最終確認です。

直感的なイメージで使い方を学びましょう。

機械学習は「データから規則を学ぶ」アプローチ。 ルールベース(明示的に書く)に対し、 データから自動でパターンを獲得する点が特徴です。

本ページでは テストデータ を、 定義・前提条件・使い方・落とし穴の順に整理して解説します。 厳密な定義より、 まず何を、 いつ、 どう使うかを理解することを優先してください。

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

テストデータは「本番初日のお客さん」を想定した未知データ。 Train で学び、 Val でハイパラを選び、 そして本番直前に 1 度だけ Test で性能を確認する。 ここで満足のいくスコアが出れば本番投入、 ダメなら再設計。 「Test を改善する」という発想自体が罠で、 改善するなら必ず別データを切り出して試す。

🎨 業界別 テストデータ設計の実例

業界タスク分割戦略注意点
金融株価予測TimeSeriesSplit過去で訓練、 未来で評価
医療画像診断GroupKFold (患者ID)同患者画像が train と test に混入禁止
EC推薦TimeSeriesSplit + UserKFold未来イベントリーク注意
製造異常検知StratifiedKFold陽性が極少 (0.1% 等)
広告CTR 予測A/B テスト (ライブ test)オフライン評価 + オンライン評価
NLP機械翻訳ホールドアウトベンチマーク汚染注意 (LLM)

🎮 触って理解する

テストデータの本質は 「学習に一切使わず封印し、 最後に 1 度だけ開封する」 ことです。 下の 2 つの図を スライダーやタッチで操作して、 (1) 訓練データとテストデータの分割・封印、 (2) テストを何度も見ると評価が楽観的に歪む(リーク)様子を体感してください。 データ規模は SSDSE-B-2026(2023 年・47 都道府県)の 47 サンプルを想定しています。

① 訓練 / テストの分割と「封印」

47 都道府県を 訓練用(学習に使う)と テスト用(🔒 封印して最後まで開けない)に分けます。 テスト割合を動かすと、 封印されるマス(🔒)が増減します。 封印マスは学習に一切触れません。


② 「テストを見る回数」とリーク(楽観バイアス)

テストデータで 何度もモデルを調整(見る)と、 偶然テストに都合の良いモデルを選んでしまい、 報告されるテスト誤差だけが下がっていきます。 一方 真の汎化誤差は変わりません。 見る回数を増やすと、 報告値と真値の 乖離=リーク が広がるのを確認してください。 図を タッチ / ドラッグしても回数を変えられます。


報告テスト誤差
–
真の汎化誤差
–
乖離(リーク量)
–

計算:候補モデルの真の誤差を $p_0=0.20$(=正解率 80%)に固定。 有限テスト($n_{\text{test}}$ 件)での誤差推定は標準偏差 $\sigma=\sqrt{p_0(1-p_0)/n_{\text{test}}}$ のばらつきを持つ。 $k$ 回テストを見て最良(最小誤差)を選ぶと、 報告値の期待値は $E[\hat p]=p_0-\sigma\,a_k$($a_k=E[k$ 個の標準正規の最大値$]$、 数値積分で厳密計算)だけ系統的に下振れ(楽観化)する。 真値 $p_0$ は不変なので差 $\sigma\,a_k$ が「リーク量」。 $k=1$ では $a_1=0$ ⇒ 乖離ゼロ=不偏推定。 テストが小さい($n_{\text{test}}$ 小)ほど $\sigma$ が大きく、 少ない開封で急速に楽観化する。

📖 もっと深く

直感: テストデータは「本番前に 1 度だけ開封する封筒」。 中身を覗くたびに封筒の価値は減り、 最終評価がおもてより甘くなります。 だからこそ最後まで封印し、 提出直前に 1 回だけ開けるのが鉄則です。

よくある落とし穴: テストで繰り返し評価してモデルや前処理を選ぶと、 テストへの過適合(過学習)と データリーク が生じ、 テスト誤差が楽観的に見えます。 「テストスコアを改善する」発想自体が罠で、 調整は必ず別データで行います。

発展: 調整用には 検証データ(Validation) を分離し、 ホールドアウト法 や 交差検証 を使ってテストを温存します。 これにより 汎化性能 の推定を、 訓練データ とテストデータのどちらも汚さずに得られます。 体系的な位置づけは 機械学習の基礎 を参照。

📐 定義

🍰 まずはやさしく

最終評価に使うデータの定義です。

分析の正しさを証明するために使います。

買い物で商品のレビューを信じる感覚です。

詳しい定義や数式について解説します。

最終評価に使うデータ

英語名 Test Data。

📐 数式・定義

テストデータを数式 / 形式定義で表す:

$$\text{Test Score} = \mathbb{E}_{(\mathbf{x},y) \sim P_{\text{prod}}}\big[\text{Metric}(y, f(\mathbf{x}))\big]$$

テストスコアは本番分布 $P_{\text{prod}}$ からの抽出に対する期待性能の推定値。 真の汎化誤差の不偏推定とみなせる。

🔬 数式を言葉で読み解く

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

記号意味
$P_{\text{prod}}$本番データの分布(既知でない真の分布)
MetricRMSE・F1・AUC など
$f$学習済みモデル
$(\mathbf{x}, y)$テストペア

🔬 数式を言葉で読み解く(拡張版・「テストデータ」の本質)

テストデータは 「モデルが訓練中に一度も見ていない」という条件付きの保留データ で、 汎化性能の不偏推定 を得るための唯一の手段です。 「テストデータをハイパラ調整に使う=バリデーションへ降格」「複数回テストして最良を選ぶ=p ハッキング」など、 一度ルールを破ると テストデータの価値が永久に失われる 厳格さがあります。

📐 分割戦略の数理

① 標準分割: $|D_{tr}|:|D_{val}|:|D_{te}| = 0.6 : 0.2 : 0.2$

② ホールドアウト法: $|D_{tr}|:|D_{te}| = 0.8 : 0.2$(簡易)

③ K-Fold CV: $D_{tr}^{(k)} = D \setminus D^{(k)}, \quad k=1,...,K$

④ Stratified K-Fold: 各 fold で目的変数の分布を保持

⑤ Nested CV: 外側で評価、 内側でハイパラ最適化

⑥ 期待リスク $R(f) = E_{(x,y) \sim P}[L(f(x), y)]$ をテスト誤差 $\hat{R}_{te}(f) = \frac{1}{n_{te}}\sum L(f(x_i), y_i)$ で不偏推定

📊 分割戦略 比較表

戦略適用場面利点欠点
ホールドアウトn > 10,000高速分散大
K-Fold CV中規模バランス良K 倍の計算量
Stratified K-Foldクラス不均衡階層分布保持回帰不可
LOO CVn < 50最大データ活用超高計算量
Time-Series Split時系列時間的順序保持前期 train 少ない
GroupKFold同一グループ重複リーク防止グループ識別必要
Nested CVハイパラ調整公正な評価超高計算量

🔬 数式を言葉で読み解く(テストデータ)

1. データ分割の根本思想:なぜ「分けて評価する」のか

機械学習モデルは、与えられた訓練データから「パターン」を学習する。しかし学習したパターンが本当に有効かどうかは、訓練時には絶対に見せていない別のデータで試して初めて分かる。これが「テストデータ」の存在意義である。模試で 100 点を取った受験生が、本試験でも 100 点を取れるとは限らない。模試の問題を丸暗記してしまえば模試の点数だけは伸びるが、本試験の問題は別物なので得点が下がる。これと同じ現象が機械学習でも起きる。これを「過学習(overfitting)」と呼び、過学習を検出するための唯一の手段がテストデータでの評価である。SSDSE-B-2026 の 47 都道府県データを使って、人口を予測するモデルを作る場面を考えてみよう。47 県全部で学習し、47 県全部で評価すれば、当然ほぼ完璧な精度が出る。だがそのモデルが「未知の県」(例えば架空の 48 番目の県)で機能する保証はどこにもない。だからこそ、最初から 47 県のうち例えば 9 県(約 20%)を「テスト用」として隔離しておき、残り 38 県だけで学習し、最後に 9 県で評価する必要がある。この 9 県でも高精度が出れば、モデルは「未知の県でも機能する」と推定できる。

2. Train/Val/Test の三分割が標準である理由

実務では単純な Train/Test の二分割ではなく、Train(訓練)/ Validation(検証)/ Test(テスト)の三分割が標準である。理由はハイパーパラメータ調整にある。決定木の深さ、ランダムフォレストの木の本数、ニューラルネットの学習率など、モデル構造を決める「設定値」は無数にある。これらを Test データで最適化してしまうと、Test データに合わせ込んだ「Test 過学習」が起きる。だから Validation データを別に用意し、ハイパーパラメータは Validation で最適化し、Test は本当に最後の最後(モデル提出直前)に 1 回だけ評価する。SSDSE-B-2026 の 47 県を例に取れば、Train 28 県(60%)、Val 9 県(20%)、Test 10 県(20%)のような分割が典型的である。Train で学習、Val で「木の深さは 5 がいいか 7 がいいか」を試行錯誤、Test で最終確認、という流れになる。Val でも Test でも 80% 精度が出れば、その値は信頼できる「未来予測の期待値」である。

3. Hold-out 法の長所と短所

Hold-out 法は最もシンプルな分割方法で、データを一度だけ Train/Test に分け、それで評価する。47 県を 38 県(Train)+ 9 県(Test)に分ければそれで終わりである。実装が簡単で計算量も少ないため、データが大量にある場合(例:10 万行以上)に有効である。ただし短所もある。それは「偶然の偏り」に弱いことだ。9 県をテスト用に抜く時、たまたま大都市圏ばかりが選ばれれば、地方の精度は分からない。逆に過疎県ばかり選ばれれば、人口予測モデルの「大都市での精度」が見えない。データが少ない(例:47 県のみ)場合、たった一度の分割では運の要素が大きすぎる。そこで次に紹介する Cross Validation(交差検証)が必要になる。

4. K-Fold Cross Validation の仕組み

K-Fold Cross Validation(K 分割交差検証)は、データを K 等分し、K 回の学習・評価を繰り返して平均を取る手法である。47 県を K=5 で分割するなら、各 fold は約 9-10 県となる。1 回目:fold1 を Test、fold2-5 を Train。2 回目:fold2 を Test、それ以外を Train。…と 5 回繰り返す。各回の精度を平均すれば、たった一度の分割による運の要素が消え、より信頼性の高い評価が得られる。さらに、47 県全部が一度は Test に使われるので「全データを評価に活用する」点でもデータ少数時に有利である。コストは「5 回学習する」ことだが、SSDSE 規模なら数秒で終わる。実務で K=5 または K=10 が最も使われる。K=5 は分散と計算量のバランスが良く、K=10 はより精密だが計算が 2 倍かかる。

5. Leave-One-Out 法(LOOCV)の極端なケース

K-Fold の極端形が Leave-One-Out(一個抜き法)である。47 県のうち 1 県を Test に、残り 46 県を Train にする。これを 47 回繰り返す。データの「ほぼ全て」を訓練に使えるので、データ少数時の最終手段として有効である。ただし計算コストは K=47 倍に跳ね上がる。また各 Test サンプルが 1 件なので、評価値の分散が大きく、必ずしも安定するとは限らない。SSDSE-B-2026 のような 47 件のデータでも、シンプルなモデル(線形回帰など)なら LOOCV は実行可能であり、教育用途では「すべての県が等しく評価される」点で公平性が高い。一方、ランダムフォレストや勾配ブースティング等の重いモデルでは LOOCV は非推奨である。

6. Stratified Sampling(層化抽出)の必要性

分類タスクで、特にクラス不均衡があるとき、ランダム分割だけでは偏りが生じる。例えば「都道府県を高齢化率の高い県(30%以上)と低い県(30%未満)に二分類する」タスクで、47 県中 35 県が高齢化率 30% 以上だとする。ランダムに 9 県を Test に選ぶと、運悪く 9 県全部が高齢化県になる確率もゼロではない。これでは低齢化県の精度が測れない。Stratified Sampling は、クラスの比率を保ったまま分割する手法である。35:12 の比率なら、Train でも Test でもこの比率を維持する。`sklearn.model_selection.train_test_split` の `stratify=y` 引数で簡単に実装できる。SSDSE-B-2026 では、地方(北海道・東北・関東・…)でも層化できる。これにより「全地方が Train にも Test にも含まれる」状態を確保し、評価の偏りを最小化できる。

7. 時系列データでの分割は時間順厳守

時系列データでは絶対にランダム分割してはいけない。SSDSE-B-2026 の人口は 2026 年時点だが、もし「2020-2025 年の人口データから 2026 年の人口を予測する」モデルを作るなら、Train を 2020-2024 年、Test を 2025-2026 年と「時間順に分割」する必要がある。ランダム分割すると、Train に 2025 年が含まれて Test に 2023 年が来る、というような時間逆転が起きる。これは「未来を見て過去を予測する」というカンニング状態であり、テスト精度が不当に高く出る。実運用では「過去のデータから未来を予測する」のだから、Train が古く Test が新しい時間順を厳守する。さらに高度な手法として TimeSeriesSplit(時系列交差検証)がある。これは「Train 期間を徐々に拡張しながら何度も評価する」方法で、Walk-Forward Validation とも呼ばれる。実務の時系列予測(株価、需要予測、災害予測)では必須技術である。

8. データリーケージ(leakage)の検出と防止

データリーケージとは、Test データの情報が Train に「漏れている」状態である。これが起きるとテスト精度が異常に高く出るが、実運用では機能しない。典型的なリーケージは「前処理を Train+Test 合わせて行うこと」だ。例えば標準化(z-score)を計算する時、Train+Test 全体の平均と標準偏差を使ってはいけない。Test の情報が Train の前処理に混入する。正しくは「Train だけで平均・標準偏差を計算し、その値を Test にも適用する」。sklearn の `Pipeline` を使うとリーケージを自動防止できるので推奨される。SSDSE-B-2026 で「消費支出を総人口で割って 1 人当たり消費支出を計算する」場合も、Train データの 1 人当たり消費支出の分布を使って前処理する必要がある。Test の平均値を含めてはならない。リーケージは検出が難しく、「Val 精度 95%、本番 60%」のような乖離で初めて気づくことが多い。だから事前の Pipeline 設計が極めて重要である。

9. 分布チェック:Train と Test が同じ分布か

Train と Test の分布が大きく違うと、テスト評価は信頼できない。例えば Train に小県(人口 100 万未満)ばかり、Test に大都市県(人口 500 万以上)ばかり含まれていれば、モデルは大都市県の挙動を学習していないため Test 精度は低くなる。だが「モデルが悪い」のではなく「分布がズレている」のである。これを Distribution Shift(分布シフト)と呼ぶ。SSDSE-B-2026 を分割した後、Train と Test の各変数の平均・分散・最小・最大を比較するのが基本である。ヒストグラムを重ねて目視確認するのも有効だ。もし大きなズレがあれば、Stratified Sampling を再適用するか、データの再分割が必要である。Kolmogorov-Smirnov 検定や Anderson-Darling 検定で統計的に分布の差を検証することもできる。実務では「定期的に新しいデータが入ってきた時、最初に分布が変わっていないか確認する」プロセスが運用上不可欠である。

10. ホールドアウトの黄金比:60-20-20 vs 80-20

分割比率に絶対的な正解はないが、データ量によって推奨が変わる。データが少ない(1000 件未満)なら、Train を確保するため 80-20 や CV を使う。データが中程度(数千件〜数万件)なら 60-20-20(Train/Val/Test)が標準である。データが大量(100 万件以上)なら、Train 98%、Val 1%、Test 1% でも十分なテスト精度が得られる。SSDSE-B-2026 は 47 件と非常に少ないので、Hold-out よりも K-Fold CV や LOOCV が現実的選択肢になる。逆に画像認識データセット ImageNet(120 万枚)では 80-10-10 でも十分以上である。実務では「Test に最低 1000 件は確保する」が経験則であり、それを満たすデータ量から逆算して分割比率を決める。

11. テストデータの「使用は 1 回だけ」原則

テストデータは「最終確認」専用であり、複数回使って結果に応じてモデルを調整してはいけない。複数回使うと、Test データに無意識に最適化してしまい、未知データへの汎化性能が下がる。これを「Test set contamination(テストセット汚染)」と呼ぶ。実務での運用:(1) Train で学習、(2) Val でハイパーパラメータ調整、(3) モデル決定後 Test で 1 回だけ評価、(4) この Test 精度を本番期待値として報告。もし Test 精度が低く「もう一度モデルを改良したい」場合、改良後のモデルを Test で再評価してはいけない。新しい Test データを別に確保する、または別の Val 分割で評価する必要がある。Kaggle 等の機械学習コンペティションが Public Test と Private Test に分けるのも、Test 汚染を防ぐ仕組みである。

12. 業務適用での重要な注意点

業務システムに機械学習モデルを実装する場合、テストデータの設計は「本番環境の予測対象」と一致させる必要がある。例えば「全国の市町村の人口を予測する」モデルなら、Test も全国市町村から抽出する。「東京都内の店舗売上を予測する」モデルなら、Test も東京都内店舗である。本番で予測する対象と Test の対象がズレていれば、Test 精度は意味を持たない。さらに、業務適用では「時間が経つにつれてデータの分布が変わる」のが当然である。コロナ前のデータで学習したモデルがコロナ後で機能しないのは典型例である。だから本番運用では定期的に「最新データでの再評価」が必要であり、これを Model Drift Detection と呼ぶ。一般的には月次・四半期ごとに Test 精度を再測定し、閾値を下回ったら再学習をトリガーする運用が標準である。SSDSE-B-2026 も 2012〜2023 年度の 12 年度分を収め、毎年新しい版が公開されるので、それに合わせたモデル更新計画が必要である。

13. テストデータが少ない場合の救済策

テストデータが少なすぎる(例:10 件以下)場合、評価値の信頼区間が広すぎて結論が出せない。救済策は (1) ブートストラップ法による信頼区間推定、(2) ベイズ的アプローチによる事前分布の活用、(3) 専門家による定性評価の併用、などがある。ブートストラップ法は Test データから復元抽出を 1000 回行い、各回で精度を計算して 95% 信頼区間を出す手法である。これにより「Test 精度 0.85、95% CI [0.78, 0.92]」のような表現が可能になり、評価の不確実性を明示できる。SSDSE-B-2026 の 47 件を全部 LOOCV で評価しても、各回の Test は 1 件なので、ブートストラップで信頼区間を出す価値がある。

散布図: 総人口と一般診療所数を Train 37 県・Test 10 県に色分け(2023 年度)
SSDSE-B-2026(2023 年度)の総人口 × 一般診療所数(47 県全体で r = 0.972)を train_test_split(test_size=0.2, random_state=0) で Train 37 県・Test 10 県に分けた散布図。相関は Train 0.985・Test 0.984 とほぼ同じだが、Train の直線は東京都(Train 側)に引っ張られ、Test に入った千葉県・埼玉県・愛知県の診療所数を多めに予測するため、Test の R² は 0.624 にとどまる。
ヒストグラム: Train と Test の総人口の分布(ランダム分割と層化分割)
同じ 2023 年度の総人口を Train と Test に分けて重ねたヒストグラム。総人口は少数の大都市圏が右裾を作る右歪の分布。左のランダム分割では Test 10 県のうち 4 県が最小の 5 分位に入り、2 番目の 5 分位は 0 県(Test 平均 297 万人、Train 256 万人)。右は総人口の 5 分位で層化した分割(stratify)で、Test は各分位から 2 県ずつになり、KS 検定の p も 0.55 から 0.91 に上がる。どちらの分割でも東京都は Train 側にある。
箱ひげ図: 分割を 200 通り変えたときの Test R²(Test 5・10・19 県)
総人口から一般診療所数を予測する単回帰を、random_state を 0〜199 に変えた 200 通りの分割で Train に学習させ、Test で R² を測った箱ひげ図。Test 5 県では中央値 0.903 でも 5〜95% の範囲が 0.378〜0.974 と広く、最小は −1.085。Test 10 県で 0.596〜0.966、19 県で 0.723〜0.957 と、Test が大きいほどぶれは小さくなる。1 回の分割の Test 精度だけで結論を出すと、この幅のどこに当たったかに左右される。

✅ 理解度チェック

  1. Q1: Hold-out 法と K-Fold CV のどちらが小規模データ(47 件)に向くか、理由とともに答えよ。
    A: K-Fold CV。Hold-out は 1 回の分割の偶然に左右されるが、K-Fold は K 回の評価平均で安定する。47 件全てを評価に活用できる利点も大きい。
  2. Q2: 時系列データをランダム分割するとなぜ「リーケージ」が起きるか説明せよ。
    A: ランダム分割では「未来」のデータが Train に含まれ「過去」のデータが Test になる時間逆転が起きる。これは「未来を見て過去を予測する」カンニング状態であり、Test 精度が不当に高く出るが本番では機能しない。
  3. Q3: Train+Test 全体で標準化(z-score 変換)するのがなぜリーケージか。正しい手順を述べよ。
    A: Test データの平均・標準偏差が Train 前処理に混入し「Test の情報を見て前処理パラメータを決めている」ことになる。正しくは Train だけで平均・標準偏差を計算し、その値を Test にも適用する(sklearn Pipeline で自動化推奨)。

🧪 Test データの「触れない原則」をデータサイエンス現場でどう守るか

テストデータの最大の倫理的義務は「モデル学習の意思決定に一切触れさせない」ことである。「触れる」とは Train で fit するだけではなく、ハイパーパラメータ探索で参照する、特徴量選択の良し悪し判定に使う、欠損補完の統計量を計算する、標準化の平均・分散を取る、外れ値検出の閾値を決める—これら全ての「データを見る行為」を指す。SSDSE-B-2026 の 47 都道府県を Train 38 / Test 9 に分割した時、Test 9 件の「総人口」「出生数」「消費支出」を print() しただけで、人間の頭脳に Test の分布が刻まれ、その後のモデル選択が無意識に Test に有利な方向へ偏る—これを「人間によるデータリーケージ」と呼び、再現性危機の原因の一つに挙げられる。

触れた瞬間にリーケージが起きる 8 つの典型操作

操作なぜ NG か代わりにすべきこと
Train+Test 全体で StandardScaler.fit()Test 平均・分散が Train 前処理に流入Pipeline で Train のみ fit、Test は transform
Train+Test 全体で欠損を平均補完Test の値が補完統計量に混入SimpleImputer を Pipeline 内で適用
Train+Test 全体で IQR 法外れ値除去Test 分布が四分位数決定に影響Train で閾値を決め、Test には適用のみ
Test を見て特徴量を取捨選択特徴量選択の意思決定に Test が関与Train 内の CV で特徴量を決める
Test 性能を見てモデル A/B を選ぶTest がモデル選択の判定基準にValidation セットでモデル選択、Test は最終評価のみ
Test 性能が悪いのでハイパラを再調整Test が間接的にチューニングを駆動Test は「一度だけ」見る覚悟を持つ
EDA で Train+Test 一括相関ヒートマップTest の構造を視覚的に記憶EDA は Train のみで行う
Train+Test を SMOTE で oversampleTest 少数派の合成データが Train にSMOTE は CV の Train fold 内のみ

「触れずに評価する」を実現する 3 つの実装パターン

このコードでやること:SSDSE-B-2026 の 47 都道府県を Train/Test に分け、Pipeline + cross_val_score で「触れない原則」を守った消費支出予測を行う。

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

地域コード 都道府県 総人口 出生数 婚姻件数 消費支出(二人以上の世帯、円/月) R01000 北海道 5,092,000 24,430 17,281 296,888 R13000 東京都 14,086,000 86,348 71,774 341,320 R27000 大阪府 8,763,000 55,292 38,513 271,246 R47000 沖縄県 1,468,000 12,549 6,316 251,222 ... (2023 年度の 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
import pandas as pd
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import Ridge

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=1)
df = df[df['年度'] == 2023]      # 2023 年度の 47 都道府県(12 年度分を混ぜない)
X = df[['総人口', '出生数', '婚姻件数']].values
y = df['消費支出(二人以上の世帯)'].values

# Step 1: 触れる前に分割する(最重要)
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, random_state=42)
print(f'Train {len(y_tr)} 県 / Test {len(y_te)} 県')

# Step 2: Pipeline で前処理を Train のみに閉じ込める
pipe = Pipeline([
    ('scaler', StandardScaler()),
    ('ridge', Ridge(alpha=1.0)),
])

# Step 3: Train 内の CV でハイパラ・モデル選択(Test は触らない)
cv_score = cross_val_score(pipe, X_tr, y_tr, cv=5, scoring='r2').mean()
print(f'Train CV R² = {cv_score:.3f}')

# Step 4: 一度だけ Test で最終評価
pipe.fit(X_tr, y_tr)
test_r2 = pipe.score(X_te, y_te)
print(f'Test R² = {test_r2:.3f}(この値を見た後の再調整は禁止)')

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

Train 37 県 / Test 10 県 Train CV R² = 0.037 Test R² = 0.033(この値を見た後の再調整は禁止)

💬 Train CV 0.037・Test 0.033 と、 どちらもほぼ 0 だった。 2023 年度の 47 県で消費支出と総人口・出生数・婚姻件数の相関は 0.31〜0.33 しかなく、 この 3 特徴量は人口規模を表すだけで世帯あたりの消費支出はほとんど説明できない。 Test は 10 県しかないので、 どの 10 県が選ばれるかで R² は簡単に上下する(次の Bootstrap で幅を確かめる)。 だからこそ「Test 0.033 を見てから Ridge の alpha や特徴量を変える」を絶対にしてはいけない。 それを許すと Test は実質的に第二の Validation になり、 未知データ性能の保証を失う。 そして偶然高く出た値を「改善した」と誤認することになる。

Test 触れた回数の管理:Goodhart の法則を回避する

Test を見た回数Test 性能の信頼性推奨対応
1 回(最終評価のみ)高い—汎化性能をほぼ正しく推定そのまま報告
2–5 回(軽微なチューニング後)中程度—Test に過適合の懸念楽観バイアス補正、本番運用前に追加評価
10 回以上(繰り返し再評価)低い—Test が事実上 Validation に新しい Test を取得、過去 Test は破棄
Kaggle 公開 Leaderboard 連続提出Public LB は Validation 化、Private LB のみ TestPrivate LB を「真の Test」と位置付ける

📊 Test データ評価指標と「サンプルサイズの呪い」

SSDSE-B-2026 の 47 県を 80:20 で分割すると Test は わずか 10 県(sklearn は 47×0.2=9.4 を切り上げる)。この少なさは「Test 指標の信頼区間が大きく、決定的な結論を出しにくい」ことを意味する。例えば回帰の R² が 0.92 と出ても、Test を別の 10 県に取り直せば 0.78 や 0.96 に振れることは普通。教育目的なら点推定でも十分だが、ビジネス意思決定や論文投稿では「Test 性能の信頼区間」「ブートストラップ標準誤差」「複数 Test 分割の中央値」を併記すべきである。

Test サンプルサイズ別の信頼度目安

Test n指標標準誤差の目安解釈
10 (SSDSE 80:20)R² 0.92±0.10 程度「概ね良好」止まり、優劣比較は厳しい
100Accuracy 0.85±0.035 (95%CI ±0.07)3% 差は誤差範囲、5% 差なら有意候補
1,000Accuracy 0.85±0.011 (95%CI ±0.022)1% 差を検出できる本格評価
10,000+Accuracy 0.85±0.00360.5% 差も区別可、本番 A/B 級

Bootstrap で Test 性能の信頼区間を出す

このコードでやること:47 県 SSDSE-B-2026 で Test R² の 95% Bootstrap 信頼区間を計算し、点推定だけでは見えない不確実性を可視化する。

📥 入力データ: 上記 Pipeline で fit 済 pipe と X_te, y_te(10 件)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import numpy as np
from sklearn.metrics import r2_score

rng = np.random.default_rng(42)
n_boot = 1000
boot_r2 = []
y_pred = pipe.predict(X_te)
for _ in range(n_boot):
    idx = rng.integers(0, len(X_te), len(X_te))
    boot_r2.append(r2_score(y_te[idx], y_pred[idx]))

ci_low, ci_high = np.percentile(boot_r2, [2.5, 97.5])
print(f'Test R² 点推定 = {r2_score(y_te, y_pred):.3f}')
print(f'95% Bootstrap CI = [{ci_low:.3f}, {ci_high:.3f}]')
print(f'CI 幅 = {ci_high - ci_low:.3f}(広いほど Test n 不足)')

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

Test R² 点推定 = 0.033 95% Bootstrap CI = [-1.885, 0.649] CI 幅 = 2.534(広いほど Test n 不足)

💬 点推定 0.033 に対して 95%CI は [-1.885, 0.649] で、幅は 2.5 もある。10 県から重複ありで選び直すと、東京都のような大きな県が入るか外れるかで R² が大きく揺れ、下側は「平均で予測するより悪い」負の値まで広がる。Test n=10 では「このモデルは使える/使えない」のどちらも断言できないので、報告時は必ず CI を併記し、「サンプル不足」を隠さない。

マルチタスク評価の落とし穴

1 つの Test セットで「Accuracy」「F1」「AUC」「Calibration」「Fairness 指標」を全て計算すると、複数指標の多重比較問題が発生する。例えば 5 指標を独立に評価すると、α=0.05 でも 1 - 0.95^5 ≈ 22.6% の確率で「どれか 1 つは偶然有意になる」。これを利用して「最も良かった指標だけを論文で報告」する Cherry Picking は学術不正に該当する。Bonferroni 補正、事前登録、複数 Test 分割の中央値報告で防ぐべきである。

場面推奨指標補助指標
バランス分類Accuracy混同行列、Macro F1
不均衡分類PR-AUC, F1Recall@K, 混同行列
確率予測Brier ScoreCalibration Plot, ECE
回帰RMSE, MAER², 残差プロット
時系列予測MAPE, sMAPEnaive baseline 比、Diebold-Mariano 検定
公平性監査Demographic Parity, Equal Oddsサブグループ別 ROC, Calibration

🚨 本番運用での Test データの「再構築」と Drift 監視

論文や Kaggle で完結する世界では「Test は固定」が常識だが、実運用では「時間が経つにつれて Test は古くなる」。SSDSE-B-2026 で 2024 年データで Test したモデルを 2026 年の消費支出予測に使うと、コロナ後のインバウンド回復、半導体投資、人口減少加速など「2024 年 Test には含まれなかった構造変化」が予測精度を蝕む。Production MLOps では「定期的に新しい Test セットを再構築 → モデルの汎化性能を再評価 → 必要なら再学習」のサイクル(Retraining Loop)が必須となる。

Production における Test 関連の 5 つの監視項目

監視項目検出する問題監視頻度アラート閾値の例
Test 性能の経時変化Concept Drift日次/週次R² が初期から -10% 以上
入力特徴量の分布Covariate Shift日次PSI > 0.25 (大きな drift)
予測分布Prediction Drift日次KL divergence > 0.1
目的変数の分布Label Drift週次/月次KS-test p < 0.01
サブグループ別性能Fairness Drift月次/四半期サブグループ間 Accuracy 差が初期の 1.5 倍

Drift 検出と「Test 再構築」のコード例

このコードでやること:SSDSE-B-2026 の 2012 年度(学習時点)と 2023 年度(現在)で総人口の Population Stability Index (PSI) を計算し、Test データの再構築が必要か判定する。

📥 入力データ: SSDSE-B-2026 の 2012 年度と 2023 年度の都道府県総人口 各 47 件(参考 reference / 監視対象 current)

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 numpy as np
import pandas as pd

def psi(reference, current, n_bins=10):
    """Population Stability Index を計算"""
    breakpoints = np.percentile(reference, np.linspace(0, 100, n_bins + 1))
    breakpoints[0] -= 1e-6; breakpoints[-1] += 1e-6
    ref_dist, _ = np.histogram(reference, bins=breakpoints)
    cur_dist, _ = np.histogram(current, bins=breakpoints)
    ref_pct = ref_dist / ref_dist.sum() + 1e-6
    cur_pct = cur_dist / cur_dist.sum() + 1e-6
    return np.sum((cur_pct - ref_pct) * np.log(cur_pct / ref_pct))

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=1)
df_ref = df[df['年度'] == 2012]   # 参照(学習時点): 2012 年度の 47 県
df_cur = df[df['年度'] == 2023]   # 監視対象(現在): 2023 年度の 47 県

psi_pop = psi(df_ref['総人口'].values, df_cur['総人口'].values)
print(f'PSI (総人口 2012→2023) = {psi_pop:.4f}')
if psi_pop < 0.10:
    print('→ 安定(Test 再構築不要)')
elif psi_pop < 0.25:
    print('→ 中程度 drift(要観察、追加 Test 推奨)')
else:
    print('→ 大きな drift(Test 再構築必須、再学習推奨)')

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

PSI (総人口 2012→2023) = 0.0929 → 安定(Test 再構築不要)

💬 11 年間で 47 県中 40 県の人口が減り、2012 年度の十分位で区切った 10 区間の県数が 5・5・4… から 6・6・3… へ下の区間に寄ったため、PSI は 0.0929 と「安定」の閾値 0.10 のすぐ手前まで来ている。同じファイルを 2 回読んで比べると PSI は 0 になるので、比較する 2 時点は年度で明示して切り出す。一方、「インバウンド観光客数」「半導体輸出額」などボラティリティの高い指標では PSI が 0.3 を超えることもあり、その場合は古い Test での精度が信用できなくなる—Test の再構築と再学習が必要。

Holdout Test の鮮度管理ベストプラクティス

理解度チェック(運用編)

  1. Q1: 2024 年 Test で R² 0.95 を達成したモデルが 2026 年本番で R² 0.72 まで低下。考えられる原因 3 つを挙げよ。
    A: ① Concept Drift(変数間の関係性自体が変化、例: コロナ後の経済構造変化)、② Covariate Shift(入力分布が変化、例: 人口減少進行)、③ Label Drift(目的変数の意味が変化、例: 消費支出の集計方法改定)。PSI 監視と定期再学習で対策する。
  2. Q2: Test 性能が「Public LB 0.85, Private LB 0.62」と乖離した。原因と対策は?
    A: Public LB に過適合し汎化を失った状態。Kaggle 提出回数を制限する(self-imposed)、CV スコアを最重視、複数モデルの ensemble で Public LB チューニング依存を下げる、最終提出は CV ベストと Public LB ベストの 2 種類用意。
  3. Q3: Pre-registered Test の意義を 80 文字以内で説明せよ。
    A: 評価指標と Test 分割を実験前に公的に固定することで、結果を見てから「都合の良い指標」を選ぶ Cherry Picking と HARKing を防ぎ、科学的再現性を担保する制度。

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

SSDSE-B-2026 の 47 都道府県を Train 30 / Val 7 / Test 10 に分割し、 「総人口 → 出生数」モデルの最終評価を実演する。

使用データ: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
17
18
19
20
21
22
import pandas as pd
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import r2_score, mean_squared_error
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

# Train+Val / Test = 37/10
X_tv, X_te, y_tv, y_te = train_test_split(X, y, test_size=10, random_state=42)
# Train / Val = 30/7
X_tr, X_va, y_tr, y_va = train_test_split(X_tv, y_tv, test_size=7, random_state=42)

m = LinearRegression().fit(X_tr, y_tr)
print(f'Train R² = {r2_score(y_tr, m.predict(X_tr)):.3f}')
print(f'Val   R² = {r2_score(y_va, m.predict(X_va)):.3f}')
print(f'Test  R² = {r2_score(y_te, m.predict(X_te)):.3f}  ← 最終評価')
📤 実行例(実測) Train R² = 0.995 Val R² = 0.942 Test R² = 0.981 ← 最終評価

💬 47 県を 30・7・10 に分けると、Train R² = 0.995、Val R² = 0.942、Test R² = 0.981 となり、検証データの値だけが低い。検証の 7 県(山形・鹿児島・広島・山梨・奈良・三重・福井)は出生数の標準偏差が約 4,100 人と小さく、分母が小さい分だけ同じ誤差でも R² が下がりやすい。逆にテストの 10 県には東京都・大阪府が入って標準偏差が約 25,600 人と大きいため、R² は高めに出る。R² は評価用の県の顔ぶれで大きく動くので、RMSE も並べて報告する。

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

🧮 SSDSE-B-2026 実値計算 ①:60/20/20 分割で訓練/検証/テスト

47 都道府県 × 12 年(合計 564 行)を 60/20/20 で分割し、 高齢化階層を予測するモデルの訓練 / 検証 / テスト精度を測定します。 テスト精度が訓練精度より大幅に低ければ過学習 です。

🐍 Python 実装(コード ①)

🎯 このコードでやること:sklearn の train_test_split を二段階で呼び、 60/20/20 の三分割を作る。 各データ集合のサイズと精度を確認。

📥 入力データ:

Total: 564 行 (47 県 × 12 年) × 10 特徴量 Train: 338 行 (60%) Val: 113 行 (20%) Test: 113 行 (20%)
 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
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['aging'] = df['A1303'] / df['A1101']
y = pd.qcut(df['aging'], 3, labels=[0, 1, 2]).astype(int)
features = ['A1101', 'A4101', 'A4103', 'B4101', 'B4102']
X = df[features].fillna(0)

# 2 段階で 60/20/20 分割
X_tv, X_te, y_tv, y_te = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)
X_tr, X_va, y_tr, y_va = train_test_split(X_tv, y_tv, test_size=0.25, random_state=42, stratify=y_tv)  # 0.25 of 0.8 = 0.2

print(f'Train: {len(X_tr)}, Val: {len(X_va)}, Test: {len(X_te)}')

model = RandomForestClassifier(n_estimators=100, random_state=42).fit(X_tr, y_tr)
print(f'Train acc: {accuracy_score(y_tr, model.predict(X_tr)):.3f}')
print(f'Val   acc: {accuracy_score(y_va, model.predict(X_va)):.3f}')
print(f'Test  acc: {accuracy_score(y_te, model.predict(X_te)):.3f}')

📤 実行例:

Train: 338, Val: 113, Test: 113 Train acc: 1.000 Val acc: 0.735 Test acc: 0.735

💬 結果の読み方:訓練精度 1.000、 検証 0.735、 テスト 0.735。 訓練のみ完全一致なのは Random Forest が深く木を伸ばして訓練データを暗記しているため。 検証とテストがほぼ同じ(差 0.000)→ 過学習はあるが汎化はできている。 検証で「真の本番」を予測する手段としては妥当。

🧮 SSDSE-B-2026 実値計算 ②:GroupKFold で都道府県リーク防止

SSDSE-B は 47 県 × 12 年のパネルデータ。 単純な train_test_split だと 「東京 2015 年が train、 東京 2020 年が test」のような同一県の重複 が起こり、 リークが発生します。 GroupKFold で都道府県をグループ単位として分割するのが正解。

🐍 Python 実装(コード ②)

🎯 このコードでやること:都道府県をグループとして GroupKFold を構築。 「ある県のデータがすべて train か test のどちらか」になることを確認。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from sklearn.model_selection import GroupKFold, cross_val_score
from sklearn.ensemble import RandomForestClassifier

groups = df['Prefecture']
gkf = GroupKFold(n_splits=5)

# 各 fold で train / test に出現する県を確認
for i, (tr, te) in enumerate(gkf.split(X, y, groups)):
    tr_pref = set(groups.iloc[tr])
    te_pref = set(groups.iloc[te])
    overlap = tr_pref & te_pref
    print(f'Fold {i}: tr={len(tr_pref)} 県, te={len(te_pref)} 県, overlap={len(overlap)}')

model = RandomForestClassifier(n_estimators=100, random_state=42)
scores = cross_val_score(model, X, y, groups=groups, cv=gkf, scoring='accuracy')
print(f'GroupKFold CV acc: {scores.mean():.3f} ± {scores.std():.3f}')

📤 実行例:

Fold 0: tr=37 県, te=10 県, overlap=0 Fold 1: tr=37 県, te=10 県, overlap=0 Fold 2: tr=38 県, te=9 県, overlap=0 Fold 3: tr=38 県, te=9 県, overlap=0 Fold 4: tr=38 県, te=9 県, overlap=0 GroupKFold CV acc: 0.581 ± 0.082

💬 結果の読み方:すべての fold で overlap=0(県の重複なし)。 通常のランダム分割だと 0.74 出ていたのに GroupKFold では 0.58 へ大きく下落。 これが 「データリークによる精度の水増し」 の正体です。 本番運用では未知の県を予測することになるので、 0.58 の方が現実的な見込み値。

🧮 SSDSE-B-2026 実値計算 ③:時系列 split(TimeSeriesSplit)

時系列データでは 「未来を train、 過去を test」は厳禁。 必ず時間軸を守って分割します。 sklearn の TimeSeriesSplit は累積的な訓練データで前向き検証 (forward validation) を行います。

🐍 Python 実装(コード ③)

🎯 このコードでやること:47 県の人口時系列を年でグループ化し、 過去年で学習 → 翌年予測の評価サイクルを 5 回繰り返す。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from sklearn.model_selection import TimeSeriesSplit
from sklearn.linear_model import LinearRegression
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import mean_absolute_error
import numpy as np

# 東京の年次人口 (A1101) で時系列 split
# SSDSE は新しい年度が先に並ぶので、古い年度から順に並べ替えてから分割する
tokyo = (df[df['Prefecture'] == '東京都'].sort_values('SSDSE-B-2026')
         [['A1101', 'A4101', 'A4103']].reset_index(drop=True))
X = tokyo[['A4101', 'A4103']].values
y = tokyo['A1101'].values

tss = TimeSeriesSplit(n_splits=5)
for i, (tr, te) in enumerate(tss.split(X)):
    # 出生数(〜10^5)と出生率(〜1)は桁が違うので、標準化してから回帰する
    m = make_pipeline(StandardScaler(), LinearRegression()).fit(X[tr], y[tr])
    pred = m.predict(X[te])
    mae = mean_absolute_error(y[te], pred)
    print(f'Fold {i}: train idx {tr.min()}-{tr.max()}, test idx {te.min()}-{te.max()}, MAE={mae:.0f} 人')

📤 実行例:

Fold 0: train idx 0-1, test idx 2-3, MAE=63635 人 Fold 1: train idx 0-3, test idx 4-5, MAE=264843 人 Fold 2: train idx 0-5, test idx 6-7, MAE=34134 人 Fold 3: train idx 0-7, test idx 8-9, MAE=88826 人 Fold 4: train idx 0-9, test idx 10-11, MAE=198908 人

💬 結果の読み方:train データを 2 年ずつ積み増しても MAE は 63,635 → 264,843 → 34,134 → 88,826 → 198,908 人と上下し、 減っていかない。 出生数と出生率で東京都の人口を当てるモデルは、 2012〜2021 年から、 とくに 2015 年以降の「出生数が減っても人口は増える」関係を学ぶので、 出生数が学習期間の最小(2021 年 95,404 人)を下回った 2022・2023 年(91,097・86,348 人)を当てる Fold 4 では外挿になって外れる。 各 fold の test は 2 年分しかなく値は大きくぶれるので、 5 つの MAE を並べて「どの時期に崩れるか」を見るのが時系列分割の使い方。

🧮 SSDSE-B-2026 実値計算 ④:分布シフト検出(train vs test の Kolmogorov-Smirnov)

train と test で特徴量分布がずれていないか確認することは、 本番運用での精度低下防止 に必須です。 各特徴量について scipy.stats.ks_2samp で K-S 検定を実施。

🐍 Python 実装(コード ④)

🎯 このコードでやること:47 県を train (前 9 年) / test (後 3 年) に分けた場合、 各特徴量の分布が同一かを K-S 検定 (p > 0.05 なら同分布) で確認。

📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行) 都道府県 A1101(総人口) A4101(出生数) A4103(合計特殊出生率) B4101(年平均気温) F3101(新規求職申込件数(一般)) 北海道 5,092,000 24,430 1.06 11.0 156,458 東京都 14,086,000 86,348 0.99 17.6 270,954 沖縄県 1,468,000 12,549 1.6 23.8 43,877 …(全 47 行)
1
2
3
4
5
6
7
8
9
10
11
12
13
import pandas as pd
from scipy.stats import ks_2samp

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
# SSDSE は新しい年度が先に並ぶので、cumcount ではなく年度の値そのもので分ける
train = df[df['SSDSE-B-2026'] <= 2020]   # 前 9 年(2012〜2020 年度)
test = df[df['SSDSE-B-2026'] >= 2021]    # 後 3 年(2021〜2023 年度)

features = ['A1101', 'A4101', 'A4103', 'B4101', 'F3101']
for f in features:
    stat, p = ks_2samp(train[f].dropna(), test[f].dropna())
    flag = '!!!' if p < 0.05 else ''
    print(f'{f}: KS={stat:.3f}, p={p:.4f} {flag}')

📤 実行例:

A1101: KS=0.083, p=0.4526 A4101: KS=0.196, p=0.0005 !!! A4103: KS=0.395, p=0.0000 !!! B4101: KS=0.194, p=0.0006 !!! F3101: KS=0.201, p=0.0003 !!!

💬 結果の読み方:A4101(出生数)・A4103(合計特殊出生率)・B4101(年平均気温)・F3101(新規求職申込件数)の 4 変数で分布シフトが検出 (p < 0.05)。 とくに合計特殊出生率は KS=0.395 と最大で、 県平均が 2012〜2020 年度の 1.42〜1.53 から 2021〜2023 年度は 1.40・1.36・1.29 と下がり続けた。 これは 少子化の進行など後期 3 年間で水準が変化 しているためで、 総人口 A1101 は県間の差が年ごとの変化よりずっと大きいので p=0.4526 とシフトが検出されない。 年度の並びを確かめずに cumcount で「前 9 年」を作ると、 新しい年度が先に並ぶ SSDSE では前後が逆になるので、 年度の値で分ける。 こうした特徴量を train で固定した分布で前処理すると test で誤差増。 分布シフト検出 → 再標準化 / 再学習 を検討。

🧮 SSDSE-B-2026 実値計算 ⑤:学習曲線で必要なテストサイズ推定

学習曲線 (learning curve) は 「データ量が増えるほど精度がどう変化するか」 を可視化。 これにより「テストサイズはこれで十分か」を見極められます。

🐍 Python 実装(コード ⑤)

🎯 このコードでやること:sklearn の learning_curve で訓練サイズ 10%〜100% における train / val 精度をプロット。

📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行) 都道府県 A1101(総人口) A1303(65歳以上人口) B4101(年平均気温) L3221(消費支出(二人以上の世帯)) 北海道 5,092,000 1,681,000 11.0 296,888 東京都 14,086,000 3,205,000 17.6 341,320 沖縄県 1,468,000 350,000 23.8 251,222 …(全 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
# ── この抜粋で使うデータを用意します ──
import numpy as np
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])
df = df[df['SSDSE-B-2026'] == 2023]          # 2023 年の 47 都道府県
X = df[['A1101', 'B4101', 'L3221']].astype(float).values
_aging = df['A1303'] / df['A1101'] * 100
y = y_binary = (_aging >= _aging.median()).astype(int).values   # 高齢化率が中央値以上か
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=42, stratify=y)

from sklearn.model_selection import learning_curve
from sklearn.ensemble import RandomForestClassifier
import numpy as np

sizes, tr_scores, va_scores = learning_curve(
    RandomForestClassifier(n_estimators=100, random_state=42),
    # 47 件しかないので、cv と最小の訓練サイズは小さめにする
    # (train_sizes=0.1 だと 1 分割に各クラス 5 件を用意できない)
    X, y, train_sizes=np.linspace(0.4, 1.0, 4), cv=3, scoring='accuracy',
)

for s, tr, va in zip(sizes, tr_scores.mean(axis=1), va_scores.mean(axis=1)):
    print(f'size={int(s)}: train={tr:.3f}, val={va:.3f}, gap={tr-va:.3f}')

📤 実行例:

size=12: train=1.000, val=0.722, gap=0.278 size=18: train=1.000, val=0.697, gap=0.303 size=24: train=1.000, val=0.676, gap=0.324 size=31: train=1.000, val=0.744, gap=0.256

💬 結果の読み方:訓練サイズ 12→31 件では val 精度が 0.722 → 0.744 とわずかに上下するだけで、 ギャップも 0.278 → 0.256 とほとんど縮まりません。 47 件しかないので、学習曲線がまだ立ち上がってすらいない状態です。 この形からは「あと何件集めれば足りるか」を読み取れません。 学習曲線が 収束していれば「これ以上集めても無駄」、 収束していなければ「集める価値あり」 という判断に直結。

📊 train/val/test の役割 早見表

データ役割使い方回数制限
訓練データパラメータ学習fit() の入力無制限
検証データハイパラ調整・モデル比較predict() で何度も評価回数制限なし
テストデータ最終汎化性能評価最終モデル確定後の 1 度のみ1 回限り
本番データサービス稼働後の入力推論専用継続監視

🧮 SSDSE-B-2026 実値計算 ⑥:adversarial validation

「train と test の分布が違うか」を機械学習で検出する手法 = adversarial validation。 train を 0、 test を 1 とラベル付けして二値分類モデルを作り、 AUC が 0.5 に近ければ「同じ分布」と判断します。

🐍 Python 実装(コード ⑥)

🎯 このコードでやること:年代で分けた train と test を分類モデルに当て、 AUC で識別容易性を測る。

📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行) 都道府県 A1101(総人口) A4101(出生数) A4103(合計特殊出生率) B4101(年平均気温) F3101(新規求職申込件数(一般)) 北海道 5,092,000 24,430 1.06 11.0 156,458 東京都 14,086,000 86,348 0.99 17.6 270,954 沖縄県 1,468,000 12,549 1.6 23.8 43,877 …(全 47 行)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_score

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
# SSDSE は新しい年度が先に並ぶので、cumcount ではなく年度の値そのもので分ける
df['is_test'] = (df['SSDSE-B-2026'] >= 2021).astype(int)  # 後 3 年(2021〜2023 年度)が test

features = ['A1101', 'A4101', 'A4103', 'B4101', 'F3101']
auc = cross_val_score(
    RandomForestClassifier(n_estimators=100, random_state=42),
    df[features].fillna(0), df['is_test'],
    cv=5, scoring='roc_auc',
).mean()
print(f'Adversarial AUC: {auc:.3f}')
print(f'判定: {"分布シフト大" if auc > 0.7 else "分布同一"}')

📤 実行例:

Adversarial AUC: 0.822 判定: 分布シフト大

💬 結果の読み方:AUC 0.822 → 5 つの特徴量だけで「2021〜2023 年度の行か、それ以前か」を 8 割以上の確度で見分けられる ≒ 分布が大きく違う。 KS 検定で最もずれていた合計特殊出生率や新規求職申込件数が年度を追って単調に動くので、 分類器にとって年代の手がかりになる。 この状態でテスト評価をすると、 本番運用時の精度を大きく見誤る 可能性が高い。 対策: domain adaptation、 ドメイン適応学習、 再標準化、 covariate shift correction (重み付け)。

🧮 数式に値を入れて手で計算する: テスト精度の信頼区間

合成 test=200 件、 精度 0.85 の信頼区間を計算する。

Step 1: 二項信頼区間

p̂ = 0.85, n = 200 SE = √(p̂(1-p̂)/n) = √(0.1275/200) ≈ 0.0252 95% CI = 0.85 ± 1.96·0.0252 = [0.801, 0.899]

🐍 Python で再現

1
2
3
4
5
import numpy as np
p_hat = 0.85; n = 200
SE = np.sqrt(p_hat*(1-p_hat)/n)
print(f"SE: {SE:.4f}")
print(f"95% CI: [{p_hat-1.96*SE:.3f}, {p_hat+1.96*SE:.3f}]")

📤 実行結果

SE: 0.0252 95% CI: [0.801, 0.899]

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

🐍 Python 実装

🐍 即使えるスニペット集

スニペット ①:シンプル 8:2 分割

🎯 やること:最小コードで train / test 分割。

1
2
from sklearn.model_selection import train_test_split
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.2, random_state=42)

📤 出力: 各 train/test の配列   💬 random_state 固定で再現性確保。

スニペット ②:Stratified 5-Fold CV

🎯 やること:クラス比率を保持したまま CV。

📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行) 都道府県 SSDSE-B-2026(年度) A1101(総人口) A4101(出生数) A9101(婚姻件数) L3221(消費支出(二人以上の世帯)) 北海道 2,023 5,092,000 24,430 17,281 296,888 東京都 2,023 14,086,000 86,348 71,774 341,320 沖縄県 2,023 1,468,000 12,549 6,316 251,222 …(全 47 行)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# ── この抜粋は「分類」の例です。ここで分類用のデータを用意します ──
import pandas as pd
from sklearn.ensemble import RandomForestClassifier

df_cls = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', header=1)
df_cls = df_cls[df_cls['地域コード'].astype(str).str.match(r'^R\d{5}$', na=False)]
df_cls = df_cls[df_cls['年度'] == df_cls['年度'].max()]
X = df_cls[['総人口', '出生数', '婚姻件数']].astype(float).values
y = (df_cls['消費支出(二人以上の世帯)'].astype(float)
     > df_cls['消費支出(二人以上の世帯)'].astype(float).median()).astype(int).values
model = RandomForestClassifier(n_estimators=100, random_state=42)

from sklearn.model_selection import StratifiedKFold, cross_val_score
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(model, X, y, cv=skf, scoring='accuracy')
print('各 fold の正解率:', scores.round(3))

📤 出力: 各 fold の正解率: [0.7 0.6 0.556 0.667 0.667]   💬 5 つの平均は 0.638 で、中央値で分けた 2 クラスを当て推量する 0.5 を少し上回る程度。人口規模の 3 変数では「消費支出が中央値より上の県」をほとんど見分けられず、0.556 の fold では 9 県中 5 県しか当たっていない。

スニペット ③:Nested CV

🎯 やること:内側でハイパラ最適化、 外側で評価。

📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行) 都道府県 A1101(総人口) A1303(65歳以上人口) A4101(出生数) L3221(消費支出(二人以上の世帯)) 北海道 5,092,000 1,681,000 24,430 296,888 東京都 14,086,000 3,205,000 86,348 341,320 沖縄県 1,468,000 350,000 12,549 251,222 …(全 47 行)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# ── この抜粋で使うデータを用意します ──
import numpy as np
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])
df = df[df['SSDSE-B-2026'] == 2023]          # 2023 年の 47 都道府県
X = df[['A1101', 'A1303', 'L3221']].astype(float).values
y = df['A4101'].astype(float).values          # 出生数
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=42)

from sklearn.linear_model import Ridge
from sklearn.model_selection import GridSearchCV, cross_val_score

model = Ridge()
param_grid = {'alpha': [0.1, 1.0, 10.0]}
inner = GridSearchCV(model, param_grid, cv=5)
outer = cross_val_score(inner, X, y, cv=5)

📤 出力: 5 個の汎化精度   💬 計算は 25 fold 分。 公正性のコスト。

スニペット ④:時系列 split

🎯 やること:未来情報のリークを防ぐ。

1
2
3
4
from sklearn.model_selection import TimeSeriesSplit
tss = TimeSeriesSplit(n_splits=5)
for tr, te in tss.split(X):
    model.fit(X[tr], y[tr]); score = model.score(X[te], y[te])

📤 出力: 5 fold それぞれの score   💬 過去→未来の方向のみで評価。

✅ テスト設計 15 項目チェックリスト

  1. train / val / test の分割比率を文書化したか
  2. random_state を固定して再現性を担保したか
  3. クラス不均衡なら stratify を指定したか
  4. 時系列なら TimeSeriesSplit を使ったか
  5. グループ構造(ユーザー / 患者)があれば GroupKFold を使ったか
  6. 標準化 / 補完を fold 内で完結させたか(Pipeline)
  7. target encoding を全データで作っていないか
  8. 重複行を drop_duplicates したか
  9. テストデータを 1 度しか評価していないか
  10. テスト評価結果でモデル選択していないか
  11. Nested CV を使ってハイパラ調整と評価を分離したか
  12. 分布シフトがないか train/test の特徴量分布を比較したか
  13. 欠損パターンが train/test で揃っているか
  14. クラス分布が train/test/val で揃っているか
  15. 本番データの想定と test の構造が一致しているか

🐍 「テストデータ取扱規程」テンプレート(実務でそのまま使える)

機械学習プロジェクトで テストデータの濫用を防ぐ社内規程 のテンプレート:

  1. テスト データは別ディレクトリで管理 :data/test/ に隔離し、 アクセスログを取る。
  2. テスト評価は専任者のみ:開発者が直接アクセスできない CI で評価。
  3. テスト評価は週 1 回まで:頻度を制限し p ハッキング防止。
  4. テスト結果はモデル選択に使わない:選択は val のみで行う。
  5. テストデータ ID は不可視化:開発者が EDA で見ないようにする。
  6. 本番リリース後は新規 test を集める:使い回しは半年まで。
  7. 分布シフトを毎月確認:adversarial validation を定期実行。
  8. テストデータ漏洩アラート:MLflow / CI に組み込み、 過剰評価を検知。

SSDSE-B-2026 のような公的統計データを Python で扱う際の基本パターン:

📥 入力例(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
 5
 6
 7
 8
 9
10
11
12
import pandas as pd
import numpy as np

# データ読み込み
df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=1)
print(df.shape)
print(df.dtypes)
print(df.describe())

# 「テストデータ」の文脈で扱う場合の例:
# 分野: ML基礎
# 関連手法は同カテゴリの他用語を参照してください。
📤 実行例(実測) (564, 112) 年度 int64 地域コード object 都道府県 object 総人口 int64 総人口(男) int64 ... 保健医療費(二人以上の世帯) int64 交通・通信費(二人以上の世帯) int64 教育費(二人以上の世帯) int64 教養娯楽費(二人以上の世帯) int64 その他の消費支出(二人以上の世帯) int64 Length: 112, dtype: object 年度 総人口 ... 教養娯楽費(二人以上の世帯) その他の消費支出(二人以上の世帯) count 564.000000 5.640000e+02 ... 564.000000 564.000000 mean 2017.500000 2.690688e+06 ... 26931.026596 59784.718085 std 3.455117 2.730951e+06 ... 4219.487086 8813.812956 min 2012.000000 5.370000e+05 ... 14661.000000 35 …(以下略)

💬 skiprows=1 で英字コードの 1 行目を飛ばしたため、列名は「年度」「総人口」「教育費(二人以上の世帯)」のような日本語になり、564 行 × 112 列で数値列は int64 として読み込まれた。年度の mean 2017.5・std 3.46 は 12 年度が同じ行数ずつ入っていることを示し、総人口の mean 約 269 万人は 564 行(県 × 年度)の平均で、ある年の県平均ではない。テストデータを切り出す前に、この 564 行を行単位でランダムに分けると同じ県の別年度が訓練とテストの両方に入ってしまう点に注意する。

具体的なコードは 機械学習の基礎 を参照してください。

🐍 検証用を分けずに 8:2 の 2 分割でテスト評価する

📥 入力例(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
# ── この抜粋は英字の項目コード(A1101 など)を使うので、見出し 1 行目で読み込みます ──
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)]
df = df[df['SSDSE-B-2026'] == df['SSDSE-B-2026'].max()]

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[['A1101', 'A1303']].fillna(0).values
y = df['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

💬 #3 と違って検証用を分けずに 37 県と 10 県の 2 分割にしたが、random_state=42 で選ばれるテストの 10 県は同じで、R² = 0.982(#3 は 0.981)とほぼ等しい。RMSE 3,439 人は出生数の県平均 15,474 人の 2 割強にあたり、R² の高さほど予測が精密なわけではない。テストに東京都(86,348 人)が含まれているので、東京都を除いた 9 県で RMSE を計算し直すと誤差がどこに集中しているかが分かる。

⚠️ よくある落とし穴

⚠️ データリーケージ 10 パターン(最重要)

テストデータの価値を破壊する 「リーケージ (data leakage)」 の典型パターン:

  1. 未来情報の混入:時系列で未来データを特徴量に含める。 → TimeSeriesSplit で防止
  2. 標準化のリーク:StandardScaler.fit を全データで → train 内で fit、 test に transform のみ
  3. 欠損値補完のリーク:全データの平均を使う → train 内の平均で test を埋める
  4. target encoding リーク:target を使う特徴量を全データで作成 → CV 内で作る
  5. 重複行:同一行 (ID 異なるが内容同一) が train と test に → drop_duplicates
  6. グループリーク:同一ユーザーの異なる行が train と test に → GroupKFold
  7. 外部 ID リーク:訓練で見た顧客 ID を特徴量に → ID は除外
  8. 過去ラベル使用:将来予測なのに過去の正解ラベルを特徴量に
  9. サンプリングバイアス:train は均等サンプリング、 test は偏ったサンプリング
  10. テスト精度を見てモデル選択:テスト = 検証になり、 真のテスト精度を二度測れない

🗓 train/test split の歴史年表

年出来事影響
1931Larson によるホールドアウト法概念の起源
1968Mosteller & Tukey による交差検証提案CV の標準化
1974Stone「Cross-validatory choice」論文理論的基礎
1995Kohavi「A Study of Cross-Validation」K-Fold の k=10 が標準に
2007scikit-learn の cross_val_score 公開実装容易化
2012Kaggle で「リーケージ」が問題に業界注意喚起
2018Nested CV の重要性が再認識ハイパラ調整時の標準
2020「Test-set overfitting」の警鐘論文 (Recht et al.)ImageNet 評価見直し
2024LLM 評価で「ベンチマーク汚染」議論事前学習データ漏洩
❌ 過学習に注意
訓練データだけ高精度でも、 未知データで失敗するモデルは無価値。
❌ データの偏りを確認
バイアスのあるデータからは、 バイアスのあるモデルが生まれます。
❌ 指標を単独で見ない
1 つの指標で「良い」と判断せず、 複数の評価軸を併用しましょう。

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

「テストデータ」を実務・試験で扱うときに頻発する典型的なミスです。 各項目を 1 度読んでおけば 9 割の事故が防げます:

❌ Test を何度も見る
Test スコアで反復チューニングすると Test に過適合し、 本番で精度が落ちる。
❌ Test と Train の分布ずれ
時系列データを Random 分割すると未来情報リーク。 時系列順分割を使う。
❌ 分割サイズが小さすぎる
Test が 10 件しかないと統計的に信頼区間が広い。 最低 100–1000 件は確保。
❌ Stratify 忘れ
不均衡データで Random 分割すると少数派が Test に消える。 Stratified split を使う。
❌ リーケージ
前処理(標準化・PCA)を全データで fit してから分割すると Test 情報が混入。 Train で fit、 Test に transform。

🗺 概念マップ

「テストデータ」を中心に、 上流の分割手順・並列の検証手法・下流のリーケージ防止と応用先を 4 方向に展開。

テストデータ train_test_split / 層化抽出 validation / CV / bootstrap data leakage / 汎化性能 model selection / A/B test

概念マップの中心は「テストデータ」で、 前提に train/test split・層化抽出 が、 並列に 検証データ・交差検証 が、 発展に データリーケージ・汎化性能 が、 応用に モデル選択・A/B テスト が並ぶ。

SSDSE-B-2026 を例にすると、 「47 県 × 12 年 = 564 行」を 60/20/20 に分けた test 113 行が右下「並列」枝に対応し、 GroupKFold で県をまたいだリークを防ぐ仕組みが「発展」枝にあたる。 各枝のリンク先で同じデータがどう扱われるかを比較すると理解が深まる。

🔗 隣接手法への橋渡し

テストデータは「分割 → 学習 → 評価」の流れの最後に位置し、 上流と下流で別の手法と接続する。

SSDSE-B-2026 で人口予測モデルを作る場合: 2015-2020 を train、 2021 を val、 2022 を test という時系列分割が正解で、 ランダム分割すると未来情報が混入する。

🌳 手法選択フロー

テストデータの作り方は、 データの性質によって 4 通りに分岐する。

  1. iid データ (47 県の住宅地価など)? Yes → train_test_split(test_size=0.2, random_state=42)、 クラス不均衡なら stratify=y
  2. 同一エンティティが複数行 (47 県 × 12 年のパネル)? Yes → GroupKFold で県をグループに、 県単位でリーク防止
  3. 時系列 (人口推移など)? Yes → TimeSeriesSplit、 「2015-2020 train、 2022 test」のように未来をテストに
  4. サンプル少 (n < 100)? Yes → 5-fold CV + Nested CV で test を分離せず CV ループで評価

誤った分割の代償は大きい: SSDSE-B-2026 で県をまたぐリークがあると、 test 精度が見かけ上 0.74 でも実運用で 0.58 に落ちる (本ページ実値計算参照)。