閾値最適化
「モデルの性能はそこそこ出てきた。だが、最終的なビジネス価値を決めるのは閾値だ。」
田中VPoEが強調する。
「同じモデルでも、閾値の設定一つでコストが数百万円変わる。ここを科学的にやるのがプロの仕事だ。」
閾値が決定に与える影響
モデルが出力する確率スコアを、最終的に「正常」か「不正」かの二値判定に変換するのが閾値の役割である。
確率スコア → [閾値] → 判定
閾値を下げると:
Recall ↑(より多くの不正を検知)
Precision ↓(誤検知が増える)
FNコスト ↓
FPコスト ↑
閾値を上げると:
Recall ↓(見逃しが増える)
Precision ↑(誤検知が減る)
FNコスト ↑
FPコスト ↓
コストベース最適化
総コスト関数
import numpy as np
from sklearn.metrics import confusion_matrix
def total_cost(y_true, y_prob, threshold, cost_fn=50000, cost_fp=500):
"""指定閾値での総ビジネスコスト"""
y_pred = (y_prob >= threshold).astype(int)
tn, fp, fn, tp = confusion_matrix(y_true, y_pred).ravel()
return fn * cost_fn + fp * cost_fp
def find_optimal_threshold(y_true, y_prob,
cost_fn=50000, cost_fp=500,
n_thresholds=1000):
"""コスト最小化閾値を探索"""
thresholds = np.linspace(0.001, 0.999, n_thresholds)
costs = [total_cost(y_true, y_prob, t, cost_fn, cost_fp)
for t in thresholds]
best_idx = np.argmin(costs)
return thresholds[best_idx], costs[best_idx], thresholds, costs
optimal_t, min_cost, thresholds, costs = find_optimal_threshold(
y_test, y_prob
)
print(f"最適閾値: {optimal_t:.4f}")
print(f"最小コスト: {min_cost:,.0f}円")
コスト曲線の詳細分析
import matplotlib.pyplot as plt
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# 総コスト
axes[0, 0].plot(thresholds, costs)
axes[0, 0].axvline(x=optimal_t, color='r', linestyle='--',
label=f'最適閾値={optimal_t:.3f}')
axes[0, 0].set_xlabel('閾値')
axes[0, 0].set_ylabel('総コスト(円)')
axes[0, 0].set_title('総コスト')
axes[0, 0].legend()
# FN/FPコストの内訳
fn_costs_list = []
fp_costs_list = []
for t in thresholds:
y_pred = (y_prob >= t).astype(int)
tn, fp, fn, tp = confusion_matrix(y_test, y_pred).ravel()
fn_costs_list.append(fn * 50000)
fp_costs_list.append(fp * 500)
axes[0, 1].plot(thresholds, fn_costs_list, label='FNコスト', color='red')
axes[0, 1].plot(thresholds, fp_costs_list, label='FPコスト', color='blue')
axes[0, 1].axvline(x=optimal_t, color='gray', linestyle='--')
axes[0, 1].set_xlabel('閾値')
axes[0, 1].set_ylabel('コスト(円)')
axes[0, 1].set_title('FN/FPコストの内訳')
axes[0, 1].legend()
# Recall/Precision
recalls = []
precisions = []
for t in thresholds:
y_pred = (y_prob >= t).astype(int)
tn, fp, fn, tp = confusion_matrix(y_test, y_pred).ravel()
recalls.append(tp / (tp + fn) if (tp + fn) > 0 else 0)
precisions.append(tp / (tp + fp) if (tp + fp) > 0 else 0)
axes[1, 0].plot(thresholds, recalls, label='Recall')
axes[1, 0].plot(thresholds, precisions, label='Precision')
axes[1, 0].axvline(x=optimal_t, color='gray', linestyle='--')
axes[1, 0].set_xlabel('閾値')
axes[1, 0].set_title('Recall / Precision')
axes[1, 0].legend()
# F1 Score
f1_scores = [2*p*r/(p+r) if (p+r) > 0 else 0
for p, r in zip(precisions, recalls)]
axes[1, 1].plot(thresholds, f1_scores, color='green')
axes[1, 1].axvline(x=optimal_t, color='gray', linestyle='--',
label='コスト最適閾値')
best_f1_t = thresholds[np.argmax(f1_scores)]
axes[1, 1].axvline(x=best_f1_t, color='orange', linestyle='--',
label='F1最適閾値')
axes[1, 1].set_xlabel('閾値')
axes[1, 1].set_title('F1-Score')
axes[1, 1].legend()
plt.tight_layout()
plt.show()
精度 vs 再現率のトレードオフ
運用シナリオ別の閾値設定
シナリオ1: 高額取引の監視(見逃し厳禁)
→ 低い閾値(例: 0.1)
→ Recall > 95% を目指す
→ 偽陽性が多くても人手で確認
シナリオ2: 通常取引のスクリーニング(バランス重視)
→ 中程度の閾値(例: 0.3〜0.5)
→ F1-Score を最大化
→ 調査リソースとのバランス
シナリオ3: 自動ブロック判定(誤検知厳禁)
→ 高い閾値(例: 0.8)
→ Precision > 80% を目指す
→ 確実な不正のみ自動ブロック
多段階閾値の設計
# 実運用では単一閾値ではなく、多段階で判定する
def multi_threshold_decision(score, thresholds):
"""
多段階閾値による判定
thresholds: {'block': 0.8, 'review': 0.3, 'monitor': 0.1}
"""
if score >= thresholds['block']:
return 'BLOCK' # 即座にブロック
elif score >= thresholds['review']:
return 'REVIEW' # 人手による調査
elif score >= thresholds['monitor']:
return 'MONITOR' # モニタリング対象
else:
return 'APPROVE' # 承認
# 各段階の閾値をコスト最適化
# BLOCK: 高確信度の不正 → 自動ブロック
# REVIEW: 疑わしい取引 → 調査チームへ
# MONITOR: 軽度の異常 → ログ記録
# APPROVE: 正常 → 通過
閾値の動的調整
# 時間帯や取引特性に応じて閾値を動的に変更
def dynamic_threshold(base_threshold, transaction):
"""取引特性に基づく動的閾値"""
threshold = base_threshold
# 高額取引は閾値を下げる(より厳しく)
if transaction['amount'] > 100000:
threshold *= 0.5
# 深夜帯は閾値を下げる
hour = transaction['hour']
if 2 <= hour <= 5:
threshold *= 0.7
# 新規顧客は閾値を下げる
if transaction['account_age_days'] < 30:
threshold *= 0.8
return max(threshold, 0.05) # 下限設定
感度分析
コスト比率の変化が閾値に与える影響
# FN/FPのコスト比率を変えて最適閾値がどう変わるか分析
cost_ratios = [10, 50, 100, 200, 500]
optimal_thresholds = []
for ratio in cost_ratios:
cost_fn = ratio * 500 # FPコストの ratio 倍
t, _, _, _ = find_optimal_threshold(
y_test, y_prob, cost_fn=cost_fn, cost_fp=500
)
optimal_thresholds.append(t)
print(f"FN/FP比率 {ratio}:1 → 最適閾値 = {t:.4f}")
# FN/FPコスト比が大きいほど、最適閾値は低くなる(Recall重視)
まとめ
| 項目 | ポイント |
|---|---|
| コスト最適化 | FN/FPコストに基づく閾値探索が最も実用的 |
| 多段階閾値 | BLOCK/REVIEW/MONITOR/APPROVEの4段階が実運用向け |
| 動的閾値 | 取引金額・時間帯・顧客属性で調整 |
| 感度分析 | コスト比率の変化が閾値に与える影響を把握しておく |
チェックリスト
- コストベースの閾値最適化を実装できる
- 多段階閾値の設計意図を説明できる
- 動的閾値の必要性と実装方法を理解した
- コスト比率の感度分析を実施できる
次のステップへ
閾値最適化を理解したところで、次は演習で実際に不正検知モデルのスコアを改善しよう。
推定読了時間: 30分