很多人做风控时,第一反应是把欺诈率压到 0,甚至写进 KPI:欺诈率必须为零、风险单必须清零。但 2022 年海外一篇讨论文章《The optimal amount of fraud is non-zero》提出了一个非常反直觉的观点:最优的欺诈数量不是 0,而是一个大于 0 的值。
这个观点如果只停留在口号层面,很容易被误读成“风控可以放水”。实际上,它是在讲一个成本决策问题:欺诈检测系统不是一个“抓坏人越多越好”的分类器,而是一个需要在“拦截损失”和“误杀成本”之间做权衡的业务系统。把欺诈率压到 0 的代价,往往是误杀大量真实用户,损失的收入和用户体验远高于那点欺诈损失。
这篇文章我会从概念、数学推导、Python 模拟、实时风控 API 设计、批量扫描、指标验证和排查思路几个维度展开。适合支付、电商、广告反作弊、账号安全、信贷风控的同学,也适合想理解“为什么反欺诈系统要留余量”的产品和技术负责人。
1. 核心概念:什么是最优欺诈率
1.1 从“零容忍”到“总成本最小”
欺诈检测在本质上是一个二分类决策问题。每一条交易、每一次登录、每一笔支付,系统都要输出一个“放行 / 拦截 / 人工审核”的决策。
如果只看欺诈这一侧,目标很清晰:把欺诈交易全部拦下来。但任何决策系统都有代价:
| 决策结果 | 含义 | 真实代价 |
|---|---|---|
| 正确放行(TN) | 正常用户通过 | 无代价 |
| 正确拦截(TP) | 欺诈行为被拦截 | 无代价,但占用风控资源 |
| 漏过欺诈(FN) | 欺诈交易放行成功 | 直接资金损失、舆情损失 |
| 误杀正常用户(FP) | 正常交易被拦截 | 用户流失、客服成本、收入损失、品牌损失 |
“最优欺诈率非零”的核心,就是不要把 FN 和 FP 看作对称的错误,而是把它们的代价量化,再去找一个让“总代价最小”的决策边界。
1.2 数学表达:最优阈值不是一个拍脑袋的值
假设模型给某条交易输出的欺诈概率是 p。我们做两种决策:
- 放行:期望代价 = p × C_FN
- 拦截:期望代价 = (1 - p) × C_FP
其中:
- C_FN 是漏过一笔欺诈的损失,包含资金、赔付、舆情等。
- C_FP 是误杀一个真实用户的损失,包含收入损失、客服成本、用户流失等。
当拦截代价小于放行代价时,我们应该拦截:
(1 - p) × C_FP <= p × C_FN化简得到最优阈值:
p* = C_FP / (C_FP + C_FN)这个公式非常重要。它说明,最优阈值不是由模型精度单独决定的,而是由业务成本决定的。如果误杀一个真实用户损失 500 元,漏过一笔欺诈损失 50 元,那么最优阈值是:
p* = 500 / (500 + 50) = 0.909也就是说,只有当模型认为这笔交易的欺诈概率超过 90.9% 时才拦截。这时候,欺诈率一定不是 0,因为大量欺诈概率在 50% 到 90% 之间的交易会被放行。
1.3 为什么不是零:三个直觉理由
第一,欺诈检测不是一个完美可分的分类问题。正常用户和欺诈者的行为特征有重叠,把阈值拉得越低,误杀的正常用户就越多,而且增长速度非常快。
第二,真实用户的价值通常远大于单笔欺诈损失。一个用户可能持续消费五年,复购、口碑、推荐都会带来收入。误杀一个优质用户,损失的往往不是一单,而是整个生命周期价值。
第三,追求零欺诈率会导致系统过度拒绝。如果业务层面要求“欺诈率必须为零”,那唯一的做法就是所有高风险动作一律拦截,哪怕概率只有 1%。这会让大量正常用户无法下单、无法登录、无法提现,业务收入和用户体验会以更快的速度崩塌。
所以,最优欺诈率不是 0,而是“再多拦一笔欺诈带来的边际成本,刚好等于再放走一笔欺诈带来的边际损失”的那个位置。
2. 适用场景与使用边界
2.1 这种思路适合什么场景
这套成本权衡思路最适合商业决策类风控场景:
- 支付风控:决定一笔交易是否放行,可以算清资金损失和挽留成本。
- 电商平台:识别虚假订单、恶意退款、黄牛抢购,但不能把正常买家误杀。
- 广告反作弊:过滤无效点击和刷量流量,但也要保证正常推广流量不被误判。
- 交易平台与内容社区:识别刷单、虚假评价、垃圾账号,但需要保证正常创作者和买家体验。
在这些场景里,“误杀”和“漏过”都有明确的业务代价,可以用金额量化,也可以转化为业务 KPI 来跟踪。
2.2 什么场景不能用“非零欺诈”
有一些场景,欺诈率必须趋近于零,不是由成本模型决定的,而是由法律合规和资金安全底线决定的:
- 洗钱、赌博、诈骗资金链路,监管明确要求止付和上报。
- 盗卡交易,涉及卡片组织的强制规则和赔付要求,必须拦截。
- 未成年人保护、金融适老化等合规场景,不能只算经济账。
- 批量盗号、撞库攻击,即使拦截的真实用户比例高,也必须先止损。
在这些场景里,成本模型只能作为辅助,不能覆盖规则底线。正确做法是把强制规则放在最前端,成本决策放在规则之后。
2.3 隐私和合规边界
风控系统会大量使用用户基础信息、设备信息、行为轨迹。使用时必须注意数据合规:
- 数据采集必须基于业务必要性和用户授权。
- 涉及人脸、声纹、指纹等生物特征数据的,需要按法律要求做单独授权和告知。
- 风控模型训练和测试使用的数据要脱敏,不能直接暴露原始敏感字段。
- 用户被误杀时要有申诉通道和人工复核机制,不能完全交给模型一票否决。
3. 技术落地:把欺诈检测做成一个决策系统
3.1 一个典型的实时风控链路
实时欺诈检测并不只是“训练一个模型”那么简单,工程上通常是一条完整链路:
客户端埋点 / 服务端事件上报 ↓ 特征平台(实时特征 + 离线特征) ↓ 规则引擎(强规则、黑名单、频控) ↓ 模型服务(欺诈概率打分) ↓ 决策引擎(结合阈值、成本、策略输出结果) ↓ 处置动作(放行 / 拦截 / 人工审核 / 二次验证) ↓ 监控与复盘(指标回溯、特征漂移、策略回滚)在这个链路里,模型输出的不是“是或否”,而是“欺诈概率”。真正的最终决策,由决策引擎结合业务成本、阈值、规则等级、用户维度、资源情况共同完成。
3.2 用 Python 模拟成本最优决策
为了理解上面那个阈值公式,我用 Python 做一个小型模拟。数据是构造的,不代表真实业务,但流程可以复用。
import numpy as np def simulate_decisions(y_true, y_prob, threshold): """ 模拟决策结果,返回误杀数和漏过数。 y_true: 真实标签,1 为欺诈 y_prob: 模型输出的欺诈概率 threshold: 拦截阈值 """ y_pred = (y_prob >= threshold).astype(int) fp = int(((y_pred == 1) & (y_true == 0)).sum()) # 误杀 fn = int(((y_pred == 0) & (y_true == 1)).sum()) # 漏过 return fp, fn def search_threshold(y_true, y_prob, cost_fp, cost_fn, grid): """ 在给定成本参数下,搜索总代价最小的阈值。 """ best_cost = float("inf") best_result = None for t in grid: fp, fn = simulate_decisions(y_true, y_prob, t) total_cost = fp * cost_fp + fn * cost_fn if total_cost < best_cost: best_cost = total_cost best_result = (t, fp, fn, total_cost) return best_result再用一份构造数据跑一下搜索:
rng = np.random.default_rng(42) n = 100000 # 构造:正常样本风险分数低,欺诈样本风险分数高 y_true = np.concatenate([ np.zeros(n - 500), np.ones(500) ]).astype(int) y_prob = np.concatenate([ rng.beta(2, 8, size=n - 500), # 正常样本 rng.beta(7, 3, size=500) # 欺诈样本 ]) # 假设:误杀一个真实用户损失 50 元,漏过一笔欺诈损失 200 元 cost_fp = 50.0 cost_fn = 200.0 grid = np.round(np.arange(0.05, 0.96, 0.01), 2) result = search_threshold(y_true, y_prob, cost_fp, cost_fn, grid) print("最优阈值:", result[0]) print("误杀数量:", result[1]) print("漏过数量:", result[2]) print("最小总代价:", result[3])运行结果会随着构造数据的分布变化,但思路是清晰的:当 cost_fp 相对 cost_fn 更大时,最优阈值会上升,系统会容忍更多欺诈;当 cost_fn 更大时,最优阈值会下降,系统会更激进地拦截。
这就是“最优欺诈率非零”在代码层面的体现:最优阈值不是让欺诈率最低的那个值,而是让总业务成本最小的那个值。
3.3 把成本敏感逻辑接入决策引擎
真实系统里,模型分数和阈值不会只有一个,而是分层决策:
def decide(score, amount, user_level, cost_fp, cost_fn): """ 简化的决策函数。 实际生产中还会有多条规则、多个模型分数和人工审核兜底。 """ threshold = cost_fp / (cost_fp + cost_fn) # 强规则优先 if amount > 50000 and score > 0.8: return "reject_high_risk", "high_amount_high_score" # 成本最优阈值 if score >= threshold: return "reject", "cost_based" # 中风险进入人工审核 if score >= threshold * 0.7: return "review", "medium_risk" return "approve", "normal"注意,这里 threshold 会随业务场景变化。大额交易的 cost_fn 更大,阈值应该更低;小额交易 cost_fn 小,阈值可以更高。同一个模型分数,在不同业务场景下应该映射到不同决策。
4. 效果验证与指标设计
4.1 不要只看欺诈率
很多团队上线风控模型时只看“拦截了多少欺诈”,这是不够的。要判断系统是否处于“最优区域”,至少要看一组指标:
| 指标 | 计算方式 | 说明 |
|---|---|---|
| 欺诈率 | 欺诈交易 / 总交易 | 衡量风险水平 |
| 拦截率 | 被拦截的欺诈交易 / 欺诈交易 | 衡量模型能力 |
| 误杀率 | 被拦截的正常交易 / 正常交易 | 衡量误伤程度 |
| 可避免损失 | 成功拦截的欺诈金额 | 量化止损效果 |
| 用户申诉率 | 申诉用户 / 被拦截用户 | 反映误杀的显性压力 |
| 收入影响 | 被误杀用户的预期消费金额 | 量化 FP 代价 |
| 风险覆盖率 | 被模型覆盖的交易量 / 总交易量 | 判断策略是否全面 |
4.2 用实验验证阈值是否合理
阈值不是上线就固定不变的。比较稳妥的做法是:
- 先做离线回测:用历史交易数据,把不同阈值下的欺诈率、误杀率、总成本拉出来,画出曲线。
- 再小流量实验:线上切 5% 流量,对比新阈值和旧阈值下的指标差异。
- 观察一个完整业务周期:至少覆盖一次正常交易高峰和一次营销活动,因为这两个阶段的行为特征差异很大。
- 确认收益:如果新阈值下,总成本下降、用户申诉没有明显上升,再逐步放开流量。
这里的关键思维是:不要单独以一个“阈值”或一个“分数”为目标,而是以业务总代价为目标。模型分数是一个中间变量,不是最终指标。
5. 接口与批量任务设计
5.1 实时决策接口
风控系统通常以 HTTP API 的方式对外提供服务。一个最简单的实时风险决策接口是这样的:
curl -X POST http://127.0.0.1:8080/v1/risk/check \ -H "Content-Type: application/json" \ -d '{ "request_id": "req_20250101_001", "user_id": "user_10086", "biz_type": "payment", "amount": 299.00, "ip": "203.0.113.8", "device_id": "device_8f2a1c", "scene": "order_pay" }'返回结果:
{ "request_id": "req_20250101_001", "decision": "approve", "score": 0.31, "threshold": 0.41, "reason_codes": [], "cost_fp_ref": 1.0, "cost_fn_ref": 0.6 }Python 调用示例:
import requests url = "http://127.0.0.1:8080/v1/risk/check" payload = { "request_id": "req_20250101_002", "user_id": "user_10087", "biz_type": "payment", "amount": 199.00, "ip": "203.0.113.9", "device_id": "device_aabb", "scene": "order_pay" } response = requests.post(url, json=payload, timeout=500) result = response.json() if result["decision"] == "approve": print("放行") elif result["decision"] == "review": print("转人工审核") else: print("拦截")接口设计时要注意:
- 所有参数必须带 request_id,方便全链路追踪。
- 返回 deciion 的同时,要把 score、threshold、reason_codes 一起返回,方便业务方定位问题。
- 服务要做好超时和降级,不能因为风控服务超时把支付主流程拖死。
5.2 批量离线扫描
除了实时决策,风控系统还需要支持离线批量任务,比如每天扫描全量历史订单、批量识别异常退款、给存量用户打风险分层标签。
批量任务通常走异步队列,不能直接同步返回结果:
POST /v1/risk/scan/batch { "task_id": "task_20250101_001", "biz_type": "order_refund", "date_range": ["2024-12-01", "2024-12-31"], "threshold_override": 0.45 }任务提交后,服务端返回任务状态,业务方轮询或通过回调获取结果:
{ "task_id": "task_20250101_001", "status": "running", "total_items": 120000, "processed_items": 45000 }批量任务要特别注意几点:
- 任务可以重跑,结果要幂等。
- 每个子任务失败要记录错误并重试,不要因为个别数据异常导致整个任务中断。
- 输出结果按风险等级分目录保存,方便不同团队取用。
- 批量任务最好支持“试运行”模式,先输出统计结果,不实际触发处置动作。
6. 性能与资源占用观察
6.1 实时风控的性能指标
风控接口的性能直接影响业务主流程,核心观察指标包括:
- 平均响应时间:通常要在 100ms 到 300ms 以内,取决于特征计算量。
- P99 响应时间:比平均值更能反映长尾问题。
- QPS:接口最大处理能力。
- 特征平台命中率:缓存命中率越高,响应越快。
- 外部数据源耗时:三方数据、黑名单查询、IP 库查询容易成为瓶颈。
如果发现实时接口变慢,优先排查这些点:
- 特征计算链路里有没有慢 SQL。
- 外部数据源有没有设置超时时间和熔断。
- Redis 缓存是否过期集中,导致缓存击穿。
- 模型推理时有没有做批量加速,还是逐条请求。
6.2 批量任务对资源的影响
批量扫描如果和实时服务共用同一套计算资源,很容易把实时服务的响应时间拉高。推荐的策略是:
- 批量任务部署在单独的进程或容器中。
- 实时服务优先使用低延迟资源,离线扫描使用独立的弹性资源。
- 批量任务限制并发数,比如设置最大 20 个并发,避免打爆数据库和特征平台。
- 批量任务运行期间要关注 CPU、内存、磁盘 IO,尤其是大量读取订单表时容易造成主库压力。
6.3 如何降低计算压力
- 特征结果做缓存,重复特征不重复计算。
- 模型做个轻量版本,先粗筛,再对高分样本用全量特征重算。
- 规则引擎和模型分阶段处理,黑名单命中直接拦截,不进入模型推理。
- 批次扫描时按用户维度做聚合,减少重复计算。
7. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方向 |
|---|---|---|---|
| 欺诈率明显上升,但拦截率没变化 | 模型或特征漂移,欺诈模式变化 | 对比近期样本分数分布,查看模型 PSI | 重新训练模型、补充新特征 |
| 误杀率突然升高 | 最优阈值被调低,或特征异常 | 检查阈值配置、特征值分布 | 回滚阈值,检查上游特征 |
| 所有风险决策都变成放行 | 模型服务未加载,或强规则未生效 | 检查模型服务日志、规则配置 | 重新加载模型,检查规则版本 |
| 风控接口响应变慢 | 特征计算慢、外部数据源超时、缓存失效 | 查看链路追踪,查看慢查询日志 | 加缓存、设置外部接口超时、预计算特征 |
| 批量任务卡住 | 消息队列堆积、下游依赖不可用 | 查看队列堆积数、任务日志 | 提高消费者并发、重试失败任务 |
| 指标口径对不上 | 不同团队对“欺诈率”定义不一致 | 统一分母、分子口径说明 | 建立指标字典,统一计算口径 |
| 阈值调优后整体负向 | 成本参数没同步更新,或实验观察时间过短 | 检查 cost_fp / cost_fn 配置,查看实验周期 | 重新核算成本参数,延长实验周期 |
| 人工审核队列堆积 | 中风险阈值设置过高,审核资源不足 | 查看队列长度和平均处理时长 | 调整中风险阈值,增加审核人力或优化审核工具 |
8. 最佳实践与合规红线
8.1 工程最佳实践
第一,第一次上线不要追求复杂。先用一个规则引擎加一个模型,固定一套最小可运行配置,跑通整条链路后再逐步加策略。
第二,模型、规则、阈值、成本参数全部做版本管理。每次变更要能回滚,最好做到一键回滚到上一个稳定版本。
第三,分目录管理好模型文件、特征配置、样本数据、输出结果。离线实验目录和线上服务目录要分开,避免误用。
第四,批量任务必须加日志和失败重试机制。批量任务涉及的数据量通常很大,如果中间断掉需要能断点续跑,而不是从头再来。
第五,接口服务要加鉴权和限流。风控接口暴露的是核心决策能力,不能让任意调用方随意访问,也要防止业务高峰期把风控服务打垮。
第六,给真实用户留“逃生通道”。被误杀的用户需要有申诉入口,申诉后要能人工复核或者二次验证,而不是永远无法恢复。
8.2 合规红线
“最优欺诈率非零”是一个成本决策工具,不是放松监管的理由。
涉及洗钱、赌博、诈骗、盗卡等监管明确禁止的资金链路,必须先按法律法规拦截并上报;涉及用户隐私数据的采集和处理,必须获得合法授权并做脱敏;涉及人脸、声纹等敏感生物数据,必须有单独授权和严格的使用边界;任何风控策略都不应该让用户陷入不可申诉、不可复核的局面。
如果成本模型算出来“放行更好”,但合规规则要求“必须拦截”,合规规则优先。成本模型只能影响业务层面的阈值,不能突破法律和安全底线。
8.3 跨团队协作机制
风控不是一个纯技术问题,需要业务、算法、研发、客服、法务共同参与:
- 业务侧提供成本参数,比如真实用户的生命周期价值、流失率、客单价。
- 算法侧提供模型分数和特征解释。
- 研发侧保证决策链路稳定和可回滚。
- 客服侧反馈误杀申诉数据,反向驱动阈值调整。
- 法务和合规侧划定不能触碰的红线。
这套机制建立起来之后,风控系统的目标就不再是“零欺诈”,而是“在合规前提下,让业务总成本最低”。
9. 总结与下一步
回到《The optimal amount of fraud is non-zero》这个观点,它真正的价值是给出了一个可操作的工程判断框架:最优欺诈率不是 0,最优阈值也不是模型精度的产物,而是成本参数的产物。
建议你先做三件事:
第一,把当前风控决策的四个结果——正确放行、正确拦截、漏过欺诈、误杀正常用户——用成本量化出来。哪怕一开始只是拍一个粗略数值,都比完全没有强。
第二,用 Python 脚本把你历史交易数据做成一张“阈值 vs 总代价”的曲线图,找到当前系统的最优阈值在哪里,再对比现行的阈值差多少。
第三,把成本参数配置化。不要写在代码里,而是做成配置项,这样业务侧调整成本参数时,不需要重新发版模型和代码。
最容易踩的坑是:把“最优欺诈率非零”理解成“欺诈可以放着不管”。它恰恰要求你更精细地管理风险,只是管理目标从“最低欺诈率”换成了“最低业务总代价”。
后续可以继续扩展的方向包括:成本敏感学习、强化学习动态调阈值、基于用户分层差异化决策、批量扫描报告自动化、以及风控模型上线后的漂移监控体系。先把成本这个维度引入系统,后续每一步优化会顺很多。