金融 AI 评测:召回率和精确率的博弈比通用场景更残酷
一、个性化深度引言
在通用 NLP 任务中,精确率和召回率的取舍通常遵循 F1 最大化。但在金融场景,这个取舍要重新算账。
一个例子:反欺诈模型漏掉一次欺诈交易(低召回率),损失可能是 50 万。但如果误拦了 100 次正常交易(低精确率),客户投诉和监管问询的代价远超 50 万。另一面,信用风险评估中,错过一个坏客户(低召回率)可能损失 100 万,而拒绝一个好客户(低精确率)只损失一笔利息收入。
同样的指标,不同的代价函数。见证奇迹的时刻在于:当我们把“误判的经济成本”直接作为损失权重后,同一个模型在两个业务场景下的最优阈值差了 0.3。
二、个性化原理剖析
代价敏感评测框架
不同金融场景的代价比
| 反欺诈检测 | 中等(客户投诉) | 高(资金损失) | 5:1 – 10:1 | 偏向高召回 |
| 信用评估 | 低(失去一笔生意) | 高(坏账损失) | 3:1 – 8:1 | 偏向高召回 |
| 合规监控 | 极高(监管处罚) | 中等(漏过可疑交易) | 0.2:1 – 0.5:1 | 偏向高精确 |
| 量化信号过滤 | 低(错过机会) | 低(小损失) | ~1:1 | 平衡 |
| 高风险客户识别 | 中等 | 极高(合规风险) | 10:1 – 50:1 | 强偏向召回 |
代价敏感阈值选择
传统方法选择使 F1 最大的阈值。但在金融场景,应该选择使总经济代价最小的阈值。
假设每个 FP 的代价是 CF,每个 FN 的代价是 CFN,则总代价为:
$$TotalCost(\\tau) = FP(\\tau) \\times C_F + FN(\\tau) \\times C_{FN}$$
最优阈值 $\\tau^*$ 满足总代价最小,而非 F1 最大。
三、个性化代码实践
import numpy as np
from typing import List, Tuple, Dict, Optional
from dataclasses import dataclass
@dataclass
class CostMatrix:
"""代价矩阵"""
fp_cost: float # 误报代价(元)
fn_cost: float # 漏报代价(元)
tp_benefit: float = 0 # TP 带来的收益
tn_benefit: float = 0 # TN 带来的收益
@property
def cost_ratio(self) -> float:
"""误报 vs 漏报的代价比"""
return self.fn_cost / max(self.fp_cost, 1e-8)
class FinancialModelEvaluator:
"""金融模型评测器:代价敏感版本"""
# 设计原因:各金融场景的默认代价矩阵
# 基于行业经验值,可通过实际业务数据进行校准
DEFAULT_COST_MATRICES = {
"fraud_detection": CostMatrix(
fp_cost=500, # 客户投诉处理成本
fn_cost=50000, # 平均欺诈损失
tp_benefit=50000,
),
"credit_scoring": CostMatrix(
fp_cost=2000, # 失去一个客户的利息收入
fn_cost=80000, # 平均坏账金额
tp_benefit=2000,
),
"compliance": CostMatrix(
fp_cost=100000, # 监管处罚
fn_cost=20000, # 漏过的可疑交易
tp_benefit=0,
),
}
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: np.ndarray,
y_score: np.ndarray,
threshold: float,
) -> Tuple[int, int, int, int]:
"""计算混淆矩阵"""
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: int, fn: int, tp: int, tn: int
) -> float:
"""计算总经济代价"""
# 设计原因:代价 = 误判损失 – 正确收益
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: np.ndarray,
y_score: np.ndarray,
n_thresholds: int = 100,
) -> Dict:
"""
寻找最优阈值(代价最小化)
设计原因:金融场景下最小化总代价,而非最大化 F1
"""
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(tp, fp, fn, 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,
"tp": tp, "fp": fp, "fn": fn, "tn": tn,
"precision": precision,
"recall": recall,
"f1": f1,
"total_cost": total_cost,
})
# 找到代价最小的阈值
best_cost = min(results, key=lambda r: r["total_cost"])
# 找到 F1 最大的阈值(对比用)
best_f1 = max(results, key=lambda r: r["f1"])
return {
"optimal_threshold": best_cost["threshold"],
"min_cost": best_cost["total_cost"],
"f1_at_optimal": best_cost["f1"],
"precision_at_optimal": best_cost["precision"],
"recall_at_optimal": best_cost["recall"],
"f1_optimal_threshold": best_f1["threshold"],
"max_f1": best_f1["f1"],
"cost_at_f1_optimal": best_f1["total_cost"],
"threshold_gap": abs(
best_cost["threshold"] – best_f1["threshold"]
),
}
def compare_scenarios(
self,
y_true: np.ndarray,
y_score: np.ndarray,
) -> Dict:
"""对比不同业务场景的最优阈值"""
comparisons = {}
for scenario_name in self.DEFAULT_COST_MATRICES:
# 临时切换场景
original_scenario = self.scenario
self.scenario = scenario_name
self.cost_matrix = self.DEFAULT_COST_MATRICES[scenario_name]
result = self.find_optimal_threshold(y_true, y_score)
comparisons[scenario_name] = {
"cost_ratio": self.cost_matrix.cost_ratio,
"optimal_threshold": result["optimal_threshold"],
"precision": result["precision_at_optimal"],
"recall": result["recall_at_optimal"],
}
# 恢复原场景
self.scenario = original_scenario
self.cost_matrix = self.DEFAULT_COST_MATRICES[original_scenario]
return comparisons
# 使用示例
np.random.seed(42)
# 模拟数据:1000 个样本,5% 异常率
n_samples = 1000
y_true = np.random.choice([0, 1], n_samples, p=[0.95, 0.05])
# 模拟模型分数:正样本分数偏高
y_score = np.zeros(n_samples)
for i in range(n_samples):
if y_true[i] == 1:
y_score[i] = np.random.beta(5, 2) # 偏高分
else:
y_score[i] = np.random.beta(2, 5) # 偏低分
# 评测:不同场景的最优阈值
evaluator = FinancialModelEvaluator("fraud_detection")
comparison = evaluator.compare_scenarios(y_true, y_score)
print("各场景最优阈值对比:")
print(f"{'场景':<15} {'代价比':<10} {'最优阈值':<10} {'精确率':<8} {'召回率':<8}")
print("-" * 55)
for scenario, metrics in comparison.items():
print(
f"{scenario:<15} "
f"{metrics['cost_ratio']:<10.1f} "
f"{metrics['optimal_threshold']:<10.4f} "
f"{metrics['precision']:<8.4f} "
f"{metrics['recall']:<8.4f}"
)
四、个性化边界权衡
| F1 Score | 通用场景、代价对称 | 忽略业务代价的不对称性 |
| AUC-ROC | 模型排序能力 | 不关心最优阈值 |
| PR-AUC | 不平衡数据 | 仍然是统计指标 |
| 总经济代价 | 金融业务决策 | 需要准确的代价估算 |
| 净收益 | 高层决策 | 更依赖代价估算 |
关键权衡:
五、总结
金融 AI 的评测必须从“统计指标最优”转向“经济代价最小”。不同金融场景的 FP/FN 代价比差异巨大——反欺诈和信用评估偏重召回率(代价比 5:1 到 50:1),合规监控偏重精确率(代价比 0.2:1 到 0.5:1)。工程上,建议建立代价矩阵字典(按场景划分 FP 和 FN 的经济成本),在模型上线前进行代价敏感阈值搜索,将最优阈值与 F1 最优阈值的差距作为模型分析报告的必选项。代价估算不必精确到个位数,但需要给出合理的波动范围,并验证在这个范围内最优阈值的稳定性。





