前阵子帮一个做信贷评分的朋友复盘模型,他发过来的评估报告上写着 ACC 0.97,语气相当轻松,觉得这版可以直接上线了。我让他把混淆矩阵和标签分布一起贴出来,结果正样本占比不到 2%,也就是说一个把所有样本都判成“负”的模型也能拿到 98% 的 ACC。那一刻他盯着屏幕沉默了半分钟,然后把报告里的指标全换了一遍。
类似的场面我遇到过太多次。AUC、ROC、ACC这三个词在教科书里挨着出现,在代码里挨着调用,在报告里挨着写,于是很多人默认它们是同一件事的三种说法,只是精度不同。实际上它们压根不在一个维度上:一个描述阈值确定之后的判决质量,一个描述阈值连续滑动时模型的表现轨迹,还有一个描述这条轨迹围出来的面积,也就是纯粹的排序能力。搞不清楚这一层,就会出现“离线指标很漂亮、上线就翻车”或者“两个模型指标差 0.002 却纠结一整天”这类典型场面。
这篇东西想做的事很具体:把 AUC、ROC、ACC 三者从定义、计算方式到适用边界彻底拆开,配上能手算、能复现的小例子,把参数怎么选、阈值怎么定、报告怎么写全部落到操作层面。刚接触分类模型评估的新手可以照着走一遍,做过几个项目但一直被指标误导的同行,应该能在里面找到几处自己踩过的坑。我会尽量少讲抽象定义,多讲“这个东西算出来之后到底能说明什么、不能说明什么”。
1. 先把三个词放回它们该在的位置
1.1 三者不是同一个维度上的东西
很多 confusion 的根源在于,把 ACC 和 AUC 当成“两种准确率”来比较,就像拿体重和身高比大小。我习惯用下面这套说法来区分:
ACC 是一次判决的成绩单。你必须先给定一个阈值(比如概率大于 0.5 判为正),模型才能对每个样本输出“正/负”,然后才有准确率可算。换一个阈值,ACC 立刻变。所以 ACC 天然带着“阈值依赖”这个属性,它衡量的是“在某个特定工作点上,模型判对了多少比例”。
ROC 是一份体检报告的全景扫描。它不锁定单一阈值,而是把阈值从高到低滑一遍,每滑一次记录两个数字:真正例率(TPR)和假正例率(FPR),把这些点连成一条曲线。曲线上的每一个点都对应一个可能的阈值,也就对应一种可能的业务策略。
AUC 是这份全景扫描的一个汇总分数。曲线下的面积,把整条 ROC 压缩成一个 0 到 1 之间的数。它衡量的是模型把正样本排在负样本前面的能力,跟阈值完全无关。
提示:判断一个指标是否“阈值无关”,最直接的方法是问一句——我把判决阈值从 0.3 改到 0.7,这个数会变吗?ACC 会变,AUC 不会变(前提是测试集和分数都不变),ROC 曲线本身也不变,只是你选中的工作点变了。
这个区分决定了三者的使用场合:AUC 用于模型选型和排序能力比较,ROC 用于观察全阈值区间的表现和挑选工作点,ACC 用于汇报某个已确定策略下的最终效果。用 AUC 去汇报业务效果,等于说“我这个模型理论上挺聪明但还没决定怎么用”;用 ACC 去选模型,等于“我用一个随机挑的阈值来代表模型的全部能力”,都不合适。
1.2 从混淆矩阵出发:ACC 和 ROC 的共同祖先
三者全部来自同一个 2×2 的混淆矩阵,只是取用的格子不同。二分类下,把正类记作 1、负类记作 0:
| 预测为正 | 预测为负 | |
|---|---|---|
| 真实为正 | TP(真正例) | FN(假负例) |
| 真实为负 | FP(假正例) | TN(真负例) |
四个格子加起来是样本总数 N = TP + FP + FN + TN。围绕这四个格子,所有指标都是某种比值:
- ACC = (TP + TN) / N,对角线之和除以总数。它同时关心正类和负类,但按样本数量加权——这是它最大的优点,也是它最大的问题。
- TPR(召回率/灵敏度)= TP / (TP + FN),正样本里被抓到的比例。分母只有正样本。
- FPR = FP / (FP + TN),负样本里被误伤的比例。分母只有负样本。
- Precision(精确率)= TP / (TP + FP),判为正的里面有多少真的正。分母是预测为正的全体。
- TNR(特异度)= TN / (FP + TN) = 1 - FPR。
注意 TPR 和 FPR 的分母各自独立,一个是正样本总数,一个是负样本总数。这个设计不是随手写的,它直接导致了 ROC 对类别比例不敏感这个特性——后面第 3 节会专门讲这个特性的好处和陷阱。
还有个容易混的点:ROC 的横轴是 FPR 不是 TNR,纵轴是 TPR 不是 Precision。我见过报告把 PR 曲线和 ROC 曲线的坐标轴写混的,也见过把“精确率-召回率曲线”直接叫成 ROC 的。前者横轴是 Recall、纵轴是 Precision,与 ROC 完全两回事,尤其在正样本稀少时两条曲线的形状可以差得非常远。
1.3 一个例子说明 ACC 为什么会骗人
拿开头那个场景量化一下。假设测试集 10000 条,正样本 200 条(2%),负样本 9800 条。
模型 A 是个“摆烂模型”,对所有样本输出负类。混淆矩阵是 TP=0、FN=200、FP=0、TN=9800。
- ACC = (0 + 9800) / 10000 =0.98
- TPR = 0 / 200 =0
- FPR = 0 / 9800 =0
- AUC =0.5(所有样本分数相同,排序能力等于随机)
一个完全没用的模型,ACC 有 98%。这就是 ACC 在类别不平衡下的失效机制:它把每个样本的权重都当成 1,于是 9800 个容易判对的负样本,把 200 个难判的正样本彻底淹没了。你优化 ACC,模型学到的最优策略就是“全部判负”,因为这样最省事、最不容易错。
模型 B 稍微做点事,它抓住了 60% 的正样本,代价是误伤 5% 的负样本:TP=120、FN=80、FP=490、TN=9310。
- ACC = (120 + 9310) / 10000 =0.943
- TPR = 120 / 200 =0.60
- FPR = 490 / 9800 =0.05
模型 B 的 ACC 比摆烂模型低,但它实际有用得多——它找出了六成坏客户。如果按 ACC 排序,你会选错模型。而 AUC 呢,模型 B 的 AUC 大概在 0.85 上下,模型 A 是 0.5,差距一目了然。
注意:这不是说 ACC 没用,而是说ACC 隐含了一个假设——正负样本的错判代价相同、且两类样本数量大致均衡。现实业务里这两个假设基本都不成立。欺诈检测里漏掉一笔欺诈的损失可能是误拦一笔正常交易的几十倍;医疗筛查里漏诊和误诊的代价更不对等。代价不对称时,ACC 天生就不是合适的优化目标。
2. ROC 曲线是怎么一笔一笔画出来的
2.1 TPR 和 FPR 的业务含义
在动手算之前,先把这两个轴的语义钉死。我一般这样跟业务方解释:
把模型对每个样本输出的分数从高到低排列,从上往下取前 K 个判为正,其余判为负。K 从 0 慢慢增加到 N,每变一次 K,就得到一组 (FPR, TPR)。
- 纵轴 TPR:在“所有真正的好东西”里,你捞到了多少。TPR=0.8 意味着 80% 的正样本被你的规则圈进来了。业务上这就是“覆盖率”“召回率”“查全率”。
- 横轴 FPR:在“所有真正的坏东西/无关东西”里,你误伤了多少。FPR=0.05 意味着 5% 的负样本被错误地圈了进来。业务上就是“误报率”“打扰率”。
所以 ROC 曲线描述的是一场交易:你想多捞正样本(往上走),就得接受更多误伤(往右走)。曲线越靠近左上角,说明在同样的误伤水平下能捞到更多正样本,模型越好。左上角那个点 (0,1) 是理想点——一个都不误伤、全部捞到。右下角的 (1,1) 是“全判正”。左下角 (0,0) 是“全判负”。对角线是从 (0,0) 到 (1,1) 的直线,代表随机猜。
这个“交易”的比喻很重要,因为业务决策本质就是在这条曲线上选一个点:你能容忍多少误伤?如果客服团队一天只能处理 500 个预警,那你就得反推出对应的 FPR,然后在曲线上找那个点,读它的 TPR。这就是阈值选择的真实过程,而不是拍脑袋定 0.5。
2.2 手算一遍:10 个样本的完整过程
光看公式容易飘,拿一组小数据从头算到尾。10 个样本,模型输出的“正类概率”和真实标签如下:
| 样本 | 分数 | 真实标签 |
|---|---|---|
| A | 0.90 | 正 |
| B | 0.80 | 正 |
| C | 0.75 | 负 |
| D | 0.70 | 正 |
| E | 0.60 | 负 |
| F | 0.55 | 正 |
| G | 0.45 | 负 |
| H | 0.40 | 正 |
| I | 0.30 | 负 |
| J | 0.20 | 负 |
正样本 5 个(A、B、D、F、H),负样本 5 个(C、E、G、I、J)。所以每次算 TPR 分母是 5,FPR 分母也是 5。
阈值从高往低走,逐个把样本纳入“判为正”:
| 阈值 | 判为正的样本 | TP | FP | TPR | FPR |
|---|---|---|---|---|---|
| > 0.90 | 无 | 0 | 0 | 0.0 | 0.0 |
| ≥ 0.90 | A | 1 | 0 | 0.2 | 0.0 |
| ≥ 0.80 | A,B | 2 | 0 | 0.4 | 0.0 |
| ≥ 0.75 | A,B,C | 2 | 1 | 0.4 | 0.2 |
| ≥ 0.70 | A,B,C,D | 3 | 1 | 0.6 | 0.2 |
| ≥ 0.60 | A,B,C,D,E | 3 | 2 | 0.6 | 0.4 |
| ≥ 0.55 | A,B,C,D,E,F | 4 | 2 | 0.8 | 0.4 |
| ≥ 0.45 | …+G | 4 | 3 | 0.8 | 0.6 |
| ≥ 0.40 | …+H | 5 | 3 | 1.0 | 0.6 |
| ≥ 0.30 | …+I | 5 | 4 | 1.0 | 0.8 |
| ≥ 0.20 | 全部 | 5 | 5 | 1.0 | 1.0 |
把这些点连起来,就是这条 ROC。可以看到它是一段一段的“台阶”——每次纳入一个正样本,曲线往上跳一格;纳入一个负样本,往右跳一格。样本少的时候曲线非常粗糙,这也解释了为什么小测试集上的 AUC 波动会很大。
顺便验证一下ACC 在这个数据上的表现。取阈值为 0.5,判为正的是 A、B、D、F(分数 0.90、0.80、0.70、0.55),H 的 0.40 被漏掉。于是 TP=4、FN=1、FP=2(C 和 E)、TN=3。
ACC = (4+3)/10 =0.7。而这条曲线的 AUC 是 0.76(下一节算)。两个数字不仅大小不同,描述的东西也完全不同。
2.3 代码复现与画图时的两个坑
同样这批数据,用 scikit-learn 跑一遍,几行就够:
import numpy as np from sklearn.metrics import roc_curve, roc_auc_score, accuracy_score, confusion_matrix y_true = np.array([1, 1, 0, 1, 0, 1, 0, 1, 0, 0]) y_score = np.array([0.9, 0.8, 0.75, 0.7, 0.6, 0.55, 0.45, 0.4, 0.3, 0.2]) fpr, tpr, thresholds = roc_curve(y_true, y_score) print("AUC =", roc_auc_score(y_true, y_score)) # 0.76 y_pred = (y_score >= 0.5).astype(int) print("ACC =", accuracy_score(y_true, y_pred)) # 0.7 print(confusion_matrix(y_true, y_pred)) # [[3 2] # [1 4]]代码里有两个非常高频的坑,我几乎每次做代码评审都会碰到。
坑一:把概率直接喂给 accuracy_score。accuracy_score期望的是标签(0/1),你传 0.87 这样的浮点数进去,它照样能跑,只是会把每个非零值当正类处理,算出个莫名其妙的数。正确做法是自己按业务阈值二值化,或者用predict而不是predict_proba。我建议把二值化那一步写成显式的一行(y_score >= thr).astype(int),这样阈值是多少、在哪一步生效,一眼看得见。
坑二:roc_curve的drop_intermediate参数。这个参数默认是 True,它会把共线的中间点丢掉,只保留“拐点”。这对画图是好事(线条更干净),但对数值核对是坏事——你手算出一堆点,发现和代码返回的thresholds长度对不上,就怀疑自己算错了。做手工验证时把它设成drop_intermediate=False,两边的点就能一一对上。我第一次被这个参数坑的时候,对着屏幕核了二十分钟,最后发现是函数在帮我“优化”。
画图还有一个实操建议:ROC 曲线一定要标注出业务选定工作点,否则这张图在评审会上只是一张好看的装饰画。用plt.scatter把选定阈值的 (FPR, TPR) 点标出来,旁边写上“阈值=0.62,覆盖 78%,误伤 6.5%”,团队才能就着这张图讨论策略,而不是讨论“这个曲线挺漂亮的”。
3. AUC 到底是什么:从面积到排序能力
3.1 两种算法:梯形积分和秩和统计
AUC 最直白的定义就是 ROC 曲线下的面积。因为 ROC 的横纵轴都在 [0,1] 区间,所以 AUC 也必然落在 [0,1] 内。用上面那张表里的点,按 FPR 从小到大遍历,相邻点之间做梯形积分:
| FPR 区间 | TPR 起点 | TPR 终点 | 面积 |
|---|---|---|---|
| 0.0 → 0.2 | 0.4 | 0.4 | 0.2 × 0.4 = 0.08 |
| 0.2 → 0.4 | 0.6 | 0.6 | 0.2 × 0.6 = 0.12 |
| 0.4 → 0.6 | 0.8 | 0.8 | 0.2 × 0.8 = 0.16 |
| 0.6 → 0.8 | 1.0 | 1.0 | 0.2 × 1.0 = 0.20 |
| 0.8 → 1.0 | 1.0 | 1.0 | 0.2 × 1.0 = 0.20 |
累加得0.76。注意因为 TPR 在每一段里是平的(台阶状),梯形退化成了矩形,直接乘就行。样本多了之后点会密集,梯形公式才真正发挥作用。
但面积只是几何解释,真正有用的是 AUC 的概率解释:随机抽一个正样本、一个负样本,模型给正样本的分数高于负样本分数的概率,就是 AUC。这个解释解决了“0.76 到底算好还是不好”的问题——0.76 意味着在 76% 的正负样本对里,模型的排序是对的。
由此还有第三种算法,秩和法,也叫 Mann-Whitney U 统计量:
AUC = (所有正样本的秩次之和 - n_pos × (n_pos + 1) / 2) / (n_pos × n_neg)把上面 10 个样本按分数从小到大排名(分数最低的秩为 1):
| 分数 | 0.20 | 0.30 | 0.40 | 0.45 | 0.55 | 0.60 | 0.70 | 0.75 | 0.80 | 0.90 |
|---|---|---|---|---|---|---|---|---|---|---|
| 标签 | 负 | 负 | 正 | 负 | 正 | 负 | 正 | 负 | 正 | 正 |
| 秩 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
正样本秩次和 = 3 + 5 + 7 + 9 + 10 = 34,n_pos = 5,n_neg = 5。
AUC = (34 − 5×6/2) / (5×5) = (34 − 15) / 25 =0.76。
和梯形法完全一致。这个一致性不是巧合,是数学上的必然。手写这套算法看起来是在做无用功,但它有一个实际价值:当 AUC 出现异常值时,秩和法能帮你快速定位问题。比如 AUC 突然从 0.82 掉到 0.31,你可以立刻想到“这是不是标签反了”——因为 AUC = 1 − 原值 恰好说明排序完全颠倒。我遇到过一次线上 AUC 崩到 0.28,排查了半天,最后发现是标签映射的枚举写反了,0 和 1 的含义在两个模块里不一致。
顺便记两个换算关系,评审会上经常用到:
- Gini 系数 = 2 × AUC − 1。AUC 0.76 对应 Gini 0.52。风控和营销领域用 Gini 的比较多,看到别的团队的 Gini 报告别懵。
- KS 统计量 = max(TPR − FPR)。在上面那张表里逐个算:0−0=0、0.2−0=0.2、0.4−0=0.4、0.4−0.2=0.2、0.6−0.2=0.4、0.6−0.4=0.2、0.8−0.4=0.4、0.8−0.6=0.2、1.0−0.6=0.4、1.0−0.8=0.2、1.0−1.0=0。所以 KS 最大是 0.4,出现在好几个阈值上。KS 只取最大值那一个点,信息量比 AUC 少,但对业务方来说更直观——“最好情况能把好坏样本拉开 40 个百分点的差距”。
3.2 AUC 的三个常见误解
误解一:AUC 高就代表模型好。这话只在“排序能力”这个层面成立。AUC 完全不关心概率的绝对数值。一个模型把正样本打成 0.51、负样本打成 0.49,另一个模型把正样本打成 0.99、负样本打成 0.01,只要相对顺序一致,AUC 可能一模一样。但后者的概率是校准的,前者不是。如果你的业务需要用到概率的绝对值——比如按概率定价、按期望损失排序、算风险敞口——那光看 AUC 会出大问题,必须补上**校准曲线(reliability curve)**和Brier score,或者做 Platt scaling / isotonic regression 校准。
误解二:AUC 对类别不平衡免疫,所以不平衡场景用 AUC。前半句对,后半句错。ROC 曲线确实对类别比例不敏感——因为 TPR 和 FPR 各自在自己的类内归一化,你把正样本从 200 个复制成 2000 个,只要分数分布不变,ROC 曲线纹丝不动。但这个“优点”在极端不平衡时会变成缺点:当正样本极少时,FPR 的分母(负样本数)巨大,一点点 FPR 的变化就对应大量的误报,而 ROC 曲线在左上角区域被极度压缩,肉眼看不出差异。这时候应该切到 PR 曲线,因为 Precision = TP/(TP+FP) 的分母直接包含 FP,对误报数量敏感得多。
经验上的分界线:正样本占比低于 5%~10% 时,我基本都会同时看 ROC-AUC 和 PR-AUC,如果两者结论冲突,以 PR-AUC 为准。别只报一个 ROC-AUC 就完事,那样等于把最有信息量的部分藏起来了。
误解三:AUC 差 0.001 说明模型 B 更好。在小测试集上,AUC 的置信区间可能宽到 ±0.03。1000 个样本、正样本 100 个的情况下,AUC 的标准误会大到让 0.001 的差异完全淹没在噪声里。要么用 bootstrap 算置信区间,要么用 DeLong 检验做配对比较,至少也要报“均值 ± 标准差”而不是一个裸数字。我见过团队为了 0.002 的 AUC 差异争论两周,最后做完 bootstrap 发现两个模型的置信区间重叠了 90%。
3.3 多分类场景下 AUC 怎么算
二分类的 AUC 好理解,一到多分类就乱了,因为“曲线下的面积”这个概念没法直接推广。常见的三种做法:
| 方式 | 做法 | 适用场景 | 注意点 |
|---|---|---|---|
| One-vs-Rest (OvR) + macro | 每个类当一次正类,其余合并为负类,各类 AUC 求算术平均 | 类别重要性相当 | 少数类的 AUC 方差大,会把平均值拖偏 |
| One-vs-Rest + weighted | 同上,但按每类样本数加权平均 | 类别不平衡、关注整体 | 结果会被多数类主导,接近二分类直觉 |
| One-vs-One (OvO) | 每两个类之间算一次 AUC,再平均 | 类别间容易混淆、需要细粒度对比 | 类别多时计算量是 O(K²),K=10 就是 45 对 |
scikit-learn 里对应roc_auc_score(y_true, y_score, multi_class='ovr', average='macro'),其中y_score必须是predict_proba输出的概率矩阵,形状是 (n_samples, n_classes),且列的顺序要和classes_对齐。
这里有个非常隐蔽的坑:多分类时如果传硬的预测标签进去,roc_auc_score会报错或者给出完全错误的数。我见过有人在多分类任务上手写了 OvR 循环,但每一轮都传model.predict()的结果而不是概率,算出来的 AUC 全是 0.5 附近,排查了半天以为是模型没学到东西。记住:AUC 的输入必须是连续的分数或概率,不能是离散标签。
还有一个实践建议:多分类报告里,除了平均 AUC,一定要把每个类的 AUC 单独列出来。平均 0.92 看着很漂亮,但如果某一类的 AUC 只有 0.61,那这个类在实际使用中就是个黑洞。我在一个 8 分类的文本打标项目里就吃过这个亏——整体 macro AUC 0.89,上线后某一个类的准确率惨不忍睹,回头单独拆开看才发现它的 AUC 是 0.58,被其他几个 0.95+ 的类平均掉了。
4. ACC 的正确打开方式
4.1 阈值在哪里,ACC 就在哪里
ACC 最大的特性是完全跟随阈值。同一批预测分数,阈值从 0.1 扫到 0.9,ACC 会画出一条先升后降的曲线。这条曲线的最高点对应的阈值,就是“让 ACC 最大的阈值”,但它通常不等于 0.5,尤其在类别不平衡的时候。
用第 2 节那 10 个样本实测一下:
| 阈值 | TP | FP | TN | FN | ACC |
|---|---|---|---|---|---|
| 0.85 | 1 | 0 | 5 | 4 | 0.60 |
| 0.72 | 3 | 1 | 4 | 2 | 0.70 |
| 0.50 | 4 | 2 | 3 | 1 | 0.70 |
| 0.42 | 4 | 3 | 2 | 1 | 0.60 |
| 0.35 | 5 | 3 | 2 | 0 | 0.70 |
| 0.25 | 5 | 4 | 1 | 0 | 0.60 |
ACC 在 0.5 到 0.85 这一带都是 0.70,最高点不唯一。这在小样本上很常见,样本大了之后曲线会平滑,但“最优阈值≠0.5”这件事在真实数据里几乎必然发生。
所以默认threshold=0.5是个工程习惯,不是业务决策。sklearn 的model.predict()内部就是拿 0.5 去切的(二分类),很多人用了几年都没意识到这一点,以为 predict 输出的是“模型的判断”,其实那只是“模型在 0.5 这个任意选择的工作点上的判断”。
阈值该怎么选?我一般走这条路:
- 先明确业务约束。是“误报总量有上限”(比如每天最多 300 个工单)还是“漏报率有上限”(比如召回率不得低于 85%)?约束不同,优化方向完全相反。
- 在验证集上扫描阈值,画出 ACC、Precision、Recall、F1 随阈值变化的曲线(
precision_recall_curve可以一次拿到)。 - 带上代价矩阵。如果漏一个正样本损失 100 元、误报一个损失 5 元,那期望代价 = 100 × FN + 5 × FP,直接按这个式子扫阈值,找最小值。这比调 F1 靠谱得多,因为 F1 隐含了“精确率和召回率同等重要”的假设,而现实中很少成立。
- 在独立测试集上验一次。千万别在测试集上选阈值——那等于用测试集调参,指标会虚高。选阈值只能用验证集或交叉验证的 out-of-fold 预测。
import numpy as np from sklearn.metrics import f1_score # y_val: 验证集标签; s_val: 验证集预测概率 best_thr, best_f1 = 0.5, -1 for thr in np.arange(0.05, 0.96, 0.01): pred = (s_val >= thr).astype(int) f1 = f1_score(y_val, pred) if f1 > best_f1: best_thr, best_f1 = thr, f1 print(f"最优阈值={best_thr:.2f}, F1={best_f1:.4f}")这段代码简单,但有一个必须提醒的点:如果验证集和线上分布的偏移很大,验证集上选出的阈值上线后会失效。我一般会在上线后做一周的阈值监控,把线上实际分数的分位数打出来,跟离线验证集的分位数对比。如果 P50 从 0.18 漂到 0.35,那阈值基本要重调,因为模型输出的整体水位变了。
4.2 不平衡场景下,ACC 的替代品清单
ACC 在均衡数据上没毛病,问题只出在不平衡和代价不对称。这时候可以换用下面这些指标,我把它们各自“修正”了什么列清楚:
| 指标 | 计算方式 | 修正了什么 | 什么时候用 |
|---|---|---|---|
| Balanced Accuracy | (TPR + TNR) / 2 | 给正负类等权,不再按样本数加权 | 两类都重要、但数量悬殊 |
| F1 | 2PR/(P+R) | 用调和平均压住极端值,兼顾精确率与召回率 | 关注正类、需要一个综合分 |
| Fβ | 带 β 的调和平均,β>1 偏召回,β<1 偏精确 | 可以调节对召回/精确的偏好 | 有明确的偏好方向 |
| MCC | 混淆矩阵的相关系数形式 | 四个格子都进入分子分母,不平衡下最稳健的单指标 | 想要一个“不会骗人”的总分 |
| Cohen's Kappa | 扣除随机一致率后的准确率 | 扣掉了“瞎猜也能蒙对”的那部分 | 多分类、需要对比随机基线 |
| PR-AUC / AP | PR 曲线下面积 | 对负样本数量敏感,聚焦正类 | 正样本极稀少 |
其中 MCC(Matthews 相关系数)值得单独说一句。它的取值范围是 [−1, 1],0 表示和随机猜一样,1 表示完美预测,−1 表示完全反着预测。它的好处是任何一个格子的异常都会拉低它:如果模型把所有样本判成正类,FP 会爆炸,MCC 直接掉到 0 附近,而 ACC 可能还有 2%。所以在“只能报一个数”的场合,我更愿意报 MCC 而不是 ACC。
注意:Balanced Accuracy 在 sklearn 里是
balanced_accuracy_score,别自己手算成 (Recall + Precision)/2,那是错的。正确形式是 (TPR + TNR)/2,也就是 (Recall + Specificity)/2。
4.3 报告里 ACC 应该跟谁一起出现
单独一个 ACC 数字,信息量为零。我在做模型验收时,至少要看到这一组:
- 样本量与标签分布(正样本数、负样本数、正样本占比)
- 混淆矩阵原始计数(不是只有比例,要看绝对数)
- ACC + Balanced ACC + F1(或 MCC)
- ROC-AUC + PR-AUC
- 选定阈值,以及这个阈值是怎么选出来的
- 分群指标(按渠道、按时间段、按用户分层各看一遍)
第 6 条最容易被忽略,但它救过我两次。一次是整体 ACC 0.91、看起来很稳,按月份拆开发现最近一个月掉到 0.74,原因是上游某个字段的口径改了。另一次是按新老用户拆开,新用户群体的召回率只有整体的六成,说明模型严重依赖历史行为特征,对新用户根本不适用。整体指标永远不会告诉你这些。
5. 三者对比与选型速查
5.1 一张表看清本质差异
| 维度 | ACC | ROC | AUC |
|---|---|---|---|
| 本质 | 某个阈值下的判决正确率 | 全阈值区间的 (FPR, TPR) 轨迹 | ROC 曲线下面积 / 排序正确概率 |
| 是否依赖阈值 | 强依赖 | 不依赖(本身就是阈值的函数) | 不依赖 |
| 取值范围 | [0, 1] | 平面曲线 | [0, 1],0.5 为随机 |
| 对类别不平衡 | 敏感,会被多数类主导 | 不敏感(类内归一化) | 不敏感(同 ROC) |
| 是否关心概率校准 | 只关心阈值两侧的排序 | 只关心排序 | 完全不关心 |
| 计算成本 | O(N) | O(N log N) 排序 | O(N log N) |
| 主要用途 | 汇报既定策略的最终效果 | 观察全区间表现、挑选工作点 | 模型选型、排序能力比较 |
| 典型误用 | 用它选模型 | 把它当指标数值汇报 | 用它证明业务收益 |
一句话总结这三者的分工:AUC 回答“这个模型有没有排序能力”,ROC 回答“在哪个工作点上用最合适”,ACC 回答“按这个工作点用,能判对多少”。顺序不能反——先有排序能力,才谈得上选工作点,最后才有准确率可报。
5.2 按场景选指标
场景一:正负样本均衡、错判代价对称。比如一个内容质量二分类(优质/低质),两类各占一半,标错一个的代价差不多。这种场景最简单,ACC、F1、AUC 结论通常一致,报 ACC 加 AUC 就够了。判断标准是:如果你把阈值从 0.5 挪到 0.4,ACC 掉不超过 2 个点,说明数据确实均衡,ACC 可信。
场景二:极端不平衡、只关心正类。比如故障检测、异常交易识别,正样本占比 1% 甚至更低。这时候 ACC 基本可以扔掉,主指标用PR-AUC 或 Recall@固定误报率。后者的形式是“在 FPR ≤ 0.5% 的前提下,TPR 能达到多少”,这个表达业务方一听就懂,因为它直接对应“每天最多误报 50 次,能抓到多少故障”。
场景三:需要排序而非判决。比如推荐排序、搜索结果排序、客户优先级排序。这种场景根本不存在“判决阈值”这回事,AUC(或者更贴切的 NDCG、MAP)才是主指标。用 ACC 去评估排序模型是彻底的错配,因为排序关心的是相对顺序,而你硬要把它切成两半。
场景四:概率数值本身要参与计算。比如按预测概率算期望收益、按违约概率定价。这种场景 AUC 不够用,必须看校准。做法是画可靠性曲线,或者直接算 Brier score,再不行就报 log loss。一个 AUC 0.85 但严重高估概率的模型,用在定价上会造成系统性亏损。
场景五:多分类、类别数多且不均衡。主指标用 macro-F1 或 balanced accuracy,辅以每类 AUC。别用整体 ACC,会被最大的类主导。如果一个 10 分类任务里最大的类占 70%,全判成这个类就有 70% 的 ACC,这个数字毫无意义。
5.3 三种典型的错误组合
错误组合一:用 ACC 挑模型,用 AUC 汇报效果。这是最常见的一种。选模型的时候看 ACC(受阈值和类别比例双重污染),汇报的时候换成 AUC(看起来更高更漂亮)。两个数字来自不同的口径,整个评估链条是断的。正确做法是选型和汇报用同一套指标,选型看 AUC + PR-AUC,汇报时同时给出“模型能力(AUC)”和“落地效果(选定阈值下的 ACC / Recall / 误报率)”。
错误组合二:AUC 涨了就说模型变好了。AUC 涨只说明排序能力提升,但如果业务是按固定阈值判决的,AUC 从 0.82 涨到 0.84 可能完全不影响那个工作点上的表现——因为曲线可能只是在某个你根本不用的区间鼓起来了。我一般会额外看“工作点附近的局部 AUC”,或者直接看固定 FPR 下的 TPR 提升了多少,这才是业务真正感知到的变化。
错误组合三:拿不同测试集上的 AUC 互相比较。两个模型在两份不同的测试集上 AUC 分别是 0.85 和 0.83,你就说第一个好——这个结论不成立,因为测试集难度不同。要么在同一份测试集上比,要么用同一套切分逻辑做交叉验证比均值。更严谨的做法是固定测试集、固定随机种子,做配对 bootstrap。
6. 常见问题与排查实录
6.1 AUC 很漂亮,上线效果却很差
这是被问得最多的一类问题。我把可能原因按出现频率排了个序:
| 可能原因 | 特征表现 | 排查方法 |
|---|---|---|
| 数据泄漏 | 离线 AUC 高得反常(0.98+) | 检查是否有未来信息、目标编码泄漏、ID 类特征 |
| 训练/测试切分不合理 | 交叉验证分数波动极大 | 改成分组切分或时间切分 |
| 分布漂移 | 上线首日正常,几天后下滑 | 对比线上线下特征分位数 |
| 阈值选错 | AUC 正常但业务指标差 | 看线上分数的实际分布,重选阈值 |
| 概率未校准 | 预警数量远超预期 | 画可靠性曲线,做概率校准 |
| 标签定义不一致 | 离线评估和线上反馈对不上 | 核对标签口径、观察窗口长度 |
数据泄漏是最隐蔽的一种。我在一个项目里见过 AUC 0.997 的模型,排查了两天才发现某个特征的口径是“该用户历史上是否发生过逾期”,而这个字段是在事件发生后才回填的——等于把答案直接喂给了模型。判断泄漏有个土办法:把特征一个一个单独拿来训模型,看哪个单特征的 AUC 高得离谱。正常业务特征单特征 AUC 一般不超过 0.7,如果某个特征单独就有 0.9,那它八成有问题。
分布漂移的判断也不难:把线上最近一周的预测分数做成分位数表,跟离线验证集对比。如果线上 P90 是 0.72、离线是 0.45,说明模型整体在往高分偏,阈值 0.5 会导致预警量暴增。这时候先别急着训新模型,先查上游数据管道有没有变更。
6.2 AUC 恰好 0.5 或者低于 0.5 意味着什么
AUC = 0.5 表示模型的排序能力和随机猜一样。常见原因有三个:模型压根没学到东西(特征全是噪声、学习率设置错误、标签和特征完全无关)、输入被破坏(特征全被填成 0、标准化时除以了 0)、评估代码写错(传了硬标签、正负类标签顺序反了)。
AUC < 0.5 反而更有意思——它说明模型的排序是系统性反着的。这几乎不可能是“模型太差”,因为随机模型的 AUC 会围绕 0.5 波动,不会稳定在 0.3。出现这种情况,第一反应应该是查标签映射:训练时把 1 当正类、评估时把 0 当正类,或者LabelEncoder的字母序把 “negative” 排在了 “positive” 前面。我用pd.factorize和LabelEncoder混用过一次,就遇到过这个坑。
顺手记一个诊断技巧:如果 AUC 稳定在 0.2~0.3 之间,用1 - AUC算一下,如果立刻变成 0.7~0.8,基本可以确认是标签方向问题。改完之后如果指标恢复正常,说明模型本身没问题,纯粹是管道上的错。
6.3 ROC 还是 PR,怎么快速决定
我给一个不用纠结的判断流程:
先看正样本占比。大于 10%,用 ROC-AUC 完全够,两者结论基本一致。1% 到 10%,两个都算,如果差异不大就报 ROC-AUC(业内更通用,便于横向比较),如果差异明显就报 PR-AUC 并说明原因。低于 1%,主报 PR-AUC,ROC-AUC 只作为参考数字附在附录里。
再想一想业务问题本身:“我关心的是把好东西挑出来(正类视角)”还是“我关心的是别把坏东西放进来(负类视角)”。前者用 PR,后者用 ROC。举个例子,垃圾内容过滤关心的是“别让垃圾漏过去”,同时非常在意“别把正常内容误杀”,两边都重要,那就看 PR 曲线上的具体工作点,而不是一个汇总面积。
还有一个细节:PR 曲线的基线不是 0.5,而是正样本占比。一个正样本占 2% 的数据集,随机模型的 PR-AUC 大概是 0.02,而不是 0.5。所以看到 PR-AUC 0.35 别急着说模型差——它可能比基线高了 17 倍。这一点在汇报时必须说清楚,否则业务方会拿 ROC 的直觉去理解 PR。
6.4 阈值、样本量、置信区间:三个容易忽略的量化细节
样本量。AUC 的估计精度和正负样本对的数量有关。粗略的经验:想让 AUC 的标准误小于 0.01,测试集里正样本和负样本各至少需要几百个。如果正样本只有 30 个,AUC 的 95% 置信区间可能宽达 ±0.15,这个数字基本不能用来做模型比较。遇到正样本太少的情况,我宁可做分层 bootstrap(对正负样本分别有放回抽样,保持比例)来估计区间,也不愿意报一个裸点估计。
置信区间。最快的做法是 bootstrap:从测试集里有放回抽 N 次,每次算一个 AUC,重复 1000 次,取 2.5% 和 97.5% 分位数。
import numpy as np from sklearn.metrics import roc_auc_score rng = np.random.default_rng(42) aucs = [] n = len(y_true) for _ in range(1000): idx = rng.integers(0, n, n) if len(np.unique(y_true[idx])) < 2: continue aucs.append(roc_auc_score(y_true[idx], y_score[idx])) print(f"AUC={np.mean(aucs):.4f}, 95%CI=[{np.percentile(aucs,2.5):.4f}, {np.percentile(aucs,97.5):.4f}]")用交叉验证报告指标。单次切分的 AUC 偶然性太大,我一般报 5 折交叉验证的均值 ± 标准差。如果折间标准差超过 0.02,说明数据本身不稳定,或者切分方式有问题,这时候该去查切分而不是调模型。
6.5 排查速查表
把上面这些经验压成一张表,出问题时按行对号入座:
| 现象 | 最可能的原因 | 第一步动作 |
|---|---|---|
| ACC 高但业务反馈差 | 类别不平衡,多数类主导 | 打印标签分布与混淆矩阵 |
| AUC 高但 ACC 低 | 阈值选在了不合理的位置 | 扫一遍阈值,看 ACC 曲线 |
| AUC 0.98 以上 | 数据泄漏 | 单特征 AUC 排查 + 时间戳检查 |
| AUC 稳定低于 0.5 | 标签方向反了 | 用 1−AUC 验证 |
| 交叉验证 AUC 方差大 | 切分方式不当或样本太少 | 改分层/分组切分,或增大样本 |
| 线上线下指标差异大 | 分布漂移或特征口径变更 | 对比线上线下特征分位数 |
| 多分类 macro AUC 高但某类差 | 被表现好的类平均掉了 | 拆开看每类 AUC |
| 预警量远超预期 | 概率未校准或阈值偏移 | 画可靠性曲线,重选阈值 |
7. 一份可以直接抄的评估模板
写到这里,把上面所有讨论收进一个可复用的函数里,日常做模型验收我基本都用这个结构,改改字段名就能用。
import numpy as np from sklearn.metrics import (roc_auc_score, average_precision_score, accuracy_score, balanced_accuracy_score, f1_score, matthews_corrcoef, confusion_matrix, brier_score_loss) def evaluate(y_true, y_score, threshold=0.5, name="model"): y_pred = (y_score >= threshold).astype(int) tn, fp, fn, tp = confusion_matrix(y_true, y_pred, labels=[0, 1]).ravel() tpr = tp / (tp + fn) if (tp + fn) else 0.0 # 召回率 fpr = fp / (fp + tn) if (fp + tn) else 0.0 # 误报率 tnr = 1 - fpr return { "model": name, "threshold": threshold, "pos_ratio": round(y_true.mean(), 4), "acc": round(accuracy_score(y_true, y_pred), 4), "balanced_acc": round(balanced_accuracy_score(y_true, y_pred), 4), "f1": round(f1_score(y_true, y_pred), 4), "mcc": round(matthews_corrcoef(y_true, y_pred), 4), "recall_tpr": round(tpr, 4), "fpr": round(fpr, 4), "specificity": round(tnr, 4), "roc_auc": round(roc_auc_score(y_true, y_score), 4), "pr_auc": round(average_precision_score(y_true, y_score), 4), "brier": round(brier_score_loss(y_true, y_score), 4), "tp": int(tp), "fp": int(fp), "fn": int(fn), "tn": int(tn), }这个函数一次把 ACC、Balanced ACC、F1、MCC、TPR、FPR、ROC-AUC、PR-AUC、Brier 全部打出来,还带上混淆矩阵的原始计数和正样本占比。好处是信息完整,不会因为漏看某个维度而误判。我一般会把它套在“整体 + 各分群 + 各时间段”三个维度上各跑一遍,把结果并排放在一张表里看,很多问题就是在并排对比的时候暴露出来的。
配合一个阈值扫描,就能完整回答“这个模型该怎么用”:
for thr in [0.3, 0.4, 0.5, 0.6, 0.7]: r = evaluate(y_val, s_val, threshold=thr, name=f"thr={thr}") print(f"阈值{r['threshold']}: 召回={r['recall_tpr']}, 误报={r['fpr']}, " f"ACC={r['acc']}, F1={r['f1']}")跑完这一圈,你手里就有了一张“阈值-效果”对照表,直接拿去和业务方谈:“要 80% 召回,误报率得到 6%,一天大概 400 个工单,你们能接吗?”这比拿一个 AUC 数字去开会有效得多。
我个人在实际操作中的体会是,指标的意义从来不在于它本身的数值,而在于它能否对应到一个具体的业务动作。AUC 对应的是“要不要换模型”,ROC 曲线对应的是“阈值定在哪”,ACC 对应的是“这么干一天要处理多少单”。搞清楚每个数字最后指向哪个决策,那些关于 AUC 和 ACC 谁更重要的争论,自然就消失了。另外提醒一句,如果团队里有人把 ROC 曲线下的面积和 PR 曲线下的面积混着叫“AUC”,趁早统一口径,命名上写清楚roc_auc和pr_auc,这种小事的成本很低,收益是后面半年没人再为指标含义吵架。