做分类模型这些年,评审会上最容易吵起来的不是特征怎么选,而是"你这个 0.83 到底是 AUC 还是 ACC"。这俩数长得像、念着也像,含义差得却很远——报错一次,业务侧可能就是一批不该拒的客户被拒掉,或者一批该拦的申请被放过。AUC、ROC、ACC 这三个词,几乎每个刚接触分类模型的人第一周就会撞上,但真正能把"为什么不能只看 ACC""ROC 曲线到底是怎么一个点一个点画出来的""AUC 高是不是就等于模型好用"这三件事讲圆的人其实没那么多。很多资料一上来就甩公式,看完还是不知道它们之间的边界在哪。
这篇就把这三个指标彻底拆开。从它们共同的根——混淆矩阵——开始讲起,到 ROC 曲线怎么靠挪动阈值扫出来,再到 AUC 为什么在数学上等价于"随机抽一个正样本和一个负样本、正样本得分更高的概率"。中间我会用一个只有六个样本的小例子,把 ACC、ROC、AUC 全部手算一遍,用梯形法和排序法两条完全不同的路径算出同一个数,互相验证。最后落到代码怎么写、不同场景该看哪个指标、以及我在真实项目里踩过的那些坑。刚学完逻辑回归的同学能看懂,已经在调模型的工程师也能从里面捞到几条能直接用的经验。
1. 三个指标共同的根:混淆矩阵
1.1 四个格子决定了后面所有的故事
讲 AUC、ROC、ACC 之前,绕不开混淆矩阵。它不是某个指标专属的东西,而是所有二分类评估的地基。模型对每个样本输出一个分数,你定一个阈值,高于阈值判正类,低于阈值判负类,然后跟真实标签一比,就得到四个数:
| 预测为正 | 预测为负 | |
|---|---|---|
| 实际为正 | TP(真正例) | FN(假负例) |
| 实际为负 | FP(假正例) | TN(真负例) |
这四格里,TP 和 TN 是判对的,FP 和 FN 是判错的。注意"判错"的两种错法代价完全不同:FP 是把坏人放进来,FN 是把好人挡在外面。在反欺诈场景里,FP 可能意味着一笔坏账;在医疗筛查场景里,FN 可能意味着一个漏诊。所以后面所有指标的设计,本质上都是在回答"你更怕哪种错"。
这里有个前置动作特别容易被跳过:先明确哪一类是正类。sklearn 默认把标签里数值大的、或者字母序靠后的当正类;predict_proba返回的列顺序是按classes_排的,取错列是新手最常见的翻车点之一。正类定义反了,算出来的 AUC 会从 0.85 变成 0.15,看起来像模型崩了,其实只是你把正负号搞反了——顺便说一句,AUC 有个性质:正负类对调后,新 AUC = 1 - 原 AUC,这个性质可以用来快速自检。
1.2 ACC:最直白,也最容易被糊弄
准确率的公式简单到不需要记:ACC = (TP + TN) / (TP + TN + FP + FN),翻译成人话就是"所有样本里判对的比例"。它直观、好解释、跟业务方沟通成本最低,所以很多项目的第一版周报里只有这一个数。
问题恰恰出在"直观"上。设想一个二分类任务,负样本占 99%,正样本占 1%。我写一行return 0,把所有样本都预测成负类,ACC 立刻就是 0.99。这个"模型"没有任何判别能力,但它能在准确率排行榜上打败一堆认真调的模型。这就是类别不平衡下 ACC 的经典陷阱:它把两类错误等权相加,但两类的样本量根本不等。
再举一个更贴近实际的例子。某次我接手一个流失预警模型,前任留下的 baseline 报告上写着 ACC 0.94,看着很漂亮。我把混淆矩阵调出来一看,TP 只有 12 个,FN 有 208 个——模型几乎把所有用户都判成"不会流失"。因为这家业务的月流失率本来就只有 3% 左右,全判不流失就有 97% 的准确率。这种模型上线等于没上线,但它的 ACC 会比一个真正能抓住流失用户的模型还高。
所以 ACC 的正确用法不是"当主指标",而是"当兜底指标":在类别大致均衡、且两类错误代价接近的场景下,它是个不错的快速参考;一旦类别比例超过 7:3,就该换指标了。
1.3 ROC:一条不依赖阈值的曲线
ROC 的全称是 Receiver Operating Characteristic,受试者工作特征曲线。名字来自雷达信号检测领域,跟机器学习没什么血缘关系,后来被搬过来用了。它的构造方式其实很朴素:把阈值从高到低挪一遍,每挪一次就记录一对数,把这些点连起来。
这两个数分别是:
- 纵轴 TPR(真正例率):
TP / (TP + FN),也叫召回率、灵敏度。含义是"所有真正的正样本里,我抓到了多少比例"。 - 横轴 FPR(假正例率):
FP / (FP + TN),含义是"所有真正的负样本里,我误伤了多少比例"。
这里有个设计上的巧思值得多说一句:为什么横轴用 FPR 而不是直接用 FP 的个数?因为 FP 的绝对数量会随负样本总量膨胀——负样本从 1 万个变成 100 万个,FP 自然会变多,但模型本身没变差。除以 (FP + TN) 之后,FPR 变成了一个"比例",只跟负类内部的排序有关,跟负类有多少个无关。TPR 同理。正因为两个轴都做了这种"类内归一化",ROC 曲线才不受类别比例影响,这是它和后面要讲的 PR 曲线最根本的分野。
阈值挪到最高时,一个样本都不判正,TPR = 0、FPR = 0,对应左下角;阈值挪到最低时,全部判正,TPR = 1、FPR = 1,对应右上角。所以任何一条 ROC 曲线都是从 (0,0) 出发、到 (1,1) 结束。中间那些点,就是各种中间阈值下的取舍。一条从左下角直直连到右上角的对角线,代表"随机猜",因为 TPR 和 FPR 同步增长,你多抓一个正样本就必然多误伤一个负样本。
1.4 AUC:把一条曲线压成一个标量
ROC 曲线能看,但没法直接排序比较——两条曲线交叉的时候,谁的模型更好?说不清。于是有了 AUC(Area Under Curve),就是 ROC 曲线下的面积。取值范围 0 到 1,0.5 是随机水平,1 是完美排序。
AUC 有一个比"面积"更本质的解释,我认为这个解释才是理解它的钥匙:随机抽取一个正样本和一个负样本,模型给正样本的分数高于负样本的概率。这个定义一下子就说明了 AUC 在衡量什么——它衡量的是排序能力,不是"判对多少"。模型不需要把分数校准成真实概率,只要保证正样本普遍排在负样本前面,AUC 就高。
这个解释也直接推出了 AUC 的几个实用性质:它对类别不平衡不敏感(因为抽样是一正一负各抽一个,跟在总体里占多少比例无关);它不受阈值影响(不涉及任何具体切分点);它对分数做任何单调递增变换都不变(比如把分数取 log、开平方,AUC 一模一样)。最后这条在实战里很有用:模型 A 输出概率、模型 B 输出 logit,两者可以直接比 AUC,不用先统一量纲。
2. 六样本实例:把三个指标全部手算一遍
光看公式容易飘,拿个最小的例子走一遍,很多东西立刻就实了。
2.1 数据设定与 ACC 的计算
假设有 6 个样本,真实标签和模型打分如下:
| 样本 | 真实标签 | 模型得分 |
|---|---|---|
| A | 正 | 0.9 |
| B | 正 | 0.8 |
| C | 负 | 0.7 |
| D | 正 | 0.4 |
| E | 负 | 0.3 |
| F | 负 | 0.2 |
正样本 3 个(A、B、D),负样本 3 个(C、E、F)。如果按最常见的 0.5 当阈值:A、B、C 判正,D、E、F 判负。对照真实标签,TP = 2(A、B),FP = 1(C),FN = 1(D),TN = 2(E、F)。
ACC = (2 + 2) / 6 ≈ 0.667。
这个数字挺难看的。但有意思的是,如果你把阈值挪到 0.35:A、B、C、D 判正,E、F 判负。这时 TP = 3,FP = 1,FN = 0,TN = 2,ACC = 5/6 ≈ 0.833。同一个模型、同一批数据,只是换了个阈值,ACC 从 0.667 跳到 0.833。
这就是 ACC 最要命的地方:它把模型能力和阈值选择混在了一个数里。你没法判断 0.667 是因为模型差,还是因为你阈值选错了。而 ROC 和 AUC 恰恰把这一层剥离开了。
2.2 逐个阈值扫一遍,画出 ROC
现在按 ROC 的构造方式,把阈值从高到低扫一遍。约定"得分大于等于阈值判为正":
| 阈值 | 判正样本 | TP | FP | TPR | FPR |
|---|---|---|---|---|---|
| 1.0 | 无 | 0 | 0 | 0 | 0 |
| 0.9 | A | 1 | 0 | 1/3 | 0 |
| 0.8 | A,B | 2 | 0 | 2/3 | 0 |
| 0.7 | A,B,C | 2 | 1 | 2/3 | 1/3 |
| 0.4 | A,B,C,D | 3 | 1 | 1 | 1/3 |
| 0.3 | A,B,C,D,E | 3 | 2 | 1 | 2/3 |
| 0.2 | 全部 | 3 | 3 | 1 | 1 |
把这些点 (FPR, TPR) 依次连起来:(0,0) → (0,1/3) → (0,2/3) → (1/3,2/3) → (1/3,1) → (2/3,1) → (1,1)。
画在图上,曲线先沿着纵轴往上爬(FPR 保持 0,说明阈值还很高,只判了少数样本为正,全是真阳性,没误伤),爬到 2/3 之后向右拐一格,再往上到顶,然后一路向右。这个"先竖后横再竖"的阶梯形状是 ROC 曲线的典型形态——每一次横向移动都对应一次误判,每一次纵向移动都对应一次正确捕获。曲线越往左上角鼓,模型越好。
2.3 梯形法与排序法:两条路算出同一个 AUC
有了点,算面积。按梯形法逐段累加:
- (0,0) 到 (0,2/3):宽度 0,面积 0
- (0,2/3) 到 (1/3,2/3):宽 1/3、高 2/3,面积 2/9
- (1/3,2/3) 到 (1/3,1):宽度 0,面积 0
- (1/3,1) 到 (2/3,1):宽 1/3、高 1,面积 1/3
- (2/3,1) 到 (1,1):宽 1/3、高 1,面积 1/3
加起来:2/9 + 1/3 + 1/3 = 2/9 + 6/9 = 8/9 ≈ 0.889。
换一条完全不同的路验证。用排序法(本质是 Wilcoxon-Mann-Whitney 统计量):把所有样本按得分从低到高排,给秩次,然后只把正样本的秩加起来。
| 样本 | 得分 | 秩 |
|---|---|---|
| F(负) | 0.2 | 1 |
| E(负) | 0.3 | 2 |
| D(正) | 0.4 | 3 |
| C(负) | 0.7 | 4 |
| B(正) | 0.8 | 5 |
| A(正) | 0.9 | 6 |
正样本秩和 = 3 + 5 + 6 = 14。代入公式AUC = (正样本秩和 - n_pos × (n_pos+1) / 2) / (n_pos × n_neg):
(14 - 3×4/2) / (3×3) = (14 - 6) / 9 = 8/9。
一模一样。第三种验证方式更直观:把所有正负样本两两配对,一共 3×3 = 9 对,数一数有多少对是"正样本得分高于负样本得分":
- 0.9 配 0.7、0.3、0.2:3 胜
- 0.8 配 0.7、0.3、0.2:3 胜
- 0.4 配 0.7 输、配 0.3 胜、配 0.2 胜:2 胜
总胜场 8,AUC = 8/9。三条路径,同一个答案。这个"数对数"的视角特别适合跟业务方解释:AUC 就是"随机拿一个正样本和一个负样本,模型认为正样本更可能为正的概率",比讲积分好懂一百倍。
回到前面的对比:这个模型的 AUC 是 0.889,看着不错;但按 0.5 阈值切出来的 ACC 只有 0.667。同一份打分,两个指标给出的印象差距这么大,原因就在于 AUC 评价的是"排序质量",而 ACC 评价的是"某个特定切点上的判决质量"。0.889 说明模型的排序能力其实不差,0.667 说明 0.5 这个阈值选得不合适。正确的做法不是换模型,而是把阈值往下挪到 0.35 附近,ACC 直接变成 0.833。
3. 本质区别:从三个维度切开
3.1 阈值敏感与阈值无关
这是三者最根本的分界线。ACC 是阈值强相关的,同一个模型换阈值,ACC 可以从 0.667 变到 0.833;ROC 是阈值遍历的,它本身不是"一个数",而是一条记录了所有阈值下表现的曲线;AUC 是阈值无关的,它是一个单一的标量,把整条曲线积分掉了,不依赖任何具体切点。
这个区分在实际工作里的意义是:AUC 用来选模型,阈值用来定策略。这两件事必须分开做。很多团队的做法是训练完直接predict(),用 0.5 切一刀,然后看 ACC——这等于把两个决策绑死了。模型选得好不好,看 AUC(或者 PR-AUC);阈值定得对不对,要结合业务成本单独优化。0.5 这个数没有任何特殊性,它只是"概率大于一半"的直觉残留。
顺便说,这也是为什么 ROC 曲线本身在报告里其实很有价值——它把"可选的阈值空间"完整呈现出来了,业务方可以看着曲线说"我们能接受 FPR 不超过 5%,看看这时候能抓多少"。单给一个 AUC,反而丢掉了这些信息。
3.2 排序能力与判别能力
AUC 衡量排序,ACC 衡量判别,这两个能力的强弱是可以脱钩的。
举个极端情况:模型 A 输出的是完美排序的分数,但所有分数都挤在 0.45 到 0.49 之间——它的 AUC 是 1.0,但如果直接按 0.5 切,全部判负,ACC 惨不忍睹。模型 B 输出的是真实概率,AUC 0.85,按 0.5 切 ACC 有 0.82。单看 AUC,A 更好;但从"直接拿来用"的角度,B 明显更省事。
反过来也有:模型 AUC 只有 0.7,但如果你知道业务上最优阈值是 0.2,切完之后 ACC 可能相当不错。AUC 0.7 意味着排序能力一般,但在那个特定的工作点上,它可能刚好够用。
这背后的道理是:AUC 是全局指标,它对所有阈值一视同仁地取了平均;而真实业务只用一小段阈值区间。如果模型的优劣只体现在曲线的左端(高精度区),而 AUC 把整条曲线都算进去了,那 AUC 就会稀释掉你真正关心的那部分表现。这种情况就该考虑 partial AUC(只算 FPR 在 0 到某个上限之间的面积),sklearn 里没有直接函数,得自己截取roc_curve返回的数组再积分。
3.3 类别不平衡下的分道扬镳
这是三个指标分歧最大、也最容易踩坑的地方。
先说 ACC,前面已经说过,它在不平衡场景下基本失真,99% 的负样本会让"全判负"拿到 0.99,不用再多说。
重点说 AUC。很多资料上写"AUC 对类别不平衡不敏感",这句话是对的,但它容易被误读成"AUC 在不平衡场景下很可靠"。不敏感不等于可靠。因为 AUC 的两个轴都是类内归一化的,它只关心"正样本内部的排序"和"负样本内部的排序",完全不关心两类各有多少个。这带来的副作用是:当正样本极度稀少(比如万分之一的欺诈率),模型只要在那几千个负样本里稍微调整一下排序,FPR 的分母巨大,横轴的变化被压得很小,曲线很容易被"撑"得靠近左上角,AUC 看起来很漂亮。
我遇到过一次典型的:一个反欺诈模型,AUC 0.92,评审一片叫好。上线后召回率惨淡。回头算 PR 曲线,AUPRC 只有 0.11,而随机水平的基线是 0.0001 量级的——虽然 0.11 远高于随机,但跟 AUC 0.92 给人的那种"接近完美"的错觉差距巨大。原因就是负样本太多了,AUC 的横轴被压扁,模型在头部精度上的缺陷被平均掉了。
PR 曲线(Precision-Recall)在这种场景下更诚实。它的纵轴是精确率TP/(TP+FP),横轴是召回率TP/(TP+FN),两个轴都直接涉及正样本,负样本只通过 FP 影响精确率。所以在正类稀少的时候,PR 曲线对模型在头部区间的表现极其敏感,一个误判就足以把精确率拉下来。判断标准也很朴素:正类占比低于 10% 左右,就该把 PR-AUC 拉到主指标的位置,AUC 退居参考。
3.4 AUC 高不等于概率准
最后这一点特别容易被忽略。AUC 只看排序,不看分数的绝对值。一个模型给所有样本打的分都乘以 10,AUC 不变;给分数加上一个常数,AUC 也不变。所以AUC 高的模型,输出的分数完全可能不是概率。
这在需要"用分数做决策"的场景里是致命的。比如信贷定价,你要根据违约概率来定利率,模型说这个客户违约概率 0.03,你就按 3% 定价格。如果模型只是排序好、但分数校准得很差,实际违约率可能是 8%,这个定价就是亏的。
评估校准要用另外的工具:校准曲线(把预测概率分箱,看每箱的实际正例率是否贴近预测均值)、Brier Score(预测概率与真实标签的均方误差,越小越好)。sklearn 里有calibration_curve和brier_score_loss。如果发现校准差,可以用 Platt scaling(在分数上再拟合一个逻辑回归)或者 isotonic regression 做后处理,这两招在评分卡项目里几乎是标配。
我的经验是:排序能力用 AUC / PR-AUC 看,概率质量用校准曲线看,这两件事要分开评估。只报 AUC 就宣称"模型可直接用于定价",是很危险的做法。
4. 代码实操:从单指标到完整评估
4.1 最小可用代码
把上面的例子落到 Python 里,先跑最小版本:
import numpy as np from sklearn.metrics import ( roc_auc_score, roc_curve, accuracy_score, precision_recall_curve, average_precision_score, confusion_matrix ) y_true = np.array([1, 1, 0, 1, 0, 0]) y_score = np.array([0.9, 0.8, 0.7, 0.4, 0.3, 0.2]) # AUC auc = roc_auc_score(y_true, y_score) # 0.8889 # ROC 曲线的点 fpr, tpr, thresholds = roc_curve(y_true, y_score, drop_intermediate=False) # ACC(阈值 0.5) y_pred = (y_score >= 0.5).astype(int) acc = accuracy_score(y_true, y_pred) # 0.6667 cm = confusion_matrix(y_true, y_pred) # [[2,1],[1,2]] # PR 曲线与 AUPRC prec, rec, pr_thr = precision_recall_curve(y_true, y_score) ap = average_precision_score(y_true, y_score)跑出来roc_auc_score是 0.8889,跟手算的 8/9 完全对上。
这里有个参数必须提一下:roc_curve默认drop_intermediate=True,它会剔除掉那些共线的中间点,让返回的阈值数组变短。面积不受影响,但如果你要做阈值扫描、或者要精确控制曲线的点数,一定把它设成False。我在一个需要"从曲线上反查最优阈值"的项目里就栽过:默认参数下阈值只剩 5 个点,间距大得没法用,排查了半天才发现是这个参数在作怪。
另外注意roc_curve返回的 threshold 数组第一个值是inf,对应"一个都不判正"的状态,如果拿它去做>=比较要小心处理。
4.2 画图与阈值挑选
ROC 曲线该画还是得画,很多信息只有图上看得到——曲线是平滑地鼓起来,还是在某一段突然塌下去,是集中在左上角还是均匀分布,这些单看 AUC 数字根本发现不了。
import matplotlib.pyplot as plt fig, ax = plt.subplots(figsize=(6, 6)) ax.plot(fpr, tpr, color='#1f4e79', lw=2, label=f'ROC (AUC = {auc:.3f})') ax.plot([0, 1], [0, 1], color='gray', lw=1, ls='--', label='Random') ax.set_xlim(0, 1); ax.set_ylim(0, 1) ax.set_xlabel('False Positive Rate') ax.set_ylabel('True Positive Rate') ax.legend(loc='lower right') ax.set_title('ROC Curve')figsize给 6×6 是刻意的——ROC 的两个轴都是 0 到 1 的比率,方图才不会把曲线的形状压变形。见过不少人画成 8×4 的长方形,曲线看着更"鼓"了,纯属视觉错觉。
挑阈值我一般用两种方法,视场景选:
约登指数(Youden's J):最大化TPR - FPR,等价于找曲线上离对角线最远的点。代码就一行:
j_scores = tpr - fpr best_idx = np.argmax(j_scores) best_thr = thresholds[best_idx]业务成本最优:如果 FP 和 FN 的代价能估出来,直接算总成本。设 FP 代价为 C_fp、FN 代价为 C_fn,对每个阈值算cost = FP × C_fp + FN × C_fn,取最小的那个。这个方法比约登指数靠谱得多,因为约登指数默认两类错误代价相等,而现实中几乎从来不等。
举个具体的数:某催收场景,漏掉一个高风险客户损失 5000,误伤一个好客户损失 200,那么 C_fp = 200、C_fn = 5000,比例 1:25。这时候最优阈值会明显偏低——宁可多误伤,也要把高风险的多抓一些。这个比例一变,阈值就跟着变,所以阈值不是模型的一部分,是策略的一部分,换了业务目标就得重算。
4.3 交叉验证与置信区间
单次切分算出来的 AUC 有个隐藏问题:它本身是个随机变量。样本量 5000 的时候,AUC 的 95% 置信区间宽度大约是 ±0.015;样本量掉到 500,宽度可能到 ±0.05。也就是说 AUC 0.82 和 0.79 的两个模型,在小样本下很可能根本没有显著差异,但你在周报上写"新版提升 3 个点",领导就以为稳了。
给 AUC 配个置信区间,用自助法最省事:
from sklearn.utils import resample def bootstrap_auc(y_true, y_score, n_boot=1000, seed=42): rng = np.random.RandomState(seed) scores = [] n = len(y_true) for _ in range(n_boot): idx = rng.randint(0, n, n) if len(np.unique(y_true[idx])) < 2: continue scores.append(roc_auc_score(y_true[idx], y_score[idx])) return np.percentile(scores, [2.5, 97.5]) lo, hi = bootstrap_auc(y_true, y_score)那个len(np.unique(...)) < 2的判断不能省——自助采样有可能抽到一个全是正类或全是负类的子集,roc_auc_score会直接抛异常。
如果要比较两个模型是否有显著差异,光看置信区间重叠是不够严谨的。更合适的做法是用DeLong 检验(针对相关 ROC 曲线的非参数检验),或者干脆用同一批 bootstrap 样本算两次 AUC 的差值分布,看差值区间是否跨过 0。后者实现简单,我一般用这个。
4.4 多分类怎么算
多分类下这三个指标都要变通。ACC 最简单,直接算预测类别 == 真实类别的比例,没有歧义。ROC 和 AUC 就麻烦了——"正例"这个概念没了。
标准做法有三种聚合:
| 聚合方式 | 计算逻辑 | 适用场景 |
|---|---|---|
| macro | 每类单独算 AUC 再取算术平均 | 各类同等重要,关心小类表现 |
| weighted | 按各类样本量加权平均 | 想反映整体表现,大类主导 |
| micro | 把多分类摊平成多个二分类,汇总后统一算 | 关心全局的判别能力 |
from sklearn.metrics import roc_auc_score # y_prob 是 (n_samples, n_classes) 的概率矩阵 auc_macro = roc_auc_score(y_true, y_prob, multi_class='ovr', average='macro') auc_weighted = roc_auc_score(y_true, y_prob, multi_class='ovr', average='weighted')multi_class有'ovr'(一对其余)和'ovo'(一对一)两个选项。'ovr'是把每一类当正类、其余全当负类,分别算 ROC 再平均;'ovo'是两两配对算,最后加权。实践中'ovr'用得多,因为它的概率输出更直观;'ovo'在类别很多、每类样本都不少的时候更稳定。注意 sklearn 在某些版本里对multi_class='ovo'有额外限制,用之前先看一眼版本文档。
还有一个坑:macro 平均会被小类拖死。如果某一类的样本只有个位数,它的 AUC 可能是 0.3 或者干脆算不出来(因为没有正样本),macro 平均一下,整体数字就崩了。遇到这种情况要么用 weighted,要么把小类合并掉,别硬报 macro。
5. 选型:什么场景看哪个指标
5.1 常规场景的默认组合
如果项目没有特殊要求,我一般报这么一组:
- AUC:主指标,用于模型间横向比较
- ROC 曲线图:放到附录,用于展示全阈值表现
- 选定阈值下的混淆矩阵 + 精确率 + 召回率 + F1:说明"这个模型在业务切点上具体什么样"
- ACC:只作为兜底参考,且要同时报正负类比例
这套组合的逻辑是:AUC 负责"选谁",混淆矩阵负责"怎么用",ACC 负责"给不看模型的人一个直觉锚点"。只报 AUC 的问题是业务方不知道该怎么用;只报 ACC 的问题是你说不清模型本身好坏。
顺带说一句,F1 也不是万能的。它是精确率和召回率的调和平均,隐含假设两者同等重要。如果你的业务里漏判的代价是误判的 20 倍,F1 会给出一个明显偏保守的评估。这种时候该用 F-beta(beta 取代价比的平方根),或者直接算加权成本。
5.2 极度不平衡场景:把 PR 曲线提到主位
前面已经说过原理,这里给个可操作的判断门槛:正类占比低于 5% 到 10%,就把 AUPRC 当主指标。具体怎么算:
from sklearn.metrics import precision_recall_curve, average_precision_score prec, rec, _ = precision_recall_curve(y_true, y_score) auprc = average_precision_score(y_true, y_score)这里有个必须澄清的概念混淆:average_precision_score算的是 AP(Average Precision),它用的是阶梯法累加,跟"PR 曲线下的面积"(AUC-PR)在数学上不完全相等。很多资料混着叫,但严格说 AP ≤ AUC-PR。sklearn 只提供 AP,要算真正的 AUC-PR 得自己用auc(rec, prec)去做,注意rec的顺序是递减的,得先翻转。日常对比模型用 AP 就够了,只要前后一致就行。
还有一个必做的动作:报 AUPRC 的时候一定要同时报基线。PR 曲线的随机基线不是 0.5,而是正类占比。正类占 2% 的时候,随机模型的 AUPRC 大约是 0.02。所以 AUPRC 0.15 看起来很低,实际上已经是随机水平的 7 倍多,可能相当不错;反过来 0.6 在均衡数据里很平庸,在不平衡数据里已经是顶级水平。脱离基线谈 AUPRC 毫无意义。
5.3 从 AUC 到实际决策:分数怎么用起来
模型评完 AUC,工作只完成了一半。真正上线还要回答:这个分数怎么变成动作?
第一种用法是硬阈值:高于某个值就走 A 流程,低于走 B 流程。这时候阈值的确定必须结合成本,前面 4.2 节的方法直接用。定完阈值之后,报一组"在该阈值下的精确率、召回率、通过率",业务方才有感觉。
第二种是分段策略:把分数切成 5 到 10 档,每档对应不同的处理方式,比如高风险档人工审核、中风险档加验证、低风险档直接过。这种方式比单阈值更细腻,也更适合风控、营销这类需要分层运营的场景。做分段的时候,每一档都要单独报样本量、实际正例率、以及预测分数均值——如果某一档的预测均值和实际正例率差得很远,说明模型在这一段校准有问题,得回头处理。
第三种是排序取 Top-K:只关心分数最高的 K 个人。这种场景下 AUC 其实不是最合适的指标,应该看Precision@K或者Recall@K,也就是前 K 个里有多少是真的正例。AUC 是全局的,而 Top-K 只看头部,两者可能背离——一个模型 AUC 更高,但头部精度更差,完全有可能。做推荐、做名单制营销的时候,这个区别一定要拎清。
5.4 这些指标都替代不了的东西
最后说点清醒话。AUC、ROC、ACC 都是统计层面的指标,它们能告诉你模型在历史数据上的排序和判别表现,但替代不了三件事。
第一是业务可解释性。AUC 0.9 的模型如果用了上百个特征、内部是个黑箱,在很多受监管的场景里根本过不了审。这时候一个 AUC 0.78 但只有 8 个变量、每个变量方向都合理的评分卡,反而是唯一能上线的选择。
第二是稳定性。AUC 是在某一段历史数据上算的,跨时间、跨人群的表现可能完全不同。我见过 AUC 0.88 的模型上线三个月就掉到 0.72,原因是客群结构变了。所以评完 AUC 还要做时间外验证(用未来的数据测)和群体稳定性检查(PSI),这两步比多提 0.01 的 AUC 重要得多。
第三是因果性。AUC 高只说明相关性拟合得好,不说明模型的判断逻辑是对的。一个用"是否持有某张卡"预测违约的模型,AUC 可能很高,但这个特征本质上是个结果而不是原因,策略一旦调整(比如停发那张卡),模型立刻失效。这类问题指标层面完全看不出来,得靠业务理解和特征归因去查。
6. 常见问题与排查实录
6.1 一张速查表
| 现象 | 最可能的原因 | 排查方向 |
|---|---|---|
| ACC 很高但业务方说没用 | 类别极度不平衡,模型偏向多数类 | 看混淆矩阵,确认 TP 数量;换 AUPRC |
| AUC 0.5 附近 | 正负类标签搞反了,或特征全无信息 | 用1 - AUC验证;检查predict_proba取列 |
| AUC 异常高(0.98+) | 特征泄漏,目标信息混进了特征 | 检查特征生成时间点,排查 ID 类特征 |
| 训练集 AUC 0.95、测试集 0.72 | 过拟合或分布漂移 | 看学习曲线,加正则,做时间外验证 |
| 多个模型 AUC 差 0.005 | 在置信区间内,无统计显著差异 | 算 bootstrap 区间或做 DeLong 检验 |
| ROC 曲线呈直角折线 | 模型输出是离散的几个值 | 检查模型是否输出了硬分类而非概率 |
| 同一模型两次算 AUC 不一致 | 样本顺序或并列分数处理不同 | 固定随机种子,检查是否有并列得分 |
| 多分类 macro AUC 低得离谱 | 某个小类样本太少,单独 AUC 不稳 | 看各类样本量,改用 weighted 或合并小类 |
6.2 我踩过的几个坑
坑一:画出来的 ROC 曲线点数少得可怜。第一次遇到的时候以为是代码写错了,折腾了半天才想起来drop_intermediate这个参数。它的默认值是True,会把共线的中间点剔掉。数据量大的时候无所谓,面积几乎不受影响;但如果要做阈值扫描、或者要把曲线给业务方看细节,一定要显式设成False。
坑二:AUC 从 0.85 突然变成 0.15。这不是模型崩了,是把正负类定义反了。sklearn 按classes_的顺序返回概率列,我当时的标签是字符串'yes'/'no',字母序上'no'排在前面,predict_proba[:, 1]取到的是负类的概率。修法是显式指定正类:pos_idx = list(model.classes_).index('yes'),然后取[:, pos_idx]。这个错误极其隐蔽,因为代码不报错,只是数字反了。
坑三:用并列分数算 AUC 得到奇怪结果。有一批模型输出的分数是分档的(比如只输出 0 到 10 的整数),大量样本得分完全相同。这种情况下梯形法和排序法算出来的值会有细微差异,因为并列对的计分规则不同。sklearn 用的是梯形法,等价于把并列对算作 0.5 胜。如果你的场景对并列很敏感(比如排序系统),最好在算 AUC 之前加一点极小的随机扰动,或者干脆改用更明确的指标。
坑四:拿训练集的 AUC 当模型效果。这个坑太经典了,但还是反复有人踩。训练集 AUC 只反映拟合程度,测试集 AUC 才反映泛化。更严格的做法是时间外样本:用 1 到 9 月训练,10 到 12 月测试。随机切分的测试集往往会高估效果,因为同一时期的数据分布更接近。
坑五:以为 AUC 0.9 就可以直接上。这是思维层面的坑。我现在的习惯是,AUC 出来后必须再问三个问题:这个模型的分数分布长什么样(是不是挤在一小段)?头部(Top 10%)的精度是多少?跨时间稳不稳?这三个问题里任何一个出问题,AUC 再高也得打回去重做。指标是用来辅助判断的,不是用来替代判断的。
坑六:忘了报基线。报 AUPRC 0.3 的时候如果不说正类占比是 3%,听的人会以为模型很差;说了之后才知道这是随机水平的 10 倍。同理,报 AUC 的时候也要说清楚样本量——500 个样本算出来的 AUC 和 50 万个算出来的,可信度完全不是一个量级。我现在写报告的习惯是,每个指标后面都跟一句"样本量 N = xxx,95% CI 为 [a, b]",这句话能省掉后面无数的扯皮。
我自己在这三个指标上折腾这么多年,最实在的一条经验是:永远不要只报一个数。ACC 单独出现会被不平衡骗,AUC 单独出现会掩盖校准问题和头部精度问题,ROC 曲线单独出现又没法比较模型。三个一起看,再加上混淆矩阵和基线,才能拼出一张完整的图。真正难的从来不是把公式背下来,而是知道在什么场景下该信哪个数、该怀疑哪个数——这一层判断,靠的就是一次次踩坑之后攒下来的直觉。