📚 背景知識(読んでから問題へ)
🎯
Day 057 — Day 056 で学んだ Confusion Matrix は「特定の閾値での分解」でした。PR 曲線はすべての閾値での Precision-Recall トレードオフを可視化します。特に不均衡データ(詐欺検出・医療診断)で ROC-AUC より正直な評価ができます。
なぜ ROC では不十分なのか
不均衡データ(例: 詐欺 5% / 正常 95%)では:
| 予測戦略 | Accuracy | ROC-AUC | AP(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%の確率」に聞こえてしまい、モデルが全く役立たないことが分かりにくい。
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 曲線の典型的な形状
💡
PR 曲線は右上(Recall=1, Precision=1)に近いほど良い。ランダムモデルは水平線(Y = 陽性割合)。曲線下の面積(AP)で一数値化できる。
📉 不均衡度ごとの AP ランダムベースライン
ランダムモデルの AP ≈ 陽性クラスの割合。不均衡が激しいほど AP の「壁」は低い
一方、ROC-AUC のランダム基準は常に 0.5(不均衡度に依存しない)
🗂️ データスキーマ(シミュレーションデータ)
| パラメータ | 値 | 説明 |
|---|---|---|
n_samples | 5,000 | 総サンプル数 |
n_features | 20 | 特徴量数 |
n_informative | 10 | 実際に有用な特徴量数 |
weights | [0.95, 0.05] | 陰性95% / 陽性5%(不均衡) |
random_state | 42 | 再現性確保 |
クラス分布(陽性 5% / 陰性 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 — データ分割とモデル学習
train_test_split(80:20,stratify=y,random_state=42)で分割せよStandardScaler+LogisticRegression(max_iter=1000, random_state=42)の Pipeline を学習せよ- テストデータの予測確率
y_probを取得せよ(predict_proba[:, 1]) - テストデータにおける陽性の割合を出力せよ
問2 — ROC-AUC vs AP の比較
roc_auc_score(y_test, y_prob)を計算せよaverage_precision_score(y_test, y_prob)を計算せよ- ランダム予測の場合の ROC-AUC と AP の理論値をそれぞれ答えよ
- 2つの指標の「ランダムからの改善量」を比較し、どちらが正直な評価かを考察せよ
問3 — PR 曲線の描画
precision_recall_curve(y_test, y_prob)で Precision・Recall・閾値を取得せよ- PR 曲線を描画せよ(X=Recall, Y=Precision, AP を凡例に表示)
- ランダム予測のベースライン(水平線: Y = 陽性割合)も合わせて描画せよ
問4 — 最適閾値の選択
以下2つの基準でそれぞれ最適閾値を求めよ:
- F1 Score 最大化: 各閾値の F1 を計算し最大値と閾値を出力
- Precision ≥ 0.6 の条件下で Recall 最大化: 精度を確保しながら見逃しを減らす戦略
問5 — PR 曲線の形状分析(考察)
- PR 曲線が左上(高 Precision・高 Recall)に近いほど何を意味するか?
- 詐欺検出に「Precision 重視」vs「Recall 重視」どちらが向いているか、その理由は?
- 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 曲線上を移動するイメージ)
🪜 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件の詐欺候補を「疑わしい順」に並べたとします:
| 順位 | 実際に詐欺か | その時点でのPrecision | Recallへの貢献 |
|---|---|---|---|
| 1位 | 詐欺 ✅ | 1/1 = 1.00 | +1/全詐欺数 |
| 2位 | 正常 ❌ | 1/2 = 0.50 | 0 |
| 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 の使い分け
📋 自己評価(解いた後に記入)
✍️
理解度: [ ] 完全理解 [ ] おおむね理解 [ ] 要復習
自分の回答:
気づき・メモ: