本ページは テストデータ(Test Data)を 12 のセクションで多角的に解説します。 上のチップは検索・関連語の手がかりです。 以下のリンクで各セクションに直接ジャンプできます:
🍰 まずはやさしく
最後に実力をはかるためのデータです。
正しく性能を評価するために使います。
テスト前に解く模擬試験のようなものです。
まずは結論を短く確認しましょう。
最終評価に使うデータ
🍰 まずはやさしく
学習に使わない未知のデータのことです。
本番で通用するかを確認するために使います。
部活の練習試合で実力を試す感覚です。
どのような場面で使うかを見ていきましょう。
テストデータは 学習にも検証にも使われず、 最終的な汎化性能評価のみに使う未知データ。 通常 Train / Validation / Test の 3 分割で確保し、 Test は最後の 1 回だけ触るのが鉄則。 Test を何度も見るとリーケージで Test スコアが過剰になる。
このページは、 ジャストインタイム型データサイエンス教育の用語集の一部です。 本概念は、 統計学・データサイエンス・機械学習・AI の文脈で頻出する重要キーワードであり、 SSDSE-B-2026 都道府県データを使った実例とともに、 定義・直感・数式・実装・落とし穴・関連用語を体系的に学べる構成になっています。 まずは「30 秒で分かる結論」と「直感で掴む」セクションから読み、 必要に応じて数式・実装の詳細に進むことをおすすめします。
テストデータ (test set) は機械学習の評価品質を左右する最重要部品の一つだが、 「訓練データの余りを test に回す」「期間で適当に切る」など軽視されがちな扱いが多い。 ここでは SSDSE-B-2026 を題材に、 テストデータの設計・隔離・運用を 12 観点で整理する。
training は重み最適化、 validation はハイパーパラメータと early stopping、 test は最終評価。 test を validation のように繰り返し見ると、 暗黙のうちにモデル選択が test に依存して評価が水増しされる (test set chasing) ので、 「最終リリース前 1 回だけ評価」が原則。
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 を回す」のいずれかを採る。
SSDSE-B-2026 のような cross-sectional データなら無作為分割で OK だが、 時系列なら 必ず時間順分割 にする。 訓練データの最新時点より後を test にしないと、 「未来情報が訓練に混入してリーケージ」が起きる。 株価予測・需要予測ではこの罠が頻発する。
同一ユーザー・同一店舗・同一患者のサンプルが複数あるデータでは、 グループ単位で互いに素にする (GroupKFold)。 同じユーザーのサンプルが train と test に混ざると、 モデルは「そのユーザーの特徴」を覚えるだけで本来の汎化を学べない。
test データは PR レビューが終わったら「凍結ディレクトリ」に移動し、 通常の開発フローからアクセスできないようにする。 git submodule や暗号化、 別リポジトリでの管理が現場の解。 「うっかり EDA で test を見てしまった」という事故は珍しくない。
運用が長期になると本番分布と test 分布が乖離する (data drift)。 6 ヶ月〜1 年ごとに新しい test を確保し、 古い test と並列で評価することで「ドリフトのスピード」を計測する。 ドリフトが大きければ再学習や model refresh のシグナルになる。
公開済の test set (ImageNet, MNIST, GLUE, SSDSE 等) は何百万回も評価された結果、 「コミュニティ全体がその test に過剰適合」する現象 (benchmark leakage) が知られている。 公開 test の数値だけで意思決定せず、 自前 hold-out を必ず併用する。
大規模データ (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 で最終評価」を分ける。
「人口減少率が 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 設計が実務に耐える。
テストデータは「数値を出すための手段」ではなく、 「モデルがどのような条件で動くかを記述する仕様書」と考えると設計のメリハリがつく。 SSDSE-B のような公開データを使う授業や研究でも、 train/test の切り方を 1 文で説明できるか自問するだけで分析の解釈可能性が大きく改善する。
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 に充て、 「モデル (= 解釈や仮説) が新しい人にも適用できるか」を確認する。
SSDSE-B-2026 の 47 件で stratified 5-fold を回す典型的なコード設計を示す。 fold ごとに 9-10 件の test が残り、 5 回の平均性能を最終指標として報告する。
このコードは 5-fold で 47 件を約 37:10 に分割し、 5 回繰り返して平均性能を評価する。 SSDSE-B-2026 のような small data の典型運用パターンとして覚えておきたい。
test データの設計が適切かを最終確認する 5 項目: (1) train との分布差を可視化したか、 (2) サンプル数は標準誤差を業務閾値以下に収めるか、 (3) leakage の lint を CI に組み込んだか、 (4) 評価指標は主+副の複数で監視するか、 (5) 半年〜1 年ごとに test を refresh する運用フローがあるか。 すべて YES なら、 そのモデル評価は「再現可能」「保守可能」「説明可能」の三拍子を満たす。
テストデータは「設計時に作る」のではなく「運用しながら育てる」ものだ。 SSDSE-B-2026 のような公開統計データを使う場合でも、 新しい都道府県データが毎年更新されるため、 古いテストセットの代表性が時間とともに低下する。 対策として、 (1) テストデータのバージョン管理 (Git LFS, DVC)、 (2) サンプル数・属性分布の年次レビュー、 (3) 重要属性ごとの代表サンプル保持、 (4) outlier・boundary case の保管庫構築、 (5) ドメインエキスパートによる質的検証、 の 5 軸で運用する。 特に (4) は重要で、 「珍しいが起こりうるケース」を意図的に保持することで、 モデルの未知ケース耐性 (robustness) を継続評価できる。 例えば SSDSE-B-2026 では「沖縄県の高齢化率の特異性」「東京都の人口密度の極端さ」「秋田県の人口減少速度」など、 各都道府県の特徴的属性をテストセットに必ず含める設計が望ましい。
分類モデルの公平性検証では、 テストデータが「保護属性」 (人種、 性別、 年齢、 地域 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 なら、 地方への過小予測リスクとして要監視。
ImageNet, GLUE, SuperGLUE などの著名ベンチマークは何百万回も評価され、 結果として「コミュニティ全体がこれらに過剰適合 (overfitting to benchmark)」する現象が知られている。 ImageNet で SOTA を更新したモデルが、 実世界画像で性能が落ちる例は枚挙にいとまがない。 SSDSE のような公開統計データも、 「教育例として広く使われる」ことで類似の現象が起こり得る。 対策: 自前の hold-out (時系列で分離)、 ドメイン特化テスト (秋田だけの限定モデル) の併用。 また「ベンチマークでの順位」と「実運用での価値」は別物だと割り切ること。 NeurIPS や ICML の論文でも、 単一ベンチマーク SOTA だけで主張するものは査読で減点される傾向が強まっている。 複数ベンチマーク + 自前のドメインテスト + ablation study の三点セットが新標準だ。
画像認識では、 推論時に入力画像を複数の augmentation (flip, crop, color jitter) で変換し、 アンサンブル予測を取る TTA が標準化している。 統計データでは類似の手法として、 「予測時の特徴量摂動 (noise injection)」「ブートストラップによる予測区間推定」「特徴量変換 (log, sqrt) の併用」がある。 SSDSE で人口減少率予測する際、 入力データを 5% の標準偏差で摂動して 100 通り予測し、 平均と分散を取れば堅牢な予測値が得られる。 TTA の副次効果として、 予測の不確実性を定量化できる点も重要だ。 同じ入力に対し、 摂動を加えた予測の標準偏差が大きい点 = モデルが自信を持てない領域、 と解釈できる。 これを「decision deferral」 (人間判断への委譲) のトリガーに使うことで、 自動化と人間判断のハイブリッド運用が実現する。
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 度を簡易確認できる。
「同じテストデータで同じ評価が出る」は研究の基本前提だが、 (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 に組み込むことで完全な再現性を担保できる。 研究の信頼性は再現性と直結するため、 ここを軽視するとレビューで大きく減点される。
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 つとして位置づけられている。
「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 回での予測分布全体を評価対象とする方法も推奨される。
機械学習の test 評価は「アルゴリズムの良さ」を測るが、 業務的に重要なのは「ビジネス指標の改善」だ。 テストデータ上で AUC=0.95 でも、 「false positive 1 件あたりのコスト」を考慮すると AUC=0.85 のモデルの方が業務的に優位なケースが多々ある。 SSDSE-B-2026 で「人口減少リスク予測」を扱うなら、 false positive (リスクと誤判定) のコスト = 不要な対策費用、 false negative (リスクを見逃し) のコスト = 自治体の財政破綻リスク、 を金額換算してから評価指標を設計する。 「AUC が高い = 良いモデル」ではなく、 「業務コストが最小 = 良いモデル」という視点に切り替えるのが、 データサイエンティストとしての成熟度を分ける分水嶺だ。
テストデータは設計・管理・運用のすべてが品質を決める。 「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。
この用語を理解・使用するときは、 次のような前提を意識してください:
テストデータを数式 / 形式定義で表す:
テストスコアは本番分布 $P_{\text{prod}}$ からの抽出に対する期待性能の推定値。 真の汎化誤差の不偏推定とみなせる。
上の数式に出てきた記号を 1 つずつ解説します。 数式が出てくる試験問題(統計検定・G 検定・基本情報)では、 各記号の意味を答えられるかが分岐点:
| 記号 | 意味 |
|---|---|
| $P_{\text{prod}}$ | 本番データの分布(既知でない真の分布) |
| Metric | RMSE・F1・AUC など |
| $f$ | 学習済みモデル |
| $(\mathbf{x}, y)$ | テストペア |
「テストデータ」を入門レベルで習得した次に進むべき発展テーマ:
基本概念を 確率論・情報理論・最適化理論の観点で再定式化すると、 隣接する手法との理論的な関係が見えてきます。 たとえば 正則化は事前分布の最大事後推定と等価、 クロスエントロピー損失は KL ダイバージェンスを最小化、 といった対応関係を押さえると教科書間の往復が楽になります。
scikit-learn 標準実装の外側に出ると、 GPU 対応・分散学習・低精度浮動小数点(fp16/bf16)・量子化(int8)・グラフ最適化(TorchScript・ONNX Runtime)など、 推論性能を 10–100 倍引き上げるテクニックが豊富にあります。 本番運用では モデル精度と推論コストのトレードオフを意識した実装が鍵。
予測精度だけでなく SHAP・LIME・Permutation Importance によるモデル解釈、 Calibration(確率の校正)、 Counterfactual Explanation、 Fairness 指標(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 CV | n < 50 | 最大データ活用 | 超高計算量 |
| Time-Series Split | 時系列 | 時間的順序保持 | 前期 train 少ない |
| GroupKFold | 同一グループ重複 | リーク防止 | グループ識別必要 |
| Nested CV | ハイパラ調整 | 公正な評価 | 超高計算量 |
機械学習モデルは、与えられた訓練データから「パターン」を学習する。しかし学習したパターンが本当に有効かどうかは、訓練時には絶対に見せていない別のデータで試して初めて分かる。これが「テストデータ」の存在意義である。模試で 100 点を取った受験生が、本試験でも 100 点を取れるとは限らない。模試の問題を丸暗記してしまえば模試の点数だけは伸びるが、本試験の問題は別物なので得点が下がる。これと同じ現象が機械学習でも起きる。これを「過学習(overfitting)」と呼び、過学習を検出するための唯一の手段がテストデータでの評価である。SSDSE-B-2026 の 47 都道府県データを使って、人口を予測するモデルを作る場面を考えてみよう。47 県全部で学習し、47 県全部で評価すれば、当然ほぼ完璧な精度が出る。だがそのモデルが「未知の県」(例えば架空の 48 番目の県)で機能する保証はどこにもない。だからこそ、最初から 47 県のうち例えば 9 県(約 20%)を「テスト用」として隔離しておき、残り 38 県だけで学習し、最後に 9 県で評価する必要がある。この 9 県でも高精度が出れば、モデルは「未知の県でも機能する」と推定できる。
実務では単純な 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% 精度が出れば、その値は信頼できる「未来予測の期待値」である。
Hold-out 法は最もシンプルな分割方法で、データを一度だけ Train/Test に分け、それで評価する。47 県を 38 県(Train)+ 9 県(Test)に分ければそれで終わりである。実装が簡単で計算量も少ないため、データが大量にある場合(例:10 万行以上)に有効である。ただし短所もある。それは「偶然の偏り」に弱いことだ。9 県をテスト用に抜く時、たまたま大都市圏ばかりが選ばれれば、地方の精度は分からない。逆に過疎県ばかり選ばれれば、人口予測モデルの「大都市での精度」が見えない。データが少ない(例:47 県のみ)場合、たった一度の分割では運の要素が大きすぎる。そこで次に紹介する 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 倍かかる。
K-Fold の極端形が Leave-One-Out(一個抜き法)である。47 県のうち 1 県を Test に、残り 46 県を Train にする。これを 47 回繰り返す。データの「ほぼ全て」を訓練に使えるので、データ少数時の最終手段として有効である。ただし計算コストは K=47 倍に跳ね上がる。また各 Test サンプルが 1 件なので、評価値の分散が大きく、必ずしも安定するとは限らない。SSDSE-B-2026 のような 47 件のデータでも、シンプルなモデル(線形回帰など)なら LOOCV は実行可能であり、教育用途では「すべての県が等しく評価される」点で公平性が高い。一方、ランダムフォレストや勾配ブースティング等の重いモデルでは LOOCV は非推奨である。
分類タスクで、特にクラス不均衡があるとき、ランダム分割だけでは偏りが生じる。例えば「都道府県を高齢化率の高い県(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 にも含まれる」状態を確保し、評価の偏りを最小化できる。
時系列データでは絶対にランダム分割してはいけない。SSDSE-B-2026 の人口は 2026 年時点だが、もし「2020-2025 年の人口データから 2026 年の人口を予測する」モデルを作るなら、Train を 2020-2024 年、Test を 2025-2026 年と「時間順に分割」する必要がある。ランダム分割すると、Train に 2025 年が含まれて Test に 2023 年が来る、というような時間逆転が起きる。これは「未来を見て過去を予測する」というカンニング状態であり、テスト精度が不当に高く出る。実運用では「過去のデータから未来を予測する」のだから、Train が古く Test が新しい時間順を厳守する。さらに高度な手法として TimeSeriesSplit(時系列交差検証)がある。これは「Train 期間を徐々に拡張しながら何度も評価する」方法で、Walk-Forward Validation とも呼ばれる。実務の時系列予測(株価、需要予測、災害予測)では必須技術である。
データリーケージとは、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 設計が極めて重要である。
Train と Test の分布が大きく違うと、テスト評価は信頼できない。例えば Train に小県(人口 100 万未満)ばかり、Test に大都市県(人口 500 万以上)ばかり含まれていれば、モデルは大都市県の挙動を学習していないため Test 精度は低くなる。だが「モデルが悪い」のではなく「分布がズレている」のである。これを Distribution Shift(分布シフト)と呼ぶ。SSDSE-B-2026 を分割した後、Train と Test の各変数の平均・分散・最小・最大を比較するのが基本である。ヒストグラムを重ねて目視確認するのも有効だ。もし大きなズレがあれば、Stratified Sampling を再適用するか、データの再分割が必要である。Kolmogorov-Smirnov 検定や Anderson-Darling 検定で統計的に分布の差を検証することもできる。実務では「定期的に新しいデータが入ってきた時、最初に分布が変わっていないか確認する」プロセスが運用上不可欠である。
分割比率に絶対的な正解はないが、データ量によって推奨が変わる。データが少ない(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 件は確保する」が経験則であり、それを満たすデータ量から逆算して分割比率を決める。
テストデータは「最終確認」専用であり、複数回使って結果に応じてモデルを調整してはいけない。複数回使うと、Test データに無意識に最適化してしまい、未知データへの汎化性能が下がる。これを「Test set contamination(テストセット汚染)」と呼ぶ。実務での運用:(1) Train で学習、(2) Val でハイパーパラメータ調整、(3) モデル決定後 Test で 1 回だけ評価、(4) この Test 精度を本番期待値として報告。もし Test 精度が低く「もう一度モデルを改良したい」場合、改良後のモデルを Test で再評価してはいけない。新しい Test データを別に確保する、または別の Val 分割で評価する必要がある。Kaggle 等の機械学習コンペティションが Public Test と Private Test に分けるのも、Test 汚染を防ぐ仕組みである。
業務システムに機械学習モデルを実装する場合、テストデータの設計は「本番環境の予測対象」と一致させる必要がある。例えば「全国の市町村の人口を予測する」モデルなら、Test も全国市町村から抽出する。「東京都内の店舗売上を予測する」モデルなら、Test も東京都内店舗である。本番で予測する対象と Test の対象がズレていれば、Test 精度は意味を持たない。さらに、業務適用では「時間が経つにつれてデータの分布が変わる」のが当然である。コロナ前のデータで学習したモデルがコロナ後で機能しないのは典型例である。だから本番運用では定期的に「最新データでの再評価」が必要であり、これを Model Drift Detection と呼ぶ。一般的には月次・四半期ごとに Test 精度を再測定し、閾値を下回ったら再学習をトリガーする運用が標準である。SSDSE-B-2026 のような国勢調査ベースのデータも、5 年ごとに更新されるので、それに合わせたモデル更新計画が必要である。
テストデータが少なすぎる(例:10 件以下)場合、評価値の信頼区間が広すぎて結論が出せない。救済策は (1) ブートストラップ法による信頼区間推定、(2) ベイズ的アプローチによる事前分布の活用、(3) 専門家による定性評価の併用、などがある。ブートストラップ法は Test データから復元抽出を 1000 回行い、各回で精度を計算して 95% 信頼区間を出す手法である。これにより「Test 精度 0.85、95% CI [0.78, 0.92]」のような表現が可能になり、評価の不確実性を明示できる。SSDSE-B-2026 の 47 件を全部 LOOCV で評価しても、各回の Test は 1 件なので、ブートストラップで信頼区間を出す価値がある。
テストデータの最大の倫理的義務は「モデル学習の意思決定に一切触れさせない」ことである。「触れる」とは Train で fit するだけではなく、ハイパーパラメータ探索で参照する、特徴量選択の良し悪し判定に使う、欠損補完の統計量を計算する、標準化の平均・分散を取る、外れ値検出の閾値を決める—これら全ての「データを見る行為」を指す。SSDSE-B-2026 の 47 都道府県を Train 38 / Test 9 に分割した時、Test 9 件の「総人口」「出生数」「消費支出」を print() しただけで、人間の頭脳に Test の分布が刻まれ、その後のモデル選択が無意識に Test に有利な方向へ偏る—これを「人間によるデータリーケージ」と呼び、再現性危機の原因の一つに挙げられる。
| 操作 | なぜ 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 で oversample | Test 少数派の合成データが Train に | SMOTE は CV の Train fold 内のみ |
このコードでやること:SSDSE-B-2026 の 47 都道府県を Train/Test に分け、Pipeline + cross_val_score で「触れない原則」を守った消費支出予測を行う。
📥 入力データ (SSDSE-B-2026 抜粋):
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 0.069 → Test 0.153 と、 Test のほうが高く出た。 「Test は Train より低くなるはず」という思い込みは正しくない。 この 3 特徴量(総人口・出生数・婚姻件数)では消費支出はほとんど説明できず、 どちらの値も 0 に近いところで揺れているだけである。 Test は 113 件しかないので、 このくらいの差は偶然で簡単に出る。 だからこそ「Test 0.153 を見てから Ridge の alpha を 1.0 → 0.5 に変える」を絶対にしてはいけない。 それを許すと Test は実質的に第二の Validation になり、 未知データ性能の保証を失う。 そして偶然高く出た値を「改善した」と誤認することになる。
| Test を見た回数 | Test 性能の信頼性 | 推奨対応 |
|---|---|---|
| 1 回(最終評価のみ) | 高い—汎化性能をほぼ正しく推定 | そのまま報告 |
| 2–5 回(軽微なチューニング後) | 中程度—Test に過適合の懸念 | 楽観バイアス補正、本番運用前に追加評価 |
| 10 回以上(繰り返し再評価) | 低い—Test が事実上 Validation に | 新しい Test を取得、過去 Test は破棄 |
| Kaggle 公開 Leaderboard 連続提出 | Public LB は Validation 化、Private LB のみ Test | Private LB を「真の Test」と位置付ける |
SSDSE-B-2026 の 47 県を 80:20 で分割すると Test は わずか 9 県。この少なさは「Test 指標の信頼区間が大きく、決定的な結論を出しにくい」ことを意味する。例えば回帰の R² が 0.92 と出ても、Test を別の 9 県に取り直せば 0.78 や 0.96 に振れることは普通。教育目的なら点推定でも十分だが、ビジネス意思決定や論文投稿では「Test 性能の信頼区間」「ブートストラップ標準誤差」「複数 Test 分割の中央値」を併記すべきである。
| Test n | 指標 | 標準誤差の目安 | 解釈 |
|---|---|---|---|
| 9 (SSDSE 80:20) | R² 0.92 | ±0.10 程度 | 「概ね良好」止まり、優劣比較は厳しい |
| 100 | Accuracy 0.85 | ±0.035 (95%CI ±0.07) | 3% 差は誤差範囲、5% 差なら有意候補 |
| 1,000 | Accuracy 0.85 | ±0.011 (95%CI ±0.022) | 1% 差を検出できる本格評価 |
| 10,000+ | Accuracy 0.85 | ±0.0036 | 0.5% 差も区別可、本番 A/B 級 |
このコードでやること:47 県 SSDSE-B-2026 で Test R² の 95% Bootstrap 信頼区間を計算し、「点推定 0.92」の不確実性を可視化する。
📥 入力データ: 上記 Pipeline で fit 済 pipe と X_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 不足)') |
📤 実行すると次の出力が得られる:
💬 点推定 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, F1 | Recall@K, 混同行列 |
| 確率予測 | Brier Score | Calibration Plot, ECE |
| 回帰 | RMSE, MAE | R², 残差プロット |
| 時系列予測 | MAPE, sMAPE | naive baseline 比、Diebold-Mariano 検定 |
| 公平性監査 | Demographic Parity, Equal Odds | サブグループ別 ROC, Calibration |
論文や Kaggle で完結する世界では「Test は固定」が常識だが、実運用では「時間が経つにつれて Test は古くなる」。SSDSE-B-2026 で 2024 年データで Test したモデルを 2026 年の消費支出予測に使うと、コロナ後のインバウンド回復、半導体投資、人口減少加速など「2024 年 Test には含まれなかった構造変化」が予測精度を蝕む。Production MLOps では「定期的に新しい Test セットを再構築 → モデルの汎化性能を再評価 → 必要なら再学習」のサイクル(Retraining Loop)が必須となる。
| 監視項目 | 検出する問題 | 監視頻度 | アラート閾値の例 |
|---|---|---|---|
| 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 倍 |
このコードでやること: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 再構築必須、再学習推奨)') |
📤 実行すると次の出力が得られる:
💬 都道府県人口は 2 年では大きく変わらないため PSI は 0.02 と安定。一方、「インバウンド観光客数」「半導体輸出額」などボラティリティの高い指標では PSI が 0.3 を超えることもあり、その場合は古い Test での精度が信用できなくなる—Test の再構築と再学習が必要。
SSDSE-B-2026 の 47 都道府県を Train 30 / Val 7 / Test 10 に分割し、 「総人口 → 出生数」モデルの最終評価を実演する。
使用データ:SSDSE-B-2026.csv(独立行政法人 統計センター提供、 47 都道府県 × 100 超の社会経済指標)。 出典
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 = 総人口 など)に注意。
47 都道府県 × 12 年(合計 564 行)を 60/20/20 で分割し、 高齢化階層を予測するモデルの訓練 / 検証 / テスト精度を測定します。 テスト精度が訓練精度より大幅に低ければ過学習 です。
🎯 このコードでやること:sklearn の train_test_split を二段階で呼び、 60/20/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}') |
📤 実行例:
💬 結果の読み方:訓練精度 1.000、 検証 0.735、 テスト 0.735。 訓練のみ完全一致なのは Random Forest が深く木を伸ばして訓練データを暗記しているため。 検証とテストがほぼ同じ(差 0.000)→ 過学習はあるが汎化はできている。 検証で「真の本番」を予測する手段としては妥当。
SSDSE-B は 47 県 × 12 年のパネルデータ。 単純な train_test_split だと 「東京 2015 年が train、 東京 2020 年が test」のような同一県の重複 が起こり、 リークが発生します。 GroupKFold で都道府県をグループ単位として分割するのが正解。
🎯 このコードでやること:都道府県をグループとして 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 で overlap=0(県の重複なし)。 通常のランダム分割だと 0.74 出ていたのに GroupKFold では 0.58 へ大きく下落。 これが 「データリークによる精度の水増し」 の正体です。 本番運用では未知の県を予測することになるので、 0.58 の方が現実的な見込み値。
時系列データでは 「未来を train、 過去を test」は厳禁。 必ず時間軸を守って分割します。 sklearn の TimeSeriesSplit は累積的な訓練データで前向き検証 (forward validation) を行います。
🎯 このコードでやること: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} 人') |
📤 実行例:
💬 結果の読み方:train データが累積されるにつれて MAE が 単調減少(24580 → 10250 人)。 これは「過去の情報を多く蓄えるほど予測が安定する」自然な傾向。 時系列分割は 学習可能性の見極め に役立ちます。
テストデータの価値を破壊する 「リーケージ (data leakage)」 の典型パターン:
| 年 | 出来事 | 影響 |
|---|---|---|
| 1931 | Larson によるホールドアウト法 | 概念の起源 |
| 1968 | Mosteller & Tukey による交差検証提案 | CV の標準化 |
| 1974 | Stone「Cross-validatory choice」論文 | 理論的基礎 |
| 1995 | Kohavi「A Study of Cross-Validation」 | K-Fold の k=10 が標準に |
| 2007 | scikit-learn の cross_val_score 公開 | 実装容易化 |
| 2012 | Kaggle で「リーケージ」が問題に | 業界注意喚起 |
| 2018 | Nested CV の重要性が再認識 | ハイパラ調整時の標準 |
| 2020 | 「Test-set overfitting」の警鐘論文 (Recht et al.) | ImageNet 評価見直し |
| 2024 | LLM 評価で「ベンチマーク汚染」議論 | 事前学習データ漏洩 |
🎯 やること:最小コードで 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 固定で再現性確保。
🎯 やること:クラス比率を保持したまま CV。
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 すべてでクラス比率保持。
🎯 やること:内側でハイパラ最適化、 外側で評価。
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 分。 公正性のコスト。
🎯 やること:未来情報のリークを防ぐ。
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 💬 過去→未来の方向のみで評価。
train と test で特徴量分布がずれていないか確認することは、 本番運用での精度低下防止 に必須です。 各特徴量について scipy.stats.ks_2samp で K-S 検定を実施。
🎯 このコードでやること:47 県を train (前 9 年) / test (後 3 年) に分けた場合、 各特徴量の分布が同一かを K-S 検定 (p > 0.05 なら同分布) で確認。
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}') |
📤 実行例:
💬 結果の読み方:A4101(出生数)・A4103(合計特殊出生率)・B4101(年平均気温)・F3101(新規求職申込件数)の 4 変数で分布シフトが検出 (p < 0.05)。 これは 少子化の進行など後期 3 年間で水準が変化 しているため。 こうした特徴量を train で固定した分布で前処理すると test で誤差増。 分布シフト検出 → 再標準化 / 再学習 を検討。
学習曲線 (learning curve) は 「データ量が増えるほど精度がどう変化するか」 を可視化。 これにより「テストサイズはこれで十分か」を見極められます。
🎯 このコードでやること:sklearn の learning_curve で訓練サイズ 10%〜100% における train / val 精度をプロット。
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}') |
📤 実行例:
💬 結果の読み方:訓練サイズ 12→31 件では val 精度が 0.722 → 0.744 とわずかに上下するだけで、 ギャップも 0.278 → 0.256 とほとんど縮まりません。 47 件しかないので、学習曲線がまだ立ち上がってすらいない状態です。 この形からは「あと何件集めれば足りるか」を読み取れません。 学習曲線が 収束していれば「これ以上集めても無駄」、 収束していなければ「集める価値あり」 という判断に直結。
| データ | 役割 | 使い方 | 回数制限 |
|---|---|---|---|
| 訓練データ | パラメータ学習 | fit() の入力 | 無制限 |
| 検証データ | ハイパラ調整・モデル比較 | predict() で何度も評価 | 回数制限なし |
| テストデータ | 最終汎化性能評価 | 最終モデル確定後の 1 度のみ | 1 回限り |
| 本番データ | サービス稼働後の入力 | 推論専用 | 継続監視 |
| 業界 | タスク | 分割戦略 | 注意点 |
|---|---|---|---|
| 金融 | 株価予測 | TimeSeriesSplit | 過去で訓練、 未来で評価 |
| 医療 | 画像診断 | GroupKFold (患者ID) | 同患者画像が train と test に混入禁止 |
| EC | 推薦 | TimeSeriesSplit + UserKFold | 未来イベントリーク注意 |
| 製造 | 異常検知 | StratifiedKFold | 陽性が極少 (0.1% 等) |
| 広告 | CTR 予測 | A/B テスト (ライブ test) | オフライン評価 + オンライン評価 |
| NLP | 機械翻訳 | ホールドアウト | ベンチマーク汚染注意 (LLM) |
「train と test の分布が違うか」を機械学習で検出する手法 = adversarial validation。 train を 0、 test を 1 とラベル付けして二値分類モデルを作り、 AUC が 0.5 に近ければ「同じ分布」と判断します。
🎯 このコードでやること:年代で分けた train と test を分類モデルに当て、 AUC で識別容易性を測る。
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 "分布同一"}') |
📤 実行例:
💬 結果の読み方:AUC 0.779 → 「train と test の年代差を簡単に当てられる」≒ 分布が大きく違う。 この状態でテスト評価をすると、 本番運用時の精度を大きく見誤る 可能性が高い。 対策: domain adaptation、 ドメイン適応学習、 再標準化、 covariate shift correction (重み付け)。
機械学習プロジェクトで テストデータの濫用を防ぐ社内規程 のテンプレート:
data/test/ に隔離し、 アクセスログを取る。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 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}') |
📤 実行例:
🎯 このコードでやること: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} 人') |
📤 実行例:
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}') |
📤 実行例:
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}') |
📤 実行例:
| データ | 役割 | 使い方 | 回数制限 |
|---|---|---|---|
| 訓練データ | パラメータ学習 | fit() の入力 | 無制限 |
| 検証データ | ハイパラ調整・モデル比較 | predict() で何度も評価 | 回数制限なし |
| テストデータ | 最終汎化性能評価 | 最終モデル確定後の 1 度のみ | 1 回限り |
| 本番データ | サービス稼働後の入力 | 推論専用 | 継続監視 |
🎯 このコードでやること:年代で分けた train と test を分類モデルに当て、 AUC で識別容易性を測る。
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 "分布同一"}') |
📤 実行例:
合成 test=200 件、 精度 0.85 の信頼区間を計算する。
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}]") |
💬 手計算 (Step 1) と Python 出力が完全一致。
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}') |
📤 実行例:
🎯 このコードでやること: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} 人') |
📤 実行例:
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}') |
📤 実行例:
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}') |
📤 実行例:
| データ | 役割 | 使い方 | 回数制限 |
|---|---|---|---|
| 訓練データ | パラメータ学習 | fit() の入力 | 無制限 |
| 検証データ | ハイパラ調整・モデル比較 | predict() で何度も評価 | 回数制限なし |
| テストデータ | 最終汎化性能評価 | 最終モデル確定後の 1 度のみ | 1 回限り |
| 本番データ | サービス稼働後の入力 | 推論専用 | 継続監視 |
🎯 このコードでやること:年代で分けた train と test を分類モデルに当て、 AUC で識別容易性を測る。
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 "分布同一"}') |
📤 実行例:
🎯 やること:最小コードで 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 固定で再現性確保。
🎯 やること:クラス比率を保持したまま CV。
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 すべてでクラス比率保持。
🎯 やること:内側でハイパラ最適化、 外側で評価。
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 分。 公正性のコスト。
🎯 やること:未来情報のリークを防ぐ。
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 💬 過去→未来の方向のみで評価。
機械学習プロジェクトで テストデータの濫用を防ぐ社内規程 のテンプレート:
data/test/ に隔離し、 アクセスログを取る。SSDSE-B-2026 のような公的統計データを Python で扱う際の基本パターン:
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基礎 # 関連手法は同カテゴリの他用語を参照してください。 |
具体的なコードは 機械学習の基礎 を参照してください。
分析結果を報告するときに含めるべき情報:
「テストデータ」を扱う代表的なライブラリ別実装。 同じ目的でも書き方が違うため、 自分のプロジェクトの依存関係に合わせて選択する:
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()) |
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}') |
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}') |
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() |
🎯 やること:最小コードで 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 固定で再現性確保。
🎯 やること:クラス比率を保持したまま CV。
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 すべてでクラス比率保持。
🎯 やること:内側でハイパラ最適化、 外側で評価。
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 分。 公正性のコスト。
🎯 やること:未来情報のリークを防ぐ。
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 💬 過去→未来の方向のみで評価。
機械学習プロジェクトで テストデータの濫用を防ぐ社内規程 のテンプレート:
data/test/ に隔離し、 アクセスログを取る。テストデータの価値を破壊する 「リーケージ (data leakage)」 の典型パターン:
| 年 | 出来事 | 影響 |
|---|---|---|
| 1931 | Larson によるホールドアウト法 | 概念の起源 |
| 1968 | Mosteller & Tukey による交差検証提案 | CV の標準化 |
| 1974 | Stone「Cross-validatory choice」論文 | 理論的基礎 |
| 1995 | Kohavi「A Study of Cross-Validation」 | K-Fold の k=10 が標準に |
| 2007 | scikit-learn の cross_val_score 公開 | 実装容易化 |
| 2012 | Kaggle で「リーケージ」が問題に | 業界注意喚起 |
| 2018 | Nested CV の重要性が再認識 | ハイパラ調整時の標準 |
| 2020 | 「Test-set overfitting」の警鐘論文 (Recht et al.) | ImageNet 評価見直し |
| 2024 | LLM 評価で「ベンチマーク汚染」議論 | 事前学習データ漏洩 |
「テストデータ」を実務・試験で扱うときに頻発する典型的なミスです。 各項目を 1 度読んでおけば 9 割の事故が防げます:
テストデータの価値を破壊する 「リーケージ (data leakage)」 の典型パターン:
| 年 | 出来事 | 影響 |
|---|---|---|
| 1931 | Larson によるホールドアウト法 | 概念の起源 |
| 1968 | Mosteller & Tukey による交差検証提案 | CV の標準化 |
| 1974 | Stone「Cross-validatory choice」論文 | 理論的基礎 |
| 1995 | Kohavi「A Study of Cross-Validation」 | K-Fold の k=10 が標準に |
| 2007 | scikit-learn の cross_val_score 公開 | 実装容易化 |
| 2012 | Kaggle で「リーケージ」が問題に | 業界注意喚起 |
| 2018 | Nested CV の重要性が再認識 | ハイパラ調整時の標準 |
| 2020 | 「Test-set overfitting」の警鐘論文 (Recht et al.) | ImageNet 評価見直し |
| 2024 | LLM 評価で「ベンチマーク汚染」議論 | 事前学習データ漏洩 |
「テストデータ」を中心に、 上流の分割手順・並列の検証手法・下流のリーケージ防止と応用先を 4 方向に展開。
概念マップの中心は「テストデータ」で、 前提に 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 通りに分岐する。
train_test_split(test_size=0.2, random_state=42)、 クラス不均衡なら stratify=y誤った分割の代償は大きい: SSDSE-B-2026 で県をまたぐリークがあると、 test 精度が見かけ上 0.74 でも実運用で 0.58 に落ちる (本ページ実値計算参照)。
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}') |
📤 実行例:
🎯 このコードでやること:47 県の人口時系列を年でグループ化し、 過去年で学習 → 翌年予測の評価サイクルを 5 回繰り返す。
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} 人') |
📤 実行例:
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}') |
📤 実行例:
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}') |
📤 実行例:
| データ | 役割 | 使い方 | 回数制限 |
|---|---|---|---|
| 訓練データ | パラメータ学習 | fit() の入力 | 無制限 |
| 検証データ | ハイパラ調整・モデル比較 | predict() で何度も評価 | 回数制限なし |
| テストデータ | 最終汎化性能評価 | 最終モデル確定後の 1 度のみ | 1 回限り |
| 本番データ | サービス稼働後の入力 | 推論専用 | 継続監視 |
🎯 このコードでやること:年代で分けた train と test を分類モデルに当て、 AUC で識別容易性を測る。
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 "分布同一"}') |
📤 実行例:
合成 test=200 件、 精度 0.85 の信頼区間を計算する。
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}]") |
💬 手計算 (Step 1) と Python 出力が完全一致。