Day 055 — ROC曲線と AUC

2026-06-05 緑 / Phase 2 理論 閾値・感度・特異度・AUC

📚 背景知識(読んでから問題へ)

🎯
Day 055 — Day 054 で学んだ F1 スコアは「閾値 0.5 固定」の評価でした。ROC 曲線はすべての閾値での性能を一枚のグラフに可視化します。AUC はモデルの本質的な識別能力を表します。

2つの指標:感度(TPR)と 1-特異度(FPR)

指標別名計算式身近な例え
感度(Sensitivity) TPR / Recall TP / (TP + FN) 川の魚のうち何割を釣ったか(見逃しゼロを目指す)
特異度(Specificity) TNR TN / (TN + FP) 石・ゴミを何割「魚じゃない」と正しく判定できたか
1 - 特異度 FPR(偽陽性率) FP / (FP + TN) 石・ゴミを誤って「魚」と判定した割合(小さいほど良い)

AUC の品質目安

AUC 値品質Kaggleでの目安
1.00完璧(過学習疑い)リークを疑う
0.90〜1.00非常に優秀上位入賞圏
0.80〜0.90良好銅メダル圏
0.70〜0.80許容範囲ベースライン以上
0.50〜0.70弱い特徴量改善が必要
0.50ランダムと同じモデルに意味なし
< 0.50ランダムより悪いラベル反転を確認

📈 ROC 曲線のイメージ

縦軸: TPR(感度)/ 横軸: FPR(1-特異度)/ 閾値を 1.0 → 0.0 と動かしながらプロット

FPR(1 - 特異度) TPR(感度) 0.0 0.25 0.50 0.75 1.00 0.0 0.25 0.50 0.75 1.00 ランダム予測 完璧なモデル 最適閾値 (左上隅に最近) AUC ≈ 0.87 良好なモデル ランダム (AUC=0.5)

📊 AUC 品質ゲージ

AUC 値とモデル品質の対応

完璧なモデル
AUC = 1.00
1.00
非常に優秀
AUC ≥ 0.90
≥ 0.90
良好
AUC ≥ 0.80
≥ 0.80
許容範囲
AUC ≥ 0.70
≥ 0.70
弱い
AUC ≥ 0.60
≥ 0.60
ランダムと同じ
AUC = 0.50
0.50
💡
AUC の確率的解釈:ランダムに陽性サンプルと陰性サンプルを1つずつ選んだとき、「陽性サンプルのモデルスコアが高い確率」= AUC。AUC=0.87 なら 87% の確率で陽性を正しく上位ランクに置く。

🗂️ データスキーマ(make_classification)

パラメータ説明
n_samples1000サンプル数
n_features20特徴量数(全体)
n_informative10予測に有効な特徴量数
n_redundant5有効特徴量の線形結合(ノイズ)
weights[0.8, 0.2]クラス0: 80% / クラス1: 20%(不均衡
random_state42再現性のための乱数シード

クラス分布(不均衡データのイメージ)

クラス0(陰性): 800件 / 80% クラス1(陽性): 200件 / 20%

📝 問題

import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.metrics import (
    roc_curve, roc_auc_score,
    confusion_matrix, classification_report
)

問1 — ROC 曲線の概念理解(空欄を埋めよ)

  1. ROC 曲線の横軸は ___(FPR = 1 - 特異度)である
  2. ROC 曲線の縦軸は ___(TPR = 感度 = Recall)である
  3. AUC が ___ のとき、モデルの性能はランダム予測と同じである
  4. AUC が高いほど、モデルは不均衡データに対して ___(強い/弱い)
  5. 閾値を下げると TPR は ___(上がる/下がる)

問2 — データ生成とモデル学習

X, y = make_classification(
    n_samples=1000,
    n_features=20,
    n_informative=10,
    n_redundant=5,
    weights=[0.8, 0.2],  # 不均衡データ
    random_state=42
)
  1. train_test_split(80:20, stratify=y, random_state=42)で分割せよ
  2. StandardScaler + LogisticRegression(random_state=42) の Pipeline を作成・学習せよ
  3. y_predy_prob(陽性クラスの確率: predict_proba の第2列)を取得せよ

問3 — ROC 曲線と AUC の計算

  1. roc_curve(y_test, y_prob)fpr, tpr, thresholds を取得せよ
  2. roc_auc_score(y_test, y_prob) で AUC を計算せよ
  3. AUC が何を意味するかを自分の言葉で説明せよ

問4 — 最適閾値の探索

ROC 曲線上で「左上隅に最も近い点」が最適閾値の候補です。

  1. 各閾値での距離 distance = sqrt(fpr² + (1-tpr)²) を計算せよ
  2. 距離が最小の閾値(optimal_threshold)を見つけよ
  3. その閾値での TPR と FPR を確認せよ

問5 — デフォルト閾値 vs 最適閾値の比較

  1. デフォルト閾値 0.5 での Precision・Recall・F1(macro)を計算せよ
  2. 最適閾値での Precision・Recall・F1(macro)を計算せよ
  3. 不均衡データに対してどちらが優れているか?理由も述べよ

🔍 ヒント(段階的開示)

ヒント1 — 方向性
  • 問1: ROC曲線の軸は「本物の陽性検出率」vs「偽の陽性検出率」
  • 問3: predict_proba(X_test)[:, 1] で陽性クラスの確率を取得
  • 問4: np.argmin(distances) で最小距離のインデックスを取得
  • 問5: (y_prob >= optimal_threshold).astype(int) で最適閾値でのラベルを取得
ヒント2 — アプローチ
# ROC曲線の取得
fpr, tpr, thresholds = roc_curve(y_test, y_prob)
auc = roc_auc_score(y_test, y_prob)

# AUCの意味: ランダムに陽性1件・陰性1件を選んだとき、
# 陽性のスコアが高い確率

# 最適閾値の探索
distances = np.sqrt(fpr**2 + (1 - tpr)**2)
opt_idx = np.argmin(distances)
optimal_threshold = thresholds[opt_idx]
ヒント3 — コード骨格(ほぼ答え)
# 最適閾値でのラベル生成
y_pred_opt = (y_prob >= optimal_threshold).astype(int)

# 比較
from sklearn.metrics import precision_score, recall_score, f1_score

for name, y_p in [("閾値0.5", y_pred), ("最適閾値", y_pred_opt)]:
    print(f"\n--- {name} ---")
    print(f"Precision (macro): {precision_score(y_test, y_p, average='macro', zero_division=0):.4f}")
    print(f"Recall    (macro): {recall_score(y_test, y_p, average='macro', zero_division=0):.4f}")
    print(f"F1        (macro): {f1_score(y_test, y_p, average='macro', zero_division=0):.4f}")

模範解答

import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.metrics import (
    roc_curve, roc_auc_score,
    precision_score, recall_score, f1_score,
    confusion_matrix, classification_report
)

# ── 問1: 概念理解 ──
print("=== 問1 ===")
print("1. 横軸: FPR(False Positive Rate = 1 - 特異度)")
print("2. 縦軸: TPR(True Positive Rate = 感度 = Recall)")
print("3. AUC = 0.5 のとき、ランダム予測と同じ")
print("4. AUC が高いほど不均衡データに強い")
print("5. 閾値を下げると TPR は上がる(見逃しが減る)")

# ── 問2: データ生成とモデル学習 ──
X, y = make_classification(
    n_samples=1000, n_features=20,
    n_informative=10, n_redundant=5,
    weights=[0.8, 0.2], random_state=42
)

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, stratify=y, random_state=42
)

pipeline = Pipeline([
    ("scaler", StandardScaler()),
    ("model", LogisticRegression(random_state=42, max_iter=1000))
])
pipeline.fit(X_train, y_train)

y_pred = pipeline.predict(X_test)
y_prob = pipeline.predict_proba(X_test)[:, 1]

print(f"\n[問2] クラス分布: y=0: {(y_test==0).sum()}件, y=1: {(y_test==1).sum()}件")

# ── 問3: ROC曲線とAUC ──
fpr, tpr, thresholds = roc_curve(y_test, y_prob)
auc = roc_auc_score(y_test, y_prob)

print(f"\n[問3] AUC = {auc:.4f}")
print("AUCの意味: ランダムに陽性サンプルと陰性サンプルを1つずつ選んだとき、")
print("          陽性サンプルのスコアが高い確率。")

# ── 問4: 最適閾値の探索 ──
distances = np.sqrt(fpr**2 + (1 - tpr)**2)
opt_idx = np.argmin(distances)
optimal_threshold = thresholds[opt_idx]
opt_tpr = tpr[opt_idx]
opt_fpr = fpr[opt_idx]

print(f"\n[問4] 最適閾値: {optimal_threshold:.4f}")
print(f"  TPR(感度)     = {opt_tpr:.4f}")
print(f"  FPR(1-特異度) = {opt_fpr:.4f}")

# ── 問5: 閾値比較 ──
y_pred_opt = (y_prob >= optimal_threshold).astype(int)

print("\n[問5] 閾値比較:")
for name, y_p in [("閾値0.5(デフォルト)", y_pred), ("最適閾値", y_pred_opt)]:
    p = precision_score(y_test, y_p, average="macro", zero_division=0)
    r = recall_score(y_test, y_p, average="macro", zero_division=0)
    f = f1_score(y_test, y_p, average="macro", zero_division=0)
    print(f"\n--- {name} ---")
    print(f"  Precision (macro): {p:.4f}")
    print(f"  Recall    (macro): {r:.4f}")
    print(f"  F1        (macro): {f:.4f}")

# 結論: 最適閾値は少数クラス(陽性)の Recall を改善し、
# 不均衡データでは Macro F1 が向上する。
# AUC を評価指標とするコンペでは閾値は不要(確率を直接評価)。

⚖️ 閾値と TPR・FPR のトレードオフ

閾値を下げる(例: 0.5 → 0.3)
✅ 陽性判定が増える
✅ TPR(感度)が上がる — 見逃しが減る
❌ FPR も上がる — 誤検知が増える
使いどころ: 癌検診(見逃しのコストが高い)
閾値を上げる(例: 0.5 → 0.7)
✅ FPR が下がる — 誤検知が減る
✅ Precision が上がる — 予測が確実
❌ TPR(感度)が下がる — 見逃しが増える
使いどころ: スパムフィルター(誤検知のコストが高い)

最適閾値の探索方法: 左上隅 (FPR=0, TPR=1) に最も近い点

理想点 (FPR=0, TPR=1) 最適閾値 距離が最小の点 d=√(fpr²+(1-tpr)²) 閾値 0.5(デフォルト)

🪜 Step-by-Step 解説

1 predict_proba で確率を取得する

# predict_proba は2列の行列を返す
# 列0: クラス0(陰性)の確率
# 列1: クラス1(陽性)の確率

y_prob_matrix = pipeline.predict_proba(X_test)
print(y_prob_matrix.shape)      # (200, 2)
print(y_prob_matrix[:3])
# 例: [[0.92, 0.08], [0.15, 0.85], ...]

y_prob = y_prob_matrix[:, 1]   # 陽性の確率だけ取り出す

2 roc_curve の戻り値を理解する

fpr, tpr, thresholds = roc_curve(y_test, y_prob)

# fpr:        各閾値での FPR(False Positive Rate)の配列
# tpr:        各閾値での TPR(True Positive Rate)の配列
# thresholds: 閾値の配列(降順)

print(f"点の数: {len(fpr)}")   # 閾値の数 + 1
print(f"閾値の範囲: {thresholds.max():.3f} 〜 {thresholds.min():.3f}")

3 AUC の計算方法(2種類)

from sklearn.metrics import roc_auc_score, auc

# 方法1: roc_auc_score(推奨・シンプル)
auc1 = roc_auc_score(y_test, y_prob)

# 方法2: roc_curve + auc(同じ結果)
fpr, tpr, _ = roc_curve(y_test, y_prob)
auc2 = auc(fpr, tpr)

print(f"AUC (方法1): {auc1:.4f}")
print(f"AUC (方法2): {auc2:.4f}")  # 同じ値

4 最適閾値の探索(Youden's Index との比較)

# 方法1: 左上隅からのユークリッド距離を最小化
distances = np.sqrt(fpr**2 + (1 - tpr)**2)
opt_idx_eu = np.argmin(distances)

# 方法2: Youden's Index を最大化(TPR - FPR)
youden_idx = tpr - fpr
opt_idx_yj = np.argmax(youden_idx)

print(f"最適閾値(距離法):   {thresholds[opt_idx_eu]:.4f}")
print(f"最適閾値(Youden):   {thresholds[opt_idx_yj]:.4f}")
# → どちらも似た結果になることが多い

5 CV で AUC を最大化する

from sklearn.model_selection import cross_val_score, StratifiedKFold

cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
auc_scores = cross_val_score(
    pipeline, X, y,
    cv=cv,
    scoring="roc_auc"   # ← AUC で評価
)
print(f"CV AUC: {auc_scores.mean():.4f} ± {auc_scores.std():.4f}")

🔢 数学・統計の補足(文系向け)

TPR と FPR はなぜトレードオフになるのか?

病院での癌検診の例:
閾値を低くする(「少し怪しければ全員陽性と判定」)
→ 本物の患者をほぼ全員発見できる(TPR ↑)
→ でも健康な人も「陽性」と判定してしまう(FPR ↑)

閾値を高くする(「確実に癌の人だけ陽性と判定」)
→ 健康な人は「陰性」と正しく判定できる(FPR ↓)
→ でも軽症の患者を見逃してしまう(TPR ↓)

ROC vs Accuracy の違い

Accuracy(問題あり)
不均衡データ (9:1) で
「全部陰性と予測」しただけで
Accuracy = 90%
でも陽性を1件も検出できていない
AUC(ロバスト)
「全部陰性と予測」するモデルは
確率が全員同じ → 順序に意味なし
AUC = 0.5(ランダムと同じ)
不均衡データの問題が見える

🏆 Kaggleでの実践的な使い方

コンペタイプ評価指標閾値の必要性
医療(癌診断)AUC-ROC提出は確率でOK(閾値不要)
不正検知(金融)AUC-ROC / Macro F1F1提出時は最適閾値を使う
信用スコアリングAUC-ROC確率を直接提出
多クラス分類Macro F1 / Log LossF1は閾値調整、Log Lossは確率
バランスデータAccuracy / Macro F10.5でも良いことが多い
# Kaggle提出の典型パターン

# パターン1: AUCが評価指標 → 確率をそのまま提出
submission = pd.DataFrame({
    "id": test_ids,
    "target": y_prob  # 確率(0〜1)をそのまま
})

# パターン2: Macro F1 が評価指標 → CV で最適閾値を探索
from sklearn.metrics import f1_score

thrs = np.arange(0.1, 0.9, 0.01)
best_thr = max(thrs, key=lambda t: f1_score(
    y_val, (y_prob_val >= t).astype(int),
    average="macro", zero_division=0
))
print(f"最適閾値 (CV): {best_thr:.2f}")

# パターン3: class_weight="balanced" で不均衡対応
from sklearn.linear_model import LogisticRegression
model = LogisticRegression(class_weight="balanced", max_iter=1000)

⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
AUC が高ければ閾値 0.5 でいいと思う AUC と閾値の関係を知らない AUC は確率の順序だけを評価。Precision/Recall は閾値に依存
predict_proba の第1列が陽性確率と思う インデックスを間違える [:, 1] が陽性(クラス1)の確率。[:, 0] は陰性
AUC < 0.5 のモデルを捨てる 悪いモデルと思う ラベルを反転すれば AUC > 0.5 になる(ラベル設定ミスの可能性)
不均衡データに Accuracy を使う デフォルトで使ってしまう AUC-ROC の方が不均衡に対してロバスト
ROC 曲線と PR 曲線を混同する 両方「曲線」と呼ぶ PR曲線(Precision-Recall)は極端な不均衡(1:1000など)でより有用

🚀 次のステップ

  • 発展: PR 曲線(Precision-Recall Curve)と AP スコア — 極端な不均衡データでの評価
  • 次回予告 (Day 056): Confusion Matrix の可視化と詳細分析(heatmap・FP/FNのビジネス解釈)

📋 自己評価(解いた後に記入)

理解度:
自分の回答:
気づき・メモ: