Day 057 — PR曲線と Average Precision — 不均衡データの評価

2026-06-07 緑 / Phase 2 分析 PR曲線・Average Precision・不均衡データ

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

🎯
Day 057 — Day 056 で学んだ Confusion Matrix は「特定の閾値での分解」でした。PR 曲線はすべての閾値での Precision-Recall トレードオフを可視化します。特に不均衡データ(詐欺検出・医療診断)で ROC-AUC より正直な評価ができます。

なぜ ROC では不十分なのか

不均衡データ(例: 詐欺 5% / 正常 95%)では:

予測戦略AccuracyROC-AUCAP(PR-AUC)詐欺を捕まえた数
全部「正常」と予測 95% ≈ 0.5 ≈ 0.05(陽性割合) 0件
ランダム予測 ≈ 90% ≈ 0.5 ≈ 0.05 5% 程度
良いモデル ≈ 97% ≈ 0.92 ≈ 0.45 多数
⚠️
ROC-AUC=0.5(ランダム)と ROC-AUC=0.92(良いモデル)の差は 0.42
AP では 0.05 と 0.45 の差 = 0.40 とほぼ同じ情報量。でも ROC の 0.5 は「50%の確率」に聞こえてしまい、モデルが全く役立たないことが分かりにくい。

PR 曲線の2軸の意味

指標計算式直感的な意味
X 軸 Recall(感度) TP / (TP + FN) 実際の詐欺のうち何%を捕まえたか
Y 軸 Precision(適合率) TP / (TP + FP) 「詐欺」と予測したうち何%が本当に詐欺か
面積 AP(Average Precision) PR曲線下の面積 ランダム予測の AP = 陽性割合(例: 0.05)

⚖️ ROC-AUC vs Average Precision(AP)

ROC-AUC
X 軸: FPR = FP / (FP + TN)
Y 軸: TPR = Recall
ランダム基準: 0.5(固定)
TN が多いと FPR が小さく見えすぎる
不均衡データでは楽観的になりがち
Average Precision(PR-AUC)
X 軸: Recall
Y 軸: Precision
ランダム基準: 陽性割合(例: 0.05)
TN の多さに惑わされない
不均衡データで正直な評価が可能

📊 PR 曲線のイメージ

不均衡データ(陽性5%)での PR 曲線の典型的な形状

Recall(感度) Precision(適合率) 0.0 0.5 1.0 0.0 0.5 1.0 ランダム(AP≈0.05) 良いモデル(AP≈0.45) F1最大閾値 PR 曲線 ランダム
💡
PR 曲線は右上(Recall=1, Precision=1)に近いほど良い。ランダムモデルは水平線(Y = 陽性割合)。曲線下の面積(AP)で一数値化できる。

📉 不均衡度ごとの AP ランダムベースライン

ランダムモデルの AP ≈ 陽性クラスの割合。不均衡が激しいほど AP の「壁」は低い

均衡(陽性 50%)
ランダムAP ≈ 0.50
0.50
やや不均衡(陽性 20%)
0.20
0.20
不均衡(陽性 5%)
0.05
0.05
極端な不均衡(陽性 1%)
0.01
0.01

一方、ROC-AUC のランダム基準は常に 0.5(不均衡度に依存しない)

🗂️ データスキーマ(シミュレーションデータ)

パラメータ説明
n_samples5,000総サンプル数
n_features20特徴量数
n_informative10実際に有用な特徴量数
weights[0.95, 0.05]陰性95% / 陽性5%(不均衡)
random_state42再現性確保

クラス分布(陽性 5% / 陰性 95%)

陰性(0) 95%(正常取引)

赤い細いバーが陽性(1) 5%(詐欺)

📝 問題

import numpy as np
import pandas as pd
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 (
    precision_recall_curve,
    average_precision_score,
    roc_auc_score,
    f1_score,
)

# 不均衡データの生成(陽性 5% / 陰性 95%)
X, y = make_classification(
    n_samples=5000,
    n_features=20,
    n_informative=10,
    weights=[0.95, 0.05],
    random_state=42,
)

問1 — データ分割とモデル学習

  1. train_test_split(80:20, stratify=y, random_state=42)で分割せよ
  2. StandardScaler + LogisticRegression(max_iter=1000, random_state=42) の Pipeline を学習せよ
  3. テストデータの予測確率 y_prob を取得せよ(predict_proba[:, 1]
  4. テストデータにおける陽性の割合を出力せよ

問2 — ROC-AUC vs AP の比較

  1. roc_auc_score(y_test, y_prob) を計算せよ
  2. average_precision_score(y_test, y_prob) を計算せよ
  3. ランダム予測の場合の ROC-AUC と AP の理論値をそれぞれ答えよ
  4. 2つの指標の「ランダムからの改善量」を比較し、どちらが正直な評価かを考察せよ

問3 — PR 曲線の描画

  1. precision_recall_curve(y_test, y_prob) で Precision・Recall・閾値を取得せよ
  2. PR 曲線を描画せよ(X=Recall, Y=Precision, AP を凡例に表示)
  3. ランダム予測のベースライン(水平線: Y = 陽性割合)も合わせて描画せよ

問4 — 最適閾値の選択

以下2つの基準でそれぞれ最適閾値を求めよ:

  1. F1 Score 最大化: 各閾値の F1 を計算し最大値と閾値を出力
  2. Precision ≥ 0.6 の条件下で Recall 最大化: 精度を確保しながら見逃しを減らす戦略

問5 — PR 曲線の形状分析(考察)

  1. PR 曲線が左上(高 Precision・高 Recall)に近いほど何を意味するか?
  2. 詐欺検出に「Precision 重視」vs「Recall 重視」どちらが向いているか、その理由は?
  3. Kaggle で評価指標が average_precision_score のとき、閾値最適化は必要か?

💡 ヒント

ヒント1(方向性)
  • precision_recall_curve は閾値を 1→0 に下げたときの (precision, recall, threshold) を返す
  • 返り値の長さに注意: len(precisions) = len(recalls) = len(thresholds) + 1
  • ランダム予測の AP ≈ y_test.mean()(陽性割合)
ヒント2(アプローチ)
# 予測確率の取得
y_prob = pipeline.predict_proba(X_test)[:, 1]

# PR 曲線の取得
precisions, recalls, thresholds = precision_recall_curve(y_test, y_prob)

# F1 の一括計算(閾値ごと)
f1_scores = 2 * precisions[:-1] * recalls[:-1] / (precisions[:-1] + recalls[:-1] + 1e-8)
best_idx = np.argmax(f1_scores)
ヒント3(コード骨格)
# PR 曲線の描画
plt.figure(figsize=(8, 6))
plt.plot(recalls, precisions, label=f'PR曲線 (AP = {ap:.3f})')
plt.axhline(y=y_test.mean(), color='red', linestyle='--',
            label=f'ランダム (AP ≈ {y_test.mean():.3f})')
plt.xlabel('Recall')
plt.ylabel('Precision')
plt.legend()

# Precision >= 0.6 の条件下で Recall 最大化
mask = precisions[:-1] >= 0.6
if mask.any():
    valid_indices = np.where(mask)[0]
    best_recall_idx = np.argmax(recalls[:-1][mask])
    best_idx_c = valid_indices[best_recall_idx]
    thr_c = thresholds[best_idx_c]

模範解答

import numpy as np
import pandas as pd
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 (
    precision_recall_curve,
    average_precision_score,
    roc_auc_score,
    f1_score,
)

# データ生成
X, y = make_classification(
    n_samples=5000, n_features=20, n_informative=10,
    weights=[0.95, 0.05], random_state=42,
)

# ── 問1: データ分割とモデル学習 ──
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(max_iter=1000, random_state=42))
])
pipeline.fit(X_train, y_train)
y_prob = pipeline.predict_proba(X_test)[:, 1]

pos_ratio = y_test.mean()
print(f"[問1] テストデータ陽性割合: {pos_ratio:.4f} ({(y_test==1).sum()}/{len(y_test)}件)")

# ── 問2: ROC-AUC vs AP ──
roc_auc = roc_auc_score(y_test, y_prob)
ap = average_precision_score(y_test, y_prob)

print(f"\n[問2] ROC-AUC: {roc_auc:.4f}  |  ランダム理論値: 0.5000  |  改善量: {roc_auc-0.5:.4f}")
print(f"[問2] AP:      {ap:.4f}  |  ランダム理論値: {pos_ratio:.4f}  |  改善量: {ap-pos_ratio:.4f}")
print(f"  考察: ROC-AUC={roc_auc:.3f} vs AP={ap:.3f}。不均衡データではAPがより正直な評価を提供する。")

# ── 問3: PR 曲線の描画 ──
precisions, recalls, thresholds = precision_recall_curve(y_test, y_prob)

plt.figure(figsize=(8, 6))
plt.plot(recalls, precisions, color='#a78bfa', linewidth=2,
         label=f'PR曲線 (AP = {ap:.3f})')
plt.axhline(y=pos_ratio, color='#ef4444', linestyle='--', linewidth=1.5,
            label=f'ランダム予測ベースライン (AP ≈ {pos_ratio:.3f})')
plt.fill_between(recalls, precisions, pos_ratio, alpha=0.1, color='#a78bfa')
plt.xlabel('Recall(感度)', fontsize=12)
plt.ylabel('Precision(適合率)', fontsize=12)
plt.title('PR 曲線 — 不均衡データ(陽性 5%)', fontsize=14)
plt.legend(loc='upper right')
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('pr_curve.png', dpi=150)
plt.show()

# ── 問4: 最適閾値の選択 ──

# (a) F1 最大化
f1_scores = 2 * precisions[:-1] * recalls[:-1] / (precisions[:-1] + recalls[:-1] + 1e-8)
best_idx = np.argmax(f1_scores)
best_thr = thresholds[best_idx]

print(f"\n[問4-a] F1最大化:")
print(f"  最適閾値: {best_thr:.3f}")
print(f"  Precision: {precisions[best_idx]:.3f}")
print(f"  Recall:    {recalls[best_idx]:.3f}")
print(f"  F1 Score:  {f1_scores[best_idx]:.3f}")

# (b) Precision >= 0.6 の条件下で Recall 最大化
mask = precisions[:-1] >= 0.6
if mask.any():
    valid_indices = np.where(mask)[0]
    best_recall_idx = np.argmax(recalls[:-1][mask])
    best_idx_c = valid_indices[best_recall_idx]
    thr_c = thresholds[best_idx_c]
    print(f"\n[問4-b] Precision >= 0.6 条件下:")
    print(f"  最適閾値: {thr_c:.3f}")
    print(f"  Precision: {precisions[best_idx_c]:.3f}")
    print(f"  Recall:    {recalls[best_idx_c]:.3f}")
    print(f"  F1 Score:  {2*precisions[best_idx_c]*recalls[best_idx_c]/(precisions[best_idx_c]+recalls[best_idx_c]+1e-8):.3f}")
else:
    print("[問4-b] Precision >= 0.6 を満たす閾値なし")

# ── 問5: 考察(出力) ──
print("""
[問5] 考察:
Q1. PR曲線が左上に近い = どの閾値でも高精度かつ見逃しが少ない完璧なモデル
Q2. 詐欺検出は「Recall重視」。詐欺見逃し(FN)の被害 >> 誤検知(FP)の不便
    ただしPrecision極端低下は運用コスト爆発のリスクあり → Precision制約付きRecall最大化が現実的
Q3. AP評価のKaggleコンペは確率値をそのまま提出 → 閾値最適化不要
""")

🎚️ ビジネス要件別・閾値選択戦略

ビジネス要件優先指標閾値の方向典型ユースケース
FN コストが致命的 Recall 最大化 閾値を下げる(例: 0.3) 癌検診・重大詐欺検出
FP コストが高い Precision 最大化 閾値を上げる(例: 0.7) 融資審査・法的証拠収集
FP/FN バランス重視 F1 最大化 PR 曲線から自動探索 スパムフィルター・推薦システム
Precision を保ちつつ Recall を上げたい 制約付き最適化 Precision ≥ X のもとで Recall 最大化 詐欺検出・医療スクリーニング

閾値と Precision / Recall のトレードオフ(PR 曲線上を移動するイメージ)

閾値(低 ← 0 〜 1 → 高) Precision↑ Recall↓ F1最大閾値 P≥0.6

🪜 Step-by-Step 解説

1precision_recall_curve の配列長の罠

precisions, recalls, thresholds = precision_recall_curve(y_test, y_prob)

# ⚠️ 配列長が異なる!
print(len(thresholds))   # 例: 999
print(len(precisions))   # 例: 1000  ← 最後は閾値=最大(全陰性予測)の特殊値
print(len(recalls))      # 例: 1000

# precisions[-1] = 1.0, recalls[-1] = 0.0(定義上)
# thresholds と対応させるには [:-1] でスライス
precisions_t = precisions[:-1]   # 閾値に対応する Precision
recalls_t    = recalls[:-1]      # 閾値に対応する Recall

2ランダムモデルとの比較(AP の意味)

# ランダム予測モデルのシミュレーション
np.random.seed(42)
y_random = np.random.rand(len(y_test))

random_ap  = average_precision_score(y_test, y_random)
random_roc = roc_auc_score(y_test, y_random)

print(f"ランダムモデル AP: {random_ap:.4f}")   # → ≈ 陽性割合 (0.05)
print(f"ランダムモデル ROC-AUC: {random_roc:.4f}")  # → ≈ 0.5

# 実際のモデルの改善量
print(f"モデル AP 改善: +{ap - random_ap:.4f}")
print(f"モデル ROC 改善: +{roc_auc - random_roc:.4f}")

3F1 最大閾値の探索

# 方法1: precision_recall_curve を使う(高速)
f1_arr = 2 * precisions[:-1] * recalls[:-1] / (precisions[:-1] + recalls[:-1] + 1e-8)
best_idx = np.argmax(f1_arr)
thr_f1 = thresholds[best_idx]

# 方法2: 総当たり(遅いが分かりやすい)
best_f1, best_thr = 0, 0.5
for thr in np.arange(0.01, 1.0, 0.01):
    y_tmp = (y_prob >= thr).astype(int)
    if y_tmp.sum() > 0:  # 全陰性予測は除外
        f = f1_score(y_test, y_tmp, zero_division=0)
        if f > best_f1:
            best_f1, best_thr = f, thr

print(f"方法1 閾値: {thr_f1:.3f}, F1: {f1_arr[best_idx]:.4f}")
print(f"方法2 閾値: {best_thr:.3f}, F1: {best_f1:.4f}")

4CV での AP スコア確認(Kaggle 実践)

from sklearn.model_selection import cross_val_score, StratifiedKFold

cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

ap_scores = cross_val_score(
    pipeline, X, y,
    cv=cv,
    scoring='average_precision'  # AP で評価
)

print(f"CV AP: {ap_scores.mean():.4f} ± {ap_scores.std():.4f}")
# 不均衡データでは各 fold に十分な陽性サンプルが必要 → stratify が重要

5AP の手動計算(理解のため)

# AP = Σ (recall[n] - recall[n-1]) × precision[n]
# sklearn の実装に準拠した手動計算

# recall を昇順に並べ直す
recall_sorted = recalls[::-1]   # 0 → 1 の順に
precision_sorted = precisions[::-1]

delta_recalls = np.diff(recall_sorted)   # 各ステップの Recall 増分
ap_manual = np.sum(delta_recalls * precision_sorted[1:])  # 各 Recall 区間の Precision

print(f"手動 AP: {ap_manual:.4f}")
print(f"sklearn AP: {ap:.4f}")

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

AP を「採点シート」で理解する

詐欺検出モデルが 100件の詐欺候補を「疑わしい順」に並べたとします:

順位実際に詐欺かその時点でのPrecisionRecallへの貢献
1位詐欺 ✅1/1 = 1.00+1/全詐欺数
2位正常 ❌1/2 = 0.500
3位詐欺 ✅2/3 = 0.67+1/全詐欺数
............

AP = 詐欺を発見するたびに「その時点でのPrecision」を記録し、平均した値。上位に詐欺が集中するほど AP は高くなる。

💡
AP の直感: 「詐欺疑惑ランキングの品質スコア」。ランキング上位に本物の詐欺が多いほど AP は高い。

ROC が楽観的になる理由(直感)

ROC の X 軸 = FPR = FP / (FP + TN)。不均衡データでは TN が巨大(例: 4750件)なので、FP が 100件増えても FPR = 100/4850 ≈ 0.02 とほとんど増えない。PR 曲線は TN を使わないため、この「水増し効果」が発生しない。

🏆 Kaggleでの実践的な使い方

場面使い方
評価指標の確認不均衡データでは AP / PR-AUC を優先。均衡データなら ROC-AUC でも可
コンペの提出形式が「確率」predict_proba[:, 1] をそのまま提出。閾値最適化は不要
コンペの提出形式が「ラベル」PR 曲線から F1 最大閾値(またはビジネス要件閾値)を選択して提出
モデル比較CV で scoring='average_precision' を使うと不均衡データでも正確に比較できる
エラー分析閾値決定後に Confusion Matrix と組み合わせて FP/FN サンプルを調査
# Kaggle 実践パターン: AP で CV 評価 → 最適閾値で最終予測
from sklearn.model_selection import cross_val_predict, StratifiedKFold

cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

# OOF(Out-of-Fold)予測確率
y_oof_prob = cross_val_predict(pipeline, X, y, cv=cv, method='predict_proba')[:, 1]

# OOF での AP
oof_ap = average_precision_score(y, y_oof_prob)
print(f"OOF AP: {oof_ap:.4f}")

# OOF での最適閾値(F1最大化)
prec_oof, rec_oof, thr_oof = precision_recall_curve(y, y_oof_prob)
f1_oof = 2 * prec_oof[:-1] * rec_oof[:-1] / (prec_oof[:-1] + rec_oof[:-1] + 1e-8)
best_thr_oof = thr_oof[np.argmax(f1_oof)]
print(f"最適閾値(OOF F1最大): {best_thr_oof:.3f}")

🚫 よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
PR 曲線のほうが ROC より「性能が低い」と誤解 グラフの見た目が低い位置に描かれる ランダム基準が異なる。AP=0.4 は大きな改善(ランダム=0.05)の場合も
precision_recall_curve の配列長の不一致で IndexError 3配列の長さが違うと思わない len(precisions) = len(recalls) = len(thresholds) + 1[:-1] でスライスして対応させる
AP ≈ ROC-AUC だと思い込む 「どちらも曲線の面積」という説明 不均衡データでは AP が大幅に低い値になる。計算基盤が全く異なる
確率提出コンペで閾値最適化にこだわる 「最適化しなければ損」という思い込み AP/ROC-AUC評価コンペでは確率そのまま提出。閾値は「0/1ラベル提出」のときのみ
常に Recall=1.0 を目指す 「見逃しゼロが最善」という直感 Recall=1.0 だと Precision が極端に低下。ビジネスコストを考慮した制約付き最適化が現実的

🚀 次のステップ

  • 発展: class_weight='balanced' や SMOTE で不均衡を補正し AP の変化を観察
  • 次回予告(Day 058): 交差検証の深掘り — Stratified K-Fold・繰り返し CV・Leave-One-Out の使い分け

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

✍️

理解度: [ ] 完全理解   [ ] おおむね理解   [ ] 要復習

自分の回答:

気づき・メモ: