ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

AUC、ROC、ACC区别:分类模型评估指标与阈值选择指南

AUC、ROC、ACC区别:分类模型评估指标与阈值选择指南

前阵子帮一个做信贷评分的朋友复盘模型,他发过来的评估报告上写着 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 个样本,模型输出的“正类概率”和真实标签如下:

样本分数真实标签
A0.90正
B0.80正
C0.75负
D0.70正
E0.60负
F0.55正
G0.45负
H0.40正
I0.30负
J0.20负

正样本 5 个(A、B、D、F、H),负样本 5 个(C、E、G、I、J)。所以每次算 TPR 分母是 5,FPR 分母也是 5。

阈值从高往低走,逐个把样本纳入“判为正”:

阈值判为正的样本TPFPTPRFPR
> 0.90无000.00.0
≥ 0.90A100.20.0
≥ 0.80A,B200.40.0
≥ 0.75A,B,C210.40.2
≥ 0.70A,B,C,D310.60.2
≥ 0.60A,B,C,D,E320.60.4
≥ 0.55A,B,C,D,E,F420.80.4
≥ 0.45…+G430.80.6
≥ 0.40…+H531.00.6
≥ 0.30…+I541.00.8
≥ 0.20全部551.01.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.20.40.40.2 × 0.4 = 0.08
0.2 → 0.40.60.60.2 × 0.6 = 0.12
0.4 → 0.60.80.80.2 × 0.8 = 0.16
0.6 → 0.81.01.00.2 × 1.0 = 0.20
0.8 → 1.01.01.00.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.200.300.400.450.550.600.700.750.800.90
标签负负正负正负正负正正
秩12345678910

正样本秩次和 = 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 个样本实测一下:

阈值TPFPTNFNACC
0.8510540.60
0.7231420.70
0.5042310.70
0.4243210.60
0.3553200.70
0.2554100.60

ACC 在 0.5 到 0.85 这一带都是 0.70,最高点不唯一。这在小样本上很常见,样本大了之后曲线会平滑,但“最优阈值≠0.5”这件事在真实数据里几乎必然发生。

所以默认threshold=0.5是个工程习惯,不是业务决策。sklearn 的model.predict()内部就是拿 0.5 去切的(二分类),很多人用了几年都没意识到这一点,以为 predict 输出的是“模型的判断”,其实那只是“模型在 0.5 这个任意选择的工作点上的判断”。

阈值该怎么选?我一般走这条路:

  1. 先明确业务约束。是“误报总量有上限”(比如每天最多 300 个工单)还是“漏报率有上限”(比如召回率不得低于 85%)?约束不同,优化方向完全相反。
  2. 在验证集上扫描阈值,画出 ACC、Precision、Recall、F1 随阈值变化的曲线(precision_recall_curve可以一次拿到)。
  3. 带上代价矩阵。如果漏一个正样本损失 100 元、误报一个损失 5 元,那期望代价 = 100 × FN + 5 × FP,直接按这个式子扫阈值,找最小值。这比调 F1 靠谱得多,因为 F1 隐含了“精确率和召回率同等重要”的假设,而现实中很少成立。
  4. 在独立测试集上验一次。千万别在测试集上选阈值——那等于用测试集调参,指标会虚高。选阈值只能用验证集或交叉验证的 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给正负类等权,不再按样本数加权两类都重要、但数量悬殊
F12PR/(P+R)用调和平均压住极端值,兼顾精确率与召回率关注正类、需要一个综合分
Fβ带 β 的调和平均,β>1 偏召回,β<1 偏精确可以调节对召回/精确的偏好有明确的偏好方向
MCC混淆矩阵的相关系数形式四个格子都进入分子分母,不平衡下最稳健的单指标想要一个“不会骗人”的总分
Cohen's Kappa扣除随机一致率后的准确率扣掉了“瞎猜也能蒙对”的那部分多分类、需要对比随机基线
PR-AUC / APPR 曲线下面积对负样本数量敏感,聚焦正类正样本极稀少

其中 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 数字,信息量为零。我在做模型验收时,至少要看到这一组:

  1. 样本量与标签分布(正样本数、负样本数、正样本占比)
  2. 混淆矩阵原始计数(不是只有比例,要看绝对数)
  3. ACC + Balanced ACC + F1(或 MCC)
  4. ROC-AUC + PR-AUC
  5. 选定阈值,以及这个阈值是怎么选出来的
  6. 分群指标(按渠道、按时间段、按用户分层各看一遍)

第 6 条最容易被忽略,但它救过我两次。一次是整体 ACC 0.91、看起来很稳,按月份拆开发现最近一个月掉到 0.74,原因是上游某个字段的口径改了。另一次是按新老用户拆开,新用户群体的召回率只有整体的六成,说明模型严重依赖历史行为特征,对新用户根本不适用。整体指标永远不会告诉你这些。

5. 三者对比与选型速查

5.1 一张表看清本质差异

维度ACCROCAUC
本质某个阈值下的判决正确率全阈值区间的 (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,这种小事的成本很低,收益是后面半年没人再为指标含义吵架。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进