金融 AI 评测中召回率与精确率的代价权衡比通用场景更严苛
为什么金融 AI 不能只看 F1
在通用 NLP 或图像分类任务里,大家习惯用 F1 Score 来平衡精确率(Precision)和召回率(Recall)。F1 高,模型就好——这套逻辑在多数场景跑得通。
但一到金融领域,这套就容易翻车。
举个真实例子:一个反欺诈模型如果漏掉一笔 50 万的交易(FN,漏报),直接损失就是 50 万。但如果它把 100 笔正常交易误判为欺诈(FP,误报),客户投诉、客服人力、甚至监管问询的成本加起来,可能远超 50 万。反过来,在信用评分中,拒绝一个优质客户(FP)只损失一笔利息,但放款给一个坏客户(FN)却可能损失整笔本金。
同样的指标,不同的代价函数。
关键在于:金融场景中,FP 和 FN 的经济影响不对称,且差异巨大。这时候再用 F1 最大化来选阈值,很可能选出一个“统计上好看、业务上亏钱”的方案。
代价敏感评测:把钱算进模型里
要解决这个问题,得引入“代价敏感评测”(Cost-Sensitive Evaluation)。核心思想很简单:不追求指标最大,而追求总经济损失最小。
不同金融场景的代价比
我们整理了几个典型金融场景的 FP/FN 代价估算(基于行业经验值):
| 场景 | FP 代价(误报) | FN 代价(漏报) | 代价比(FN/FP) | 优化倾向 |
|---|---|---|---|---|
| 反欺诈检测 | 中等(客户投诉处理) | 高(资金损失) | 5:1 – 10:1 | 偏向高召回 |
| 信用评估 | 低(失去利息收入) | 高(坏账) | 3:1 – 8:1 | 偏向高召回 |
| 合规监控 | 极高(监管罚款) | 中等(漏过可疑) | 0.2:1 – 0.5:1 | 偏向高精确 |
| 高风险客户识别 | 中等 | 极高(合规风险) | 10:1 – 50:1 | 强偏向召回 |
注意合规监控这个特例:误报(把正常交易标成可疑)可能触发监管报送,一旦出错就是百万级罚款;而漏报(没发现可疑)反而风险较低。所以这里反而要压低召回率、提高精确率。
总经济代价公式
设每个 FP 的代价为 $C_{FP}$,每个 FN 的代价为 $C_{FN}$,则总代价为:
$$ \text{TotalCost}(\tau) = \text{FP}(\tau) \times C_{FP} + \text{FN}(\tau) \times C_{FN} $$
最优阈值 $\tau^*$ 应使 TotalCost 最小,而不是让 F1 最大。
代码实践:自动找金融最优阈值
下面是一个轻量级的 Python 实现,支持按场景自动加载默认代价矩阵,并对比不同业务下的最优阈值。
import numpy as np
from typing import List, Tuple, Dict
from dataclasses import dataclass
@dataclass
class CostMatrix:
fp_cost: float # 误报代价(元)
fn_cost: float # 漏报代价(元)
tp_benefit: float = 0
tn_benefit: float = 0
@property
def cost_ratio(self) -> float:
return self.fn_cost / max(self.fp_cost, 1e-8)
class FinancialModelEvaluator:
DEFAULT_COST_MATRICES = {
"fraud_detection": CostMatrix(fp_cost=500, fn_cost=50000),
"credit_scoring": CostMatrix(fp_cost=2000, fn_cost=80000),
"compliance": CostMatrix(fp_cost=100000, fn_cost=20000),
}
def __init__(self, scenario: str = "fraud_detection"):
self.scenario = scenario
self.cost_matrix = self.DEFAULT_COST_MATRICES.get(
scenario, CostMatrix(fp_cost=1, fn_cost=1)
)
def compute_confusion_matrix(self, y_true, y_score, threshold):
y_pred = (y_score >= threshold).astype(int)
tp = int(np.sum((y_true == 1) & (y_pred == 1)))
fp = int(np.sum((y_true == 0) & (y_pred == 1)))
fn = int(np.sum((y_true == 1) & (y_pred == 0)))
tn = int(np.sum((y_true == 0) & (y_pred == 0)))
return tp, fp, fn, tn
def compute_total_cost(self, fp, fn, tp, tn):
loss = fp * self.cost_matrix.fp_cost + fn * self.cost_matrix.fn_cost
benefit = tp * self.cost_matrix.tp_benefit + tn * self.cost_matrix.tn_benefit
return loss - benefit
def find_optimal_threshold(self, y_true, y_score, n_thresholds=100):
thresholds = np.linspace(0, 1, n_thresholds)
results = []
for tau in thresholds:
tp, fp, fn, tn = self.compute_confusion_matrix(y_true, y_score, tau)
total_cost = self.compute_total_cost(fp, fn, tp, tn)
precision = tp / max(tp + fp, 1)
recall = tp / max(tp + fn, 1)
f1 = 2 * precision * recall / max(precision + recall, 1e-8)
results.append({
"threshold": tau,
"precision": precision,
"recall": recall,
"f1": f1,
"total_cost": total_cost,
})
best_cost = min(results, key=lambda r: r["total_cost"])
best_f1 = max(results, key=lambda r: r["f1"])
return {
"optimal_threshold": best_cost["threshold"],
"f1_at_optimal": best_cost["f1"],
"precision_at_optimal": best_cost["precision"],
"recall_at_optimal": best_cost["recall"],
"f1_optimal_threshold": best_f1["threshold"],
"threshold_gap": abs(best_cost["threshold"] - best_f1["threshold"]),
}
def compare_scenarios(self, y_true, y_score):
comparisons = {}
for name in self.DEFAULT_COST_MATRICES:
original = self.scenario
self.scenario = name
self.cost_matrix = self.DEFAULT_COST_MATRICES[name]
res = self.find_optimal_threshold(y_true, y_score)
comparisons[name] = {
"cost_ratio": self.cost_matrix.cost_ratio,
"optimal_threshold": res["optimal_threshold"],
"precision": res["precision_at_optimal"],
"recall": res["recall_at_optimal"],
}
self.scenario = original
self.cost_matrix = self.DEFAULT_COST_MATRICES[original]
return comparisons用模拟数据跑一遍,结果可能长这样:
场景 代价比 最优阈值 精确率 召回率
-------------------------------------------------------
fraud_detection 100.0 0.2121 0.3421 0.9231
credit_scoring 40.0 0.2828 0.4102 0.8765
compliance 0.2 0.7879 0.9654 0.3210可以看到,反欺诈和信用评分的最优阈值都很低(0.2–0.3),以换取高召回;而合规监控的阈值高达 0.78,宁可漏掉也不愿误报。
更重要的是,这些最优阈值与 F1 最优解的差距可能超过 0.3——这意味着如果你只调 F1,模型上线后可能每天都在亏钱。
统计指标 vs 业务指标:别被 AUC 骗了
很多人迷信 AUC-ROC,觉得 0.95 就是好模型。但在金融场景,AUC 高不代表业务表现好。
AUC 衡量的是模型排序能力,但它完全不关心你最终用哪个阈值做决策。一个 AUC 0.85 的模型,如果在代价敏感阈值下总损失更低,那它就比 AUC 0.95 的模型更值得上线。
同样,PR-AUC 虽然对不平衡数据更友好,但它仍是纯统计指标,无法反映真实经济损失。
真正该盯的,是“总经济代价”或“净收益”。
实施建议
- 建立场景化代价矩阵:不要用一套 FP/FN 代价打天下。反欺诈、信贷、合规、反洗钱,每个子场景都应有独立的代价估算。
- 代价不必精确,但要有范围:FP 成本可能是 500–2000 元,FN 可能是 5 万–10 万。用区间做敏感性分析,看最优阈值是否稳定。
- 把阈值差距写进报告:每次模型上线前,必须报告“代价最优阈值”与“F1 最优阈值”的差距。如果差距大,说明传统评测严重误导。
- 多目标场景用 Pareto 前沿:如果既要控制漏报又要控制误报(比如 VIP 客户反欺诈),可以用多目标优化找折中解,而不是硬塞一个单一阈值。
结语
金融 AI 的评测,本质上是一道经济学题,不是统计学题。召回率和精确率的博弈在这里格外残酷,因为每一次误判都直接对应真金白银的损失或风险。放弃对 F1 和 AUC 的盲目崇拜,把业务代价嵌入评测流程,才是金融 AI 落地的关键一步。