漏洩の検出|機械学習の問題設定と評価分割

このページはプロモーション(広告)を含みます。

機械学習の問題設定と評価分割(中級・読む30分+演習40分)

この授業のゴール

仕事でどう使うか

漏洩(leakage)は、評価には使えたが本番の予測時点では手に入らない情報が、モデルや評価に入り込むことです。漏洩があると評価は高く出て、導入後に性能が大きく下がります。この授業は、ここまでの5授業で学んだことを「見つける側」から使う総まとめです。

路線 この授業の内容が使われる成果物・意思決定
DS 予測モデルのレビュー。「良すぎる結果」を受け取ったときに、どの順で何を確かめたかを記録する
ML 学習パイプラインの検査。特徴量ごとの確定時点の表(データ辞書)と、分割の重なりの自動チェックを組み込む
DX 導入判定の会議。ベンダーの報告に、基準・分割単位・テストの使用回数が書かれているかを確認する
RS 論文の査読と再現。前処理や特徴選択を交差検証の内側で行ったかを確かめる

前提

次の言葉を使います。

新しく AUC(Area Under the ROC Curve)を使います。定義は「正確な定義」の節で、小さな数値例とともに説明します。

身近な状況:「AUC 1.00、正解率100%」の解約モデル

あるチームが、解約予測モデルの検証結果を報告しました。「AUC 1.00、正解率100%。すぐに導入できます」。

使った列を確認すると、契約期間・月額料金・問い合わせ回数・利用時間に加えて、解約理由コード(cancel_reason_code)が入っていました。解約理由は、解約の手続きのときに顧客が選ぶものです。つまり、解約した人にだけ値が入り、解約していない人は空欄です。

顧客 解約理由コード 解約した
A (空欄) 0
B C02 1
C (空欄) 0
D C01 1

月初に「今月解約しそうな人」を予測する時点では、まだ誰も解約手続きをしていないので、この列は全員空欄です。モデルは「理由が入っていれば解約」という答えのカンニングを覚えただけで、本番では何も当てられません。

漏洩は、この例のようにわかりやすいものばかりではありません。この授業では、漏洩の型と、見つけるための具体的な検査を学びます。

直感的な説明:本番の日に、その情報は手元にあるか

漏洩を見つける基本の質問は1つです。「本番で予測する日に、その情報は手元にあるか」。答えが「いいえ」なら、モデルにも評価にも入れてはいけません。漏洩は入り込む場所によって4つの型に分けられます。

型 何が入り込むか 例
① 未来特徴(ターゲット漏洩) 予測時点より後に決まる情報、目的変数の言い換え 解約理由、返金額、解約後の最終ログインからの日数
② 同一個体・重複 評価の答えとほぼ同じ行が訓練にある 同じ人の別の月、同じ店の別の日、コピーされた行
③ 前処理・選択 評価データの値が前処理や特徴選択に使われる 全データの平均で補完、全データで列を選ぶ、地域別解約率を全データで計算
④ テストの反復利用 テストの結果を見て選び直す テストを10回見て一番良い設定を報告

もう1つ、基準なしの報告も危険です。漏洩そのものではありませんが、比べる基準がないと「良すぎる」ことに気づけません。①〜④と基準なしは、モデルのレビューで毎回確認します。

正確な定義:AUCと5つの検査

AUCは、陽性(解約した人)を1人、陰性(解約しなかった人)を1人無作為に選んだとき、陽性のほうに高い点数が付いている確率です。同点は0.5と数えます。0.5ならでたらめ、1.0なら完全に分けられています。

数値で確かめます。ある列の値が、陽性2人で $\{8, 5\}$、陰性3人で $\{6, 2, 1\}$ だったとします。陽性と陰性の組は $2\times3=6$ 組です。8は6・2・1のすべてより大きいので3組、5は2・1より大きいので2組、合計5組で陽性が上です。

$$\text{AUC} = \frac{\text{陽性が上の組の数}}{\text{陽性の人数}\times\text{陰性の人数}} = \frac{5}{2\times3} \approx 0.833$$

列の値が小さいほど陽性になりやすい列(契約期間など)ではAUCが0.5未満になるので、単独AUCとして $\max(\text{AUC},\ 1-\text{AUC})$ を使います。sklearn.metrics.roc_auc_score(y_true, y_score) は、正解の0/1と点数(列の値でも予測確率でもよい)を受け取り、AUCを浮動小数で返します。

漏洩の検査は次の5つです。どれも「見つかったら必ず漏洩」ではなく、確認のきっかけです。

  1. 基準との差:基準(ベースライン)より極端に良い、またはAUCが0.95を超える → まず漏洩を疑う。
  2. 1列ずつの単独AUC:1列だけで0.9以上の列 → その列がいつ・どう作られるかを調べる。
  3. 列の確定時点:データ辞書に「その値がいつ確定するか(予測時点の前か後か)」を書き、後の列を除く。
  4. 集合間の重なり:訓練と評価で、ID・群・完全に同じ行が重なっていないか(時点・個体・群の分離)。
  5. 実験記録:テストを何回評価したか、前処理・特徴選択をどのデータで fit したか。

段階付き例題:解約データで漏洩を見つけて除く

churn.csv の訓練(1800人)で学習し、検証(600人)で評価します。解約理由コードは「値があるか」の0/1列 has_reason にしてモデルに入れます。

import pandas as pd
from sklearn.metrics import roc_auc_score
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression

df = pd.read_csv('/data/churn.csv')
df['has_reason'] = df['cancel_reason_code'].notna().astype(int)   # 解約理由が入っているか
tr, va = df[df['split'] == 'train'], df[df['split'] == 'valid']
base = ['tenure_months', 'monthly_fee', 'support_calls', 'usage_hours']

def evaluate(cols):
    m = make_pipeline(StandardScaler(), LogisticRegression(max_iter=1000)).fit(tr[cols], tr['churned'])
    p = m.predict_proba(va[cols])[:, 1]
    return float(round(roc_auc_score(va['churned'], p), 3)), float(round(((p >= 0.5) == va['churned']).mean(), 3))

print('① 理由あり  AUC, 正解率:', evaluate(base + ['has_reason']))
print('   基準(全員継続)正解率:', round(1 - va['churned'].mean(), 3))
print('② 1列ずつのAUC(訓練データ)')
for c in base + ['has_reason']:
    a = roc_auc_score(tr['churned'], tr[c])
    print(f'   {c:14s} {max(a, 1 - a):.3f}')
print('③ 理由を除く AUC, 正解率:', evaluate(base))

出力:

① 理由あり  AUC, 正解率: (1.0, 1.0)
   基準(全員継続)正解率: 0.79
② 1列ずつのAUC(訓練データ)
   tenure_months  0.698
   monthly_fee    0.534
   support_calls  0.636
   usage_hours    0.558
   has_reason     1.000
③ 理由を除く AUC, 正解率: (0.745, 0.8)

ステップ1(検査1):AUC 1.0、正解率1.0は、基準の0.79と比べて極端です。ここで「すごい」ではなく「おかしい」と考えます。

ステップ2(検査2):1列ずつ見ると、has_reason だけで単独AUCが1.000です。ほかの列は0.53〜0.70で、1列で完全に分けられる列は不自然です。

ステップ3(検査3):解約理由は解約手続きのときに記録されるので、確定時点は予測時点の後です。除外します。

ステップ4:除いたモデルは AUC 0.745、正解率0.80です。正解率は基準0.79とほぼ同じなので、正解率では価値を示せません。AUC 0.745 は「解約しやすい順に並べる力」がある程度あることを示すので、上位の人に連絡する使い方なら、ベースライン の期待利益で基準と比べて判断します。これが本番で期待できる正直な出発点です。

操作して理解する:でたらめなデータでも「当たる」前処理の漏洩

100人×1000列の特徴量も正解も、すべて乱数で作ります。本当は何も当てられないはずなので、正しい評価なら正解率は50%前後になるはずです。SelectKBest(f_classif, k=20) は、目的変数との関係が強い順に20列を選ぶ前処理です。cross_val_score は5分割の交差検証の正解率を返します。

import numpy as np
from sklearn.feature_selection import SelectKBest, f_classif
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
from sklearn.pipeline import make_pipeline

rng = np.random.default_rng(3)
X = rng.normal(size=(100, 1000))       # 100人 × 1000列の「でたらめな」特徴量
y = rng.integers(0, 2, 100)            # でたらめな正解(当てられるはずがない)

# 誤り:全データで「yと関係が強そうな20列」を選んでから交差検証
X_sel = SelectKBest(f_classif, k=20).fit_transform(X, y)
bad = cross_val_score(LogisticRegression(max_iter=1000), X_sel, y, cv=5).mean()
# 正しい:列の選択も Pipeline に入れ、各foldの訓練部分だけで選ぶ
pipe = make_pipeline(SelectKBest(f_classif, k=20), LogisticRegression(max_iter=1000))
good = cross_val_score(pipe, X, y, cv=5).mean()
print('全データで選んでから評価:', round(bad, 3))
print('訓練内で選んで評価      :', round(good, 3))

出力:

全データで選んでから評価: 0.8
訓練内で選んで評価      : 0.45

全データで列を選ぶと、評価側の行の正解も使って「たまたま y と合っている列」を拾います。そのため、でたらめなデータでも正解率80%に見えます。Pipeline に入れて各foldの訓練部分だけで選ぶと45%で、偶然の範囲(100人の正解率の標準誤差は約5ポイント)に収まります。

次のように変えて試してください。

  1. k=20 を k=5 にすると? → 選ぶ列が少なくても、全データで選べば50%を大きく超えやすいままです。
  2. size=(100, 1000) を (100, 50) にすると? → 候補が少ないと「たまたま合う列」が見つかりにくくなり、差は小さくなります。列が多いデータ(遺伝子、テキストの単語、ログの集計)ほど、この漏洩は深刻です。

発展:見つけにくい漏洩と、検査の限界

よくある失敗

失敗1:重要度1位の列を「大発見」として報告する

例題のモデルで、係数の大きさや特徴量の重要度を見ると has_reason が圧倒的な1位になります。これを「解約理由の有無が最も重要な要因です」と報告するのは誤りです。

見方 誤った解釈 正しい解釈
単独AUC 1.000 最強の予測因子 目的変数の言い換え。確定時点を確認する
正解率 1.0 完璧なモデル 基準0.79から極端に離れているので漏洩を疑う

直し方:1列で単独AUCが0.9以上、または重要度が他を大きく引き離す列は、必ず確定時点をデータ辞書で確認します。業務的に当たり前すぎる(解約した人には解約理由がある)列ほど、漏洩の可能性が高いと考えます。

失敗2:目的変数を使った集計を全データで作ってから分ける

「地域別の解約率」を特徴量にするとき、分割の前に全データで計算すると、評価の行の正解が特徴量に混ざります。

import pandas as pd
d = pd.DataFrame({'region': ['東', '東', '東', '西', '西', '西'],
                  'churned': [0, 0, 1, 0, 0, 0],
                  'set': ['train', 'train', 'test', 'train', 'train', 'test']})
print('全データの地域別解約率:', d.groupby('region')['churned'].mean().round(3).to_dict())
print('訓練だけの地域別解約率:', d[d['set'] == 'train'].groupby('region')['churned'].mean().round(3).to_dict())

出力:

全データの地域別解約率: {'東': 0.333, '西': 0.0}
訓練だけの地域別解約率: {'東': 0.0, '西': 0.0}

訓練の「東」の人は誰も解約していないのに、テストの1人の解約が混ざって「東は解約率0.333」という特徴量になります。モデルは評価の答えの一部を見ていることになります。直し方:目的変数を使う集計(ターゲットエンコーディング)は、訓練データだけで計算し、評価データにはその値を当てはめます。交差検証では各foldの訓練部分だけで計算します。補完・標準化・特徴選択も同じです。

失敗3:テストを何度も使い、基準なしで最良値を報告する

ある実験記録です。

回 変更 テストAUC
1 最初のモデル 0.71
2 特徴量を追加 0.73
… (設定を変えてテストを見る) …
8 木の深さを変更 0.76

報告書には「テストAUC 0.76」とだけ書かれ、基準の値も、テストを8回見たことも書かれていません。8回の中から最良を選んでいるので0.76は楽観的です(訓練・検証・テスト)。基準がないので、0.76に価値があるかもわかりません。

直し方:選択は検証または訓練内の交差検証で行い、テストは最後に1回だけ使う。報告には、①基準の値、②分割の単位と区切り、③テストの使用回数、④除いた列と理由、を必ず書きます。

まとめ

用語

漏洩(leakage)

本番の予測時点では手に入らない情報(未来の情報、評価データの答えなど)が、モデルや評価に入り込むこと。

ターゲット漏洩(target leakage)

目的変数が決まった後に確定する情報や、目的変数の言い換えを特徴量に使ってしまうこと。

AUC(Area Under the ROC Curve)

陽性と陰性を1人ずつ選んだとき、陽性のほうに高い点数が付いている確率。0.5はでたらめ、1.0は完全に分けられる。

データ辞書(data dictionary)

各列の意味・単位・作られ方・確定時点などをまとめた表。漏洩の判定に使う。

ターゲットエンコーディング(target encoding)

カテゴリごとの目的変数の平均(地域別解約率など)を特徴量にする方法。訓練データだけで計算する必要がある。

よくある質問

なぜ漏洩はこんなに起きやすいのですか?

分析用のデータは、ふつう「今日」抽出されるからです。今日の表には、予測時点の後に起きた出来事(解約理由、返金、最新のログイン)が全部入っています。予測時点のデータを作り直す手間を省くと、自然に未来の情報が混ざります。

過学習と漏洩は何が違うのですか?

過学習は、モデルが訓練データの偶然まで覚え、評価で性能が下がる現象で、評価が正しければ検出できます。漏洩は、評価そのものが甘くなり、評価でも性能が高く見える現象です。過学習は「検証で下がる」、漏洩は「検証では下がらず本番で下がる」点が違います。

漏洩を見つけるのに一番失敗しやすいのはどこですか?

業務的に意味が通る列です。「返金額が多い人は解約しやすい」は筋が通って見えるので、疑われにくいのです。列の意味ではなく、「その値はいつ確定するか」を基準に判断します。データ辞書に確定時点の欄を作るのが確実です。

予測時点を変えると、漏洩かどうかも変わりますか?

変わります。「解約手続き後に、返金額を予測する」なら解約理由は使える情報です。同じ列でも、予測時点の前に確定していれば使え、後なら漏洩です。問題の定義(目的変数と利用時点)が変われば、データ辞書の判定もやり直します。

仕事で漏洩が見つかったら、どう報告すればよいですか?

①どの列(または手順)が、②なぜ予測時点で使えないか、③除いた後の成績と基準の成績、を並べて報告します。成績が下がることは失敗ではなく、本番で期待できる値がわかった成果です。以前の報告の数値を使った判断があれば、その訂正も伝えます。

テストで漏洩が見つかった後、同じテストで評価し直してもよいですか?

漏洩した列を除くだけなら、同じテストで評価し直すのは自然です。ただし、テストの結果を見て他の設定も変えたなら、テストを選択に使ったことになります。何を変えたか、テストを何回使ったかを記録し、可能なら新しい期間のデータで最終確認します。

確認問題

問1 月初に今月の解約を予測するモデルで、検証の正解率が100%だった。最初にとるべき行動はどれか。

  1. ① すぐに本番に導入する
  2. ② テストでも評価し、100%なら導入する
  3. ③ 基準と比べ、1列ずつの予測力と各列の確定時点を調べて漏洩を疑う
  4. ④ より複雑なモデルに変えて、さらに確かめる
解答と解説

正解:③

良すぎる結果は、まず漏洩を疑うきっかけです。基準との差、単独で強すぎる列、その列が予測時点に手に入るかを確かめます。

問2 ある列の値が、陽性2人で {9, 4}、陰性2人で {6, 3} だった。この列のAUCはいくつか。

  1. ① 0.25
  2. ② 0.5
  3. ③ 0.75
  4. ④ 1.0
解答と解説

正解:③

組は2×2=4組。9は6・3より大きく2組、4は3より大きく1組、合計3組で、AUC=3/4=0.75です。

問3 全データで「目的変数と関係の強い20列」を選んでから交差検証した。でたらめなデータでも正解率が80%になった理由はどれか。

  1. ① 交差検証の分割数が少なかったから
  2. ② 列の選択に評価側の行の正解も使い、偶然 y と合う列を拾ったから
  3. ③ ロジスティック回帰が過学習しにくいモデルだから
  4. ④ でたらめなデータでも80%当たるのが普通だから
解答と解説

正解:②

評価側の正解を使った前処理は漏洩です。列の選択も Pipeline に入れ、各foldの訓練部分だけで行います。

問4 次の列のうち、「月初に今月の解約を予測する」モデルの特徴量として使ってよいものはどれか。

  1. ① 今月の返金額
  2. ② 解約理由コード
  3. ③ 前月末までの3か月の問い合わせ回数
  4. ④ データ抽出日時点での最終ログインからの日数
解答と解説

正解:③

予測時点(月初)より前に確定し、その時点で手元にある情報だけが使えます。

問5 単独AUCが0.55の列について正しいものはどれか。

  1. ① 漏洩はないと断定できる
  2. ② 予測に役立たないので必ず削除する
  3. ③ 1列では弱いが、他の列との組み合わせで漏洩していないとは言い切れず、確定時点の確認は必要
  4. ④ AUCが0.5より大きいので漏洩している
解答と解説

正解:③

単独AUCは確認のきっかけで、低いことは漏洩がない証明になりません。組み合わせの漏洩もあり得ます。

問6 ベンダーの報告書に「テストAUC 0.82」とだけ書かれていた。導入判断の前に確認すべき組み合わせとして最も適切なものはどれか。

  1. ① 使用したモデルの名前と学習時間
  2. ② 基準の成績、分割の単位と区切り、テストの使用回数、除外した列と理由
  3. ③ AUC以外の指標をできるだけ多く
  4. ④ 学習データの件数だけ
解答と解説

正解:②

基準なし・同一個体・テスト反復・未来特徴の4点が確認できないと、0.82の意味は判断できません。

次に学ぶこと

これで 機械学習の問題設定と評価分割 の6授業は終わりです。モジュールの総合テストと自力課題(解約予測で未来特徴を除き、利用者単位で分割する)に進んでください。この授業の確認問題に正解しても、習得の判定はモジュールの課題で行います。

前処理を訓練の中で行う書き方は 「Pipeline」(有料版)、交差検証は 「指標と交差検証」(有料版)、予測時点の定義は 目的変数と利用時点、分け方は 時点・個体・群の分離 へ戻ってください。

キャリア学習:データサイエンティスト・機械学習エンジニア・DX推進・研究者を目指す人の学習アプリ。164モジュール・984授業を、ブラウザで動くSQLとPythonの演習で。基礎と各分野の最初のモジュールは無料。全部は2,980円の買い切り。

アプリを無料で開く有料版と無料版の違い