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

🔖 キーワード索引

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

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

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

💡 30秒で分かる結論

🍰 まずはやさしく

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

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

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

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

最終評価に使うデータ

💡 30秒で分かる結論

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

🍰 まずはやさしく

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

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

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

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

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

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

このページは、 ジャストインタイム型データサイエンス教育の用語集の一部です。 本概念は、 統計学・データサイエンス・機械学習・AI の文脈で頻出する重要キーワードであり、 SSDSE-B-2026 都道府県データを使った実例とともに、 定義・直感・数式・実装・落とし穴・関連用語を体系的に学べる構成になっています。 まずは「30 秒で分かる結論」と「直感で掴む」セクションから読み、 必要に応じて数式・実装の詳細に進むことをおすすめします。

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

テストデータ (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 で最終評価」を分ける。

12. 実装チェックリスト

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 設計が実務に耐える。

14. 振り返り

テストデータは「数値を出すための手段」ではなく、 「モデルがどのような条件で動くかを記述する仕様書」と考えると設計のメリハリがつく。 SSDSE-B のような公開データを使う授業や研究でも、 train/test の切り方を 1 文で説明できるか自問するだけで分析の解釈可能性が大きく改善する。

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 の典型運用パターンとして覚えておきたい。

18. 最終チェックポイント

test データの設計が適切かを最終確認する 5 項目: (1) train との分布差を可視化したか、 (2) サンプル数は標準誤差を業務閾値以下に収めるか、 (3) leakage の lint を CI に組み込んだか、 (4) 評価指標は主+副の複数で監視するか、 (5) 半年〜1 年ごとに test を refresh する運用フローがあるか。 すべて YES なら、 そのモデル評価は「再現可能」「保守可能」「説明可能」の三拍子を満たす。

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

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 が高い = 良いモデル」ではなく、 「業務コストが最小 = 良いモデル」という視点に切り替えるのが、 データサイエンティストとしての成熟度を分ける分水嶺だ。

11. 締めくくり

テストデータは設計・管理・運用のすべてが品質を決める。 「test しか見ない」運用、 「同じ test を何度も使う」運用、 「test の更新を怠る」運用は、 すべて長期的にモデル品質を蝕む。 SSDSE-B-2026 のような実データを使う研究・教育プロジェクトで、 上記 10 観点を意識的に実装すれば、 研究的にも実務的にも堅牢なテスト評価基盤が作れる。 本ページで紹介した 18 観点 (R671 deepdive) + 10 観点 (R691 extension) を合わせて、 テストデータ運用の包括的フレームワークとして活用してほしい。 教育的には「test を聖域として扱う」「定期的に refresh する」「ドリフトを監視する」「業務指標で評価する」の 4 原則を最初に身につけることが、 機械学習エンジニアとしてのキャリアを長期的に支える基盤になる。

🎨 直感で掴む

🍰 まずはやさしく

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

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

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

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

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

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

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

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

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

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

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

業界タスク分割戦略注意点
金融株価予測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)$テストペア

🔬 発展トピック

「テストデータ」を入門レベルで習得した次に進むべき発展テーマ:

① 理論的拡張

基本概念を 確率論・情報理論・最適化理論の観点で再定式化すると、 隣接する手法との理論的な関係が見えてきます。 たとえば 正則化は事前分布の最大事後推定と等価クロスエントロピー損失は KL ダイバージェンスを最小化、 といった対応関係を押さえると教科書間の往復が楽になります。

② 実装的拡張

scikit-learn 標準実装の外側に出ると、 GPU 対応・分散学習・低精度浮動小数点(fp16/bf16)・量子化(int8)・グラフ最適化(TorchScript・ONNX Runtime)など、 推論性能を 10–100 倍引き上げるテクニックが豊富にあります。 本番運用では モデル精度と推論コストのトレードオフを意識した実装が鍵。

③ 評価・解釈の拡張

予測精度だけでなく SHAP・LIME・Permutation Importance によるモデル解釈、 Calibration(確率の校正)Counterfactual ExplanationFairness 指標(demographic parity, equalized odds 等)を組合せると、 業務応用での説得力が一段増します。

④ 業界応用

医療(薬機法・GxP)・金融(モデル管理ガイドライン)・公共(個人情報保護法)など、 業界固有の規制・ガイドラインを モデル設計段階から埋め込むのが現代のスタンダード。 「テストデータ」を業務適用するときは、 ドメインの専門家・法務との早期コラボレーションが成否を分けます。

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

テストデータは 「モデルが訓練中に一度も見ていない」という条件付きの保留データ で、 汎化性能の不偏推定 を得るための唯一の手段です。 「テストデータをハイパラ調整に使う=バリデーションへ降格」「複数回テストして最良を選ぶ=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 のような国勢調査ベースのデータも、5 年ごとに更新されるので、それに合わせたモデル更新計画が必要である。

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

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

散布図
SSDSE-B-2026(2023)の散布図。横軸:総人口、縦軸:一般診療所数。両者は強い正の相関(r=0.972)を示す。分割の際は Train/Test でこうした関係が偏らないよう注意する。
ヒストグラム
SSDSE-B-2026 の総人口の分布(ヒストグラム)。少数の大都市圏が右裾を作る右歪の分布で、Train/Test 分割時はこの偏りが両セットに保たれるよう層化が望ましい。
箱ひげ図
KMeans クラスタ別の一般診療所数の箱ひげ図。クラスタごとに水準が異なり、分割時にクラスタ構成が Train/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 抜粋):

SSDSE-B-2026 都道府県 総人口 消費支出 R01000 北海道 5183 19,3920 R13000 東京都 14048 115,7140 R27000 大阪府 8809 40,6010 R47000 沖縄県 1457 4,5670 ... (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
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)
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)

# 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 CV R² = 0.069 Test R² = 0.153(この値を見た後の再調整は禁止)

💬 Train CV 0.069 → Test 0.153 と、 Test のほうが高く出た。 「Test は Train より低くなるはず」という思い込みは正しくない。 この 3 特徴量(総人口・出生数・婚姻件数)では消費支出はほとんど説明できず、 どちらの値も 0 に近いところで揺れているだけである。 Test は 113 件しかないので、 このくらいの差は偶然で簡単に出る。 だからこそ「Test 0.153 を見てから Ridge の alpha を 1.0 → 0.5 に変える」を絶対にしてはいけない。 それを許すと 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 は わずか 9 県。この少なさは「Test 指標の信頼区間が大きく、決定的な結論を出しにくい」ことを意味する。例えば回帰の R² が 0.92 と出ても、Test を別の 9 県に取り直せば 0.78 や 0.96 に振れることは普通。教育目的なら点推定でも十分だが、ビジネス意思決定や論文投稿では「Test 性能の信頼区間」「ブートストラップ標準誤差」「複数 Test 分割の中央値」を併記すべきである。

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

Test n指標標準誤差の目安解釈
9 (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 信頼区間を計算し、「点推定 0.92」の不確実性を可視化する。

📥 入力データ: 上記 Pipeline で fit 済 pipeX_te, y_te(9 件)

 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.153 95% Bootstrap CI = [0.001, 0.298] CI 幅 = 0.296(広いほど Test n 不足)

💬 点推定 0.921 だけ見ると「素晴らしいモデル」に見えるが、95%CI は [0.751, 0.989] と非常に広い。「0.92 だから 0.85 のベンチマークモデルより優れる」とは断言できない—Test n=9 では CI が重なる可能性が高い。報告時は必ず 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 2024 と 2026 で Population Stability Index (PSI) を計算し、Test データの再構築が必要か判定する。

📥 入力データ: 2 年分の SSDSE-B 都道府県人口(参考 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
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_2024 = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=1)
df_2026 = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=1)

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

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

PSI (総人口 2024→2026) = 0.0000 → 安定(Test 再構築不要)

💬 都道府県人口は 2 年では大きく変わらないため PSI は 0.02 と安定。一方、「インバウンド観光客数」「半導体輸出額」などボラティリティの高い指標では 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
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'})

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}  ← 最終評価')

▲ 上記コードはそのまま実行可能。 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
from sklearn.model_selection import TimeSeriesSplit
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_absolute_error
import numpy as np

# 東京の年次人口 (A1101) で時系列 split
tokyo = df[df['Prefecture'] == '東京都'][['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)):
    m = 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-2, MAE=24580 人 Fold 1: train idx 0-2, test idx 3-3, MAE=18420 人 Fold 2: train idx 0-3, test idx 4-4, MAE=15100 人 Fold 3: train idx 0-4, test idx 5-5, MAE=12830 人 Fold 4: train idx 0-5, test idx 6-6, MAE=10250 人

💬 結果の読み方:train データが累積されるにつれて MAE が 単調減少(24580 → 10250 人)。 これは「過去の情報を多く蓄えるほど予測が安定する」自然な傾向。 時系列分割は 学習可能性の見極め に役立ちます。

⚠️ データリーケージ 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 評価で「ベンチマーク汚染」議論事前学習データ漏洩

📚 まとめカード — 30 秒で復習

📝 試験・面接で頻出 Q&A 10 選

  1. Q: 検証データとテストデータの違いは? A: 検証はハイパラ調整用、 テストは最終評価用(1 度のみ使用)。
  2. Q: K-Fold の K はいくつにすべき? A: 5 or 10 が標準。 計算コストとの折衷。
  3. Q: Stratified K-Fold を使うべき時は? A: クラス不均衡な分類問題。
  4. Q: GroupKFold が必要な状況は? A: 同一ユーザー / 患者 / セッションが複数行ある場合。
  5. Q: 時系列で K-Fold を使ってはいけない理由は? A: 時間順序が崩れ未来情報がリークする。
  6. Q: Nested CV は何のため? A: ハイパラ最適化を外側 CV と分離し、 公正な評価を得る。
  7. Q: テストデータをハイパラ調整に使うと何が起きる? A: テストが実質「検証」になり、 真の汎化性能を測れなくなる。
  8. Q: train/test split で必ず stratify を使うべき? A: クラス不均衡な分類なら yes、 回帰や均衡ならoptional。
  9. Q: テスト精度が訓練精度より高い場合の原因は? A: ラベルリーク / 分布ズレ / サンプリングバイアス。
  10. Q: 「テストはなぜ 1 度だけ」? A: 何度も評価すると意図的・無意識的に最良を選び、 結果がテストに過学習する。

🐍 即使えるスニペット集

スニペット ①:シンプル 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
17
18
19
20
# ── ここから下は「分類」の例です ──
# 直前までの y は東京の人口(連続値)なので、そのままでは分類できません。
# 47 都道府県を「消費支出が中央値より上か下か」の 2 クラスに分けて使います。
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)

print('クラスごとの件数:', pd.Series(y).value_counts().to_dict())

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')

📤 出力: array([0.92, 0.88, ...])   💬 5 fold すべてでクラス比率保持。

スニペット ③: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 の構造が一致しているか

🌐 関連用語マップ

前提: 訓練データ検証データデータセットサンプル

並列: 交差検証ホールドアウトブートストラップK-Fold

発展: データリーケージ過学習汎化A/B テスト

応用: モデル評価グリッドサーチハイパーパラメータ・ベンチマーク

🧮 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
14
import pandas as pd
from scipy.stats import ks_2samp

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
df['year_idx'] = df.groupby('Prefecture').cumcount()

train = df[df['year_idx'] < 9]
test = df[df['year_idx'] >= 9]

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.071, p=0.6496 A4101: KS=0.170, p=0.0040 !!! A4103: KS=0.163, p=0.0067 !!! B4101: KS=0.241, p=0.0000 !!! F3101: KS=0.260, p=0.0000 !!!

💬 結果の読み方:A4101(出生数)・A4103(合計特殊出生率)・B4101(年平均気温)・F3101(新規求職申込件数)の 4 変数で分布シフトが検出 (p < 0.05)。 これは 少子化の進行など後期 3 年間で水準が変化 しているため。 こうした特徴量を 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 回限り
本番データサービス稼働後の入力推論専用継続監視

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

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

🧮 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])
df['year_idx'] = df.groupby('Prefecture').cumcount()
df['is_test'] = (df['year_idx'] >= 9).astype(int)  # 後 3 年が 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.779 判定: 分布シフト大

💬 結果の読み方:AUC 0.779 → 「train と test の年代差を簡単に当てられる」≒ 分布が大きく違う。 この状態でテスト評価をすると、 本番運用時の精度を大きく見誤る 可能性が高い。 対策: domain adaptation、 ドメイン適応学習、 再標準化、 covariate shift correction (重み付け)。

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

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

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

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

🐍 Python 実装(コード ②)

📥 入力例(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
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# ── この抜粋は「分類」の例です。ここで分類用のデータを用意します ──
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)

# グループ分割用のグループ(ここでは地方ブロック)を作る
groups = df_cls['地域コード'].astype(str).str[1:3].values   # 都道府県コードでグループ化

from sklearn.model_selection import GroupKFold, cross_val_score
from sklearn.ensemble import RandomForestClassifier

groups = df_cls['都道府県']
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.527 ± 0.127

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

🐍 Python 実装(コード ③)

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from sklearn.model_selection import TimeSeriesSplit
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_absolute_error
import numpy as np

# 東京の年次人口 (A1101) で時系列 split
tokyo = df[df['Prefecture'] == '東京都'][['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)):
    m = 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-2, MAE=24580 人 Fold 1: train idx 0-2, test idx 3-3, MAE=18420 人 Fold 2: train idx 0-3, test idx 4-4, MAE=15100 人 Fold 3: train idx 0-4, test idx 5-5, MAE=12830 人 Fold 4: train idx 0-5, test idx 6-6, MAE=10250 人

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

🐍 Python 実装(コード ④)

📥 入力例(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
import pandas as pd
from scipy.stats import ks_2samp

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
df['year_idx'] = df.groupby('Prefecture').cumcount()

train = df[df['year_idx'] < 9]
test = df[df['year_idx'] >= 9]

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.071, p=0.6496 A4101: KS=0.170, p=0.0040 !!! A4103: KS=0.163, p=0.0067 !!! B4101: KS=0.241, p=0.0000 !!! F3101: KS=0.260, p=0.0000 !!!

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

🐍 Python 実装(コード ⑤)

📥 入力例(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
17
18
19
20
21
22
23
# ── この抜粋は「分類」の例です。ここで分類用のデータを用意します ──
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 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),
    X, y, train_sizes=np.linspace(0.1, 1.0, 5), cv=5, 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=3: train=1.000, val=0.511, gap=0.489 size=12: train=1.000, val=0.620, gap=0.380 size=20: train=1.000, val=0.598, gap=0.402 size=28: train=1.000, val=0.551, gap=0.449 size=37: train=1.000, val=0.636, gap=0.364

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

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

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

🐍 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])
df['year_idx'] = df.groupby('Prefecture').cumcount()
df['is_test'] = (df['year_idx'] >= 9).astype(int)  # 後 3 年が 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.779 判定: 分布シフト大

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

合成 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.800, 0.900]

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

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

🐍 Python 実装(コード ②)

📥 入力例(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
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# ── この抜粋は「分類」の例です。ここで分類用のデータを用意します ──
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)

# グループ分割用のグループ(ここでは地方ブロック)を作る
groups = df_cls['地域コード'].astype(str).str[1:3].values   # 都道府県コードでグループ化

from sklearn.model_selection import GroupKFold, cross_val_score
from sklearn.ensemble import RandomForestClassifier

groups = df_cls['都道府県']
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.527 ± 0.127

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

🐍 Python 実装(コード ③)

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from sklearn.model_selection import TimeSeriesSplit
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_absolute_error
import numpy as np

# 東京の年次人口 (A1101) で時系列 split
tokyo = df[df['Prefecture'] == '東京都'][['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)):
    m = 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-2, MAE=24580 人 Fold 1: train idx 0-2, test idx 3-3, MAE=18420 人 Fold 2: train idx 0-3, test idx 4-4, MAE=15100 人 Fold 3: train idx 0-4, test idx 5-5, MAE=12830 人 Fold 4: train idx 0-5, test idx 6-6, MAE=10250 人

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

🐍 Python 実装(コード ④)

📥 入力例(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
import pandas as pd
from scipy.stats import ks_2samp

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
df['year_idx'] = df.groupby('Prefecture').cumcount()

train = df[df['year_idx'] < 9]
test = df[df['year_idx'] >= 9]

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.071, p=0.6496 A4101: KS=0.170, p=0.0040 !!! A4103: KS=0.163, p=0.0067 !!! B4101: KS=0.241, p=0.0000 !!! F3101: KS=0.260, p=0.0000 !!!

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

🐍 Python 実装(コード ⑤)

📥 入力例(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
17
18
19
20
21
22
23
# ── この抜粋は「分類」の例です。ここで分類用のデータを用意します ──
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 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),
    X, y, train_sizes=np.linspace(0.1, 1.0, 5), cv=5, 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=3: train=1.000, val=0.511, gap=0.489 size=12: train=1.000, val=0.620, gap=0.380 size=20: train=1.000, val=0.598, gap=0.402 size=28: train=1.000, val=0.551, gap=0.449 size=37: train=1.000, val=0.636, gap=0.364

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

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

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

🐍 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])
df['year_idx'] = df.groupby('Prefecture').cumcount()
df['is_test'] = (df['year_idx'] >= 9).astype(int)  # 後 3 年が 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.779 判定: 分布シフト大

🐍 即使えるスニペット集

🐍 即使えるスニペット集

スニペット ①:シンプル 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
# ── この抜粋は「分類」の例です。ここで分類用のデータを用意します ──
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')

📤 出力: array([0.92, 0.88, ...])   💬 5 fold すべてでクラス比率保持。

スニペット ③: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基礎
# 関連手法は同カテゴリの他用語を参照してください。

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

📝 レポートでの報告

分析結果を報告するときに含めるべき情報:

✅ チェックリスト

🔗 同カテゴリの他用語

データリーケージ訓練・テスト分割教師あり学習教師なし学習強化学習分類回帰タスク目的変数説明変数特徴量訓練データ検証データ過学習汎化

🐍 Python 実装バリエーション

「テストデータ」を扱う代表的なライブラリ別実装。 同じ目的でも書き方が違うため、 自分のプロジェクトの依存関係に合わせて選択する:

① pandas + numpy(最小依存)

📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行) 都道府県 A1101(総人口) A4101(出生数) A5101(転入者数(日本人移動者)) 北海道 5,092,000 24,430 47,388 東京都 14,086,000 86,348 406,749 沖縄県 1,468,000 12,549 26,410 …(全 47 行)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# ── この抜粋で使うデータを用意します(英字の項目コードで読み込み)──
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()]

# ── この抜粋は英字の項目コード(A1101 など)を使うので、見出し 1 行目で読み込みます ──
import pandas as pd

import pandas as pd
import numpy as np

df = df.rename(columns={df.columns[2]: 'pref'})

print('行数:', len(df), '列数:', df.shape[1])
print(df[['pref', 'A1101', 'A4101', 'A5101']].head())

② scikit-learn(学習・評価)

📥 入力例(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}')

③ scipy.stats(統計検定・分布)

📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行) 都道府県 A1101(総人口) A4101(出生数) 北海道 5,092,000 24,430 東京都 14,086,000 86,348 沖縄県 1,468,000 12,549 …(全 47 行)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# ── この抜粋は英字の項目コード(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 scipy import stats
# 見出し 1 行目で読むと数値も文字列になるので、使う列だけ数値に直す
df['A1101'] = pd.to_numeric(df['A1101'], errors='coerce')
df['A4101'] = pd.to_numeric(df['A4101'], errors='coerce')
df = df.dropna(subset=['A1101', 'A4101'])

# 例: 2 変数の Pearson 相関 + p 値
r, p = stats.pearsonr(df['A1101'], df['A4101'])
print(f'相関係数 r = {r:.3f}, p 値 = {p:.2e}')

# 例: 1 標本 t 検定(平均が一定値と異なるか)
t, p = stats.ttest_1samp(df['A4101'], popmean=df['A4101'].mean())
print(f't = {t:.3f}, p = {p:.3f}')

④ 可視化(matplotlib + seaborn)

📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行) 都道府県 A1101(総人口) A4101(出生数) 北海道 5,092,000 24,430 東京都 14,086,000 86,348 沖縄県 1,468,000 12,549 …(全 47 行)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# ── この抜粋は英字の項目コード(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()]

import matplotlib.pyplot as plt
import seaborn as sns

fig, ax = plt.subplots(figsize=(8,5))
sns.scatterplot(data=df, x='A1101', y='A4101', ax=ax)
ax.set_xlabel('総人口')
ax.set_ylabel('出生数')
ax.set_title(f'{len(df)} 都道府県の関係')
plt.tight_layout()
plt.savefig('out.png', dpi=120)
plt.close()

🐍 即使えるスニペット集

スニペット ①:シンプル 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
# ── この抜粋は「分類」の例です。ここで分類用のデータを用意します ──
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')

📤 出力: array([0.92, 0.88, ...])   💬 5 fold すべてでクラス比率保持。

スニペット ③: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 に組み込み、 過剰評価を検知。

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

⚠️ データリーケージ 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。

⚠️ データリーケージ 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 評価で「ベンチマーク汚染」議論事前学習データ漏洩

🗺 概念マップ

「テストデータ」を中心に、 上流の分割手順・並列の検証手法・下流のリーケージ防止と応用先を 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 に落ちる (本ページ実値計算参照)。

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

🐍 Python 実装(コード ②)

📥 入力例(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
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# ── この抜粋は「分類」の例です。ここで分類用のデータを用意します ──
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)

# グループ分割用のグループ(ここでは地方ブロック)を作る
groups = df_cls['地域コード'].astype(str).str[1:3].values   # 都道府県コードでグループ化

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.527 ± 0.127

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

🐍 Python 実装(コード ③)

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

📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行) 都道府県 A1101(総人口) A4101(出生数) A4103(合計特殊出生率) 北海道 5,092,000 24,430 1.06 東京都 14,086,000 86,348 0.99 沖縄県 1,468,000 12,549 1.6 …(全 47 行)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# ── この抜粋は英字の項目コード(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)]
# 時系列の例なので、年度は絞らずに全年度を使う

from sklearn.model_selection import TimeSeriesSplit
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_absolute_error
import numpy as np

# 東京の年次人口 (A1101) で時系列 split
tokyo = df[df['Prefecture'] == '東京都'][['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)):
    m = 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-2, MAE=24580 人 Fold 1: train idx 0-2, test idx 3-3, MAE=18420 人 Fold 2: train idx 0-3, test idx 4-4, MAE=15100 人 Fold 3: train idx 0-4, test idx 5-5, MAE=12830 人 Fold 4: train idx 0-5, test idx 6-6, MAE=10250 人

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

🐍 Python 実装(コード ④)

📥 入力例(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
import pandas as pd
from scipy.stats import ks_2samp

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
df['year_idx'] = df.groupby('Prefecture').cumcount()

train = df[df['year_idx'] < 9]
test = df[df['year_idx'] >= 9]

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.071, p=0.6496 A4101: KS=0.170, p=0.0040 !!! A4103: KS=0.163, p=0.0067 !!! B4101: KS=0.241, p=0.0000 !!! F3101: KS=0.260, p=0.0000 !!!

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

🐍 Python 実装(コード ⑤)

📥 入力例(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
17
18
19
20
21
22
23
# ── この抜粋は「分類」の例です。ここで分類用のデータを用意します ──
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 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),
    X, y, train_sizes=np.linspace(0.1, 1.0, 5), cv=5, 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=3: train=1.000, val=0.511, gap=0.489 size=12: train=1.000, val=0.620, gap=0.380 size=20: train=1.000, val=0.598, gap=0.402 size=28: train=1.000, val=0.551, gap=0.449 size=37: train=1.000, val=0.636, gap=0.364

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

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

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

🐍 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])
df['year_idx'] = df.groupby('Prefecture').cumcount()
df['is_test'] = (df['year_idx'] >= 9).astype(int)  # 後 3 年が 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.779 判定: 分布シフト大

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

合成 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.800, 0.900]

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