
1. 评估指标到底在衡量什么前阵子帮一位做风控的朋友看模型他给我看了一份评估报告准确率 0.97AUC 0.93报告写得很漂亮。但真正上线跑了两周业务方反馈几乎没拦到该拦的人。我们把样本拉出来重新切片一看正样本占比只有 1.2%模型把所有样本都判成负类准确率就能有 98.8%。那份 0.93 的 AUC是因为训练集和测试集切分时用了随机切分同一个人在不同时间点的记录被分到了两边特征里又混进了带时间信息的字段等于提前把答案漏给了模型。这件事之后我形成了个习惯拿到任何一份模型评估报告先不看指标数值先看三件事——正负样本比例、测试集是怎么切的、指标是在哪个集合上算的。这三件事没搞清楚后面的 AUC、PR 曲线、F1 全都是空中楼阁。模型评估这件事的本质是回答一个问题模型在没见过的数据上把该挑出来的挑出来了多少同时把不该挑的误伤了多少。所有指标都是在这两个数字之间做权衡的不同表达方式。ROC 曲线、PR 曲线不过是把这个权衡过程在阈值维度上完整地铺开让你看清楚我换个阈值会发生什么。所以想真正读懂这两条曲线得先把最底层的四个格子吃透。1.1 混淆矩阵四个格子决定了后面所有事二分类问题的评估起点永远是混淆矩阵。模型输出一个分数你定一个阈值高于阈值判正、低于判负于是就有了四种结果实际为正实际为负预测为正TP真正例FP假正例预测为负FN假负例TN真负例这四个数看着简单但绝大多数评估困惑都源于没分清分母是谁。举个具体例子1000 个样本里 20 个正、980 个负模型挑出 30 个判正其中 15 个是真的正例。那 TP15FP15FN5TN965。准确率 (15965)/1000 98%看起来很美。精确率 15/30 50%也就是说模型说是坏人的一半是冤枉的。召回率 15/20 75%20 个真坏人只抓到 15 个。同一组预测三个指标讲出三个完全不同的故事。准确率的分母是全体样本精确率的分母是模型判正的那部分召回率的分母是实际为正的那部分。分母不同指标服务的对象就不同——精确率关心的是我出手的这批质量如何召回率关心的是该抓的我漏了多少。业务上你到底更怕哪种错决定了你该看哪个指标。这里有个特别容易被忽略的点sklearn里precision_score和recall_score默认pos_label1也就是把标签 1 当正类。如果你的标签是 0/1 编码但业务上0 才是异常指标会整个反过来而且不会报错。我见过有人跑了一周实验最后发现精确率和召回率全反了白折腾。1.2 精确率与召回率的拉扯关系精确率和召回率不是独立的它们被同一个阈值绑在一起一动全动。阈值调高模型更谨慎判正的少了精确率通常上升、召回率下降阈值调低模型更激进召回率上升、精确率下降。这种此消彼长的关系画出来就是 PR 曲线。理解这个拉扯关系最好的方式是把它想成门卫查证件查得严阈值高进来的基本都是有证的但队伍排到门外查得松阈值低人都进来了混进来几个没证的。你没法同时要效率高和零漏检只能选一个平衡点。F1 就是把这两个指标揉成一个数F1 2 × P × R / (P R)本质是精确率和召回率的调和平均数。用调和平均而不是算术平均的原因很实在——调和平均对小的那个值更敏感。P1.0、R0.1 时算术平均是 0.55 看着还行调和平均只有 0.18直接把偏科暴露出来。这也解释了为什么 F1 在类别不平衡场景下比准确率靠谱得多。但 F1 也有它的局限它默认精确率和召回率同等重要。实际业务里往往不是这样。内容审核漏放一条违规内容的代价和误删一条正常内容的代价可能差几十倍。这时候该用F-betaF_β (1β²) × P × R / (β² × P R)β1 更看重召回率β1 更看重精确率。β 取多少不是拍脑袋得从业务成本倒推这块后面讲阈值选择时会展开。1.3 为什么准确率在类别不平衡时会骗人再回到开头那个 98.8% 准确率的例子。这里的问题不是准确率这个指标不好而是它的分母选错了。准确率把每个样本看成等权重的但在不平衡场景下负样本的数量优势会直接淹没正样本的表现。1000 个样本里 988 个负样本你全判负就拿到 98.8%。判断数据是否不平衡别只看比例还要看绝对数量。正样本占比 1% 但总量 100 万那还有 1 万个正样本模型完全学得动正样本占比 30% 但总量只有 200 条那 60 个正样本根本不够训练这时候的评估波动会大到没有参考价值。我一般的判断标准是验证集里正样本少于 100 条所有指标的置信区间都会宽到没法做决策这时候要么扩充数据要么老老实实做交叉验证取均值。还有一个隐蔽的坑阈值默认 0.5 在不平衡数据上几乎总是错的。模型在 1:99 的数据上训练完输出的分数分布会整体偏向 0绝大多数样本分数落在 0.1 以下你拿 0.5 当阈值等于要求模型极度自信才肯判正召回率自然惨不忍睹。这一点是很多人第一次做不平衡分类时最想不通的地方——AUC 明明 0.9怎么召回只有 0.2因为 AUC 衡量的是排序能力跟阈值无关而召回率是定阈值之后的结果。两者根本不是一回事。2. ROC 曲线是怎么一步步画出来的ROC 曲线的全称是 Receiver Operating Characteristic名字来自二战时的雷达信号检测分析跟机器学习没半点关系后来被搬过来描述在噪声中检测信号的能力这个历史背景其实很能说明它的本质在负样本的干扰下你能把正样本排到多前面。它的横轴是 FPR FP / (FP TN)也就是所有负样本里被误判为正的比例纵轴是 TPR TP / (TP FN)就是召回率。两个轴的分母分别是负样本总数和正样本总数所以 ROC 曲线描述的是模型对正负两类的相对区分能力跟正负样本比例无关——这正是它最大的优点也是它在极端不平衡场景下最大的缺点。2.1 阈值滑动如何把离散点连成曲线很多人第一次看 ROC 曲线会觉得它是算出来的其实更准确的说法是扫出来的。模型给每个样本一个分数你把所有分数从高到低排个序然后从无穷大开始一点点往下挪阈值每挪一次就重新算一遍 TPR 和 FPR得到一个坐标点所有点连起来就是 ROC 曲线。举个 8 个样本的小例子标签和分数是y_true [0, 0, 1, 1, 0, 1, 0, 1] y_score [0.1, 0.35, 0.4, 0.8, 0.2, 0.9, 0.6, 0.7]总共 4 正 4 负。按分数降序排0.9(正)、0.8(正)、0.7(正)、0.6(负)、0.4(正)、0.35(负)、0.2(负)、0.1(负)。依次往下扫每遇到一个样本就更新 TP 或 FP阈值位置TPFPTPRFPR起始全判负0000≥0.9100.250≥0.8200.500≥0.7300.750≥0.6310.750.25≥0.4411.000.25≥0.35421.000.50≥0.2431.000.75≥0.1441.001.00把 (FPR, TPR) 这些点画出来再按梯形法算面积从 (0,0) 到 (0,0.75) 这一段横坐标没变面积为 0从 (0,0.75) 到 (0.25,0.75) 面积 0.25 × 0.75 0.1875之后 (0.25,1)→(0.5,1)→(0.75,1)→(1,1) 三段各是 0.25 × 1 0.25。加起来AUC 0.1875 0.75 0.9375。这个手算过程建议每个人都至少跑一遍跑完你对 AUC 的理解会完全不一样。用代码验证也很简单import numpy as np from sklearn.metrics import roc_curve, auc y_true np.array([0, 0, 1, 1, 0, 1, 0, 1]) y_score np.array([0.1, 0.35, 0.4, 0.8, 0.2, 0.9, 0.6, 0.7]) fpr, tpr, thresholds roc_curve(y_true, y_score) print(AUC:, auc(fpr, tpr)) # 0.9375 print(阈值:, thresholds) # 注意第一个阈值是 infroc_curve返回的thresholds[0]是inf对应的点是 (0,0)代表全部判负这个极端状态。这就是为什么thresholds的长度永远比fpr/tpr多一个——第一个是虚拟的起点。2.2 手写 ROC 绘制并列分数是个隐形杀手自己实现一遍 ROC 计算最能暴露问题。最朴素的写法是这样的def naive_roc(y_true, y_score): order np.argsort(-y_score) y np.asarray(y_true)[order] P y.sum() N len(y) - P tps np.cumsum(y) fps np.cumsum(1 - y) return fps / N, tps / P这个版本在分数没有并列的情况下是对的但一旦出现并列分数就会画出一段其实不存在的锯齿。原因很直观如果有两个样本分数完全相同按上面的写法排序后它们会被当成先后扫过产生了中间状态。而真实情况是阈值没法把这两个样本分开——要么都判正要么都判负不可能一个正一个负。所以正规实现必须先按分数分组同一组分数一次性全部纳入再取坐标点。sklearn内部就是这么处理的。这个细节在日常使用中一般不会出问题但如果你在做模型融合、对分数做了四舍五入、或者分数本身就是离散的比如某些树模型输出的叶子索引并列分数会非常普遍算出来的 AUC 会偏高。还有一种情况也得注意样本量很小的时候ROC 曲线是一条阶梯状的折线不是平滑曲线。这时候用梯形法算面积会引入系统偏差。样本量少于几百的时候我更倾向于直接看曲线形状和关键阈值点而不是盯着那个 AUC 数值。2.3 AUC 到底在说什么AUC 的官方解释是ROC 曲线下的面积但这个定义没什么用因为它没告诉你这个数字代表什么。我个人最常用的两种解读方式第一种概率解读。AUC 等于随机抽一个正样本和一个负样本模型给正样本打分高于负样本的概率。AUC0.9375 意味着你随便拿一对正负样本模型有 93.75% 的概率把正的那个排前面。这个解读特别适合向非技术同事解释——你不需要讲什么 FPR、TPR就说模型的排序准确率。第二种排序质量解读。AUC 只关心排序不关心分数的绝对值。你把所有预测分数取对数、开平方、甚至做个单调递增的任意变换AUC 一点不变。这既是优点模型输出的分数不需要校准就能算 AUC也是缺点AUC 好不代表分数可以直接当概率用。顺带提一个常被问到的换算Gini 系数 2 × AUC − 1。AUC0.5 时 Gini0AUC1 时 Gini1。风控领域经常用 Gini 或者 KS 统计量本质都是在描述模型把两类分开的能力跟 AUC 是同一件事的不同刻度。关于多高的 AUC 算好网上流传的 0.7/0.8/0.9 分档纯属经验值没有普适性。同样是 0.85在医学影像里可能已经很不错在推荐排序里可能完全不可用。真正有意义的对比基准只有两个随机猜的 AUC0.5和现有线上系统的 AUC。比 0.5 高多少说明模型学到了多少比线上高多少说明这次迭代有没有价值其他都是虚的。2.4 ROC 在极端不平衡下为什么会失真现在讲 ROC 最要命的短板。看 FPR 的分母FP TN也就是所有负样本的数量。当负样本有 100 万条的时候你把 FP 从 100 增加到 10000FPR 只从 0.0001 变成 0.01在图上几乎看不出变化。但实际业务里多出来的 9900 个误报可能已经让你的审核团队崩溃了。这就是 ROC 在极度不平衡场景下的失真机制它把负样本的绝对数量摊薄了。曲线依然漂亮AUC 依然 0.95但模型在高精确率区域的表现完全被掩盖了。判断什么时候 ROC 会失真可以用一个粗略的经验正负比例超过 1:10 就该警惕超过 1:100 就该换 PR 曲线做主看板。具体阈值没有定论取决于你的业务能容忍多少误报。广告点击率预估的正负比常常是 1:100 到 1:1000欺诈检测更极端这时候 ROC 只能当个参考真正的决策依据得看 PR。还有一个衍生问题ROC 曲线对模型在头部排序的表现不敏感。因为它从阈值正无穷一路扫到负无穷每个样本都只贡献一次头部那几百个高分样本的表现被淹没在几十万个样本里。而业务上我们往往只关心推给用户的 Top 100质量如何这时候该看的是PrecisionK、RecallK、NDCG这类排序指标ROC 和 PR 都不太够用。3. PR 曲线正样本稀缺时该盯的地方如果说 ROC 是从负样本视角看模型那 PR 曲线就是从正样本视角看模型。横轴是召回率 R TP/(TPFN)纵轴是精确率 P TP/(TPFP)。注意这两个轴的分母里都只有正样本相关的量TPFN 是实际正样本数TPFP 是预测正样本数负样本只通过 FP 间接参与。这个坐标设计的差别直接决定了 PR 曲线在不平衡场景下的表现当正样本极少时任何一个 FP 都会让精确率明显下降所以 PR 曲线对误报的惩罚被放大了正好补上了 ROC 的短板。3.1 两条曲线的形状差异说明了什么拿同一组数据同时画 ROC 和 PR最容易观察到的现象是ROC 曲线通常从 (0,0) 出发一路向右上走而 PR 曲线通常从左上角出发或者从某个较高的精确率开始向右下走。这不是画错了是坐标系方向决定的。ROC 的起点是全判负TPR0FPR0在左下角。PR 的起点是全判负召回率0精确率按定义为 1因为没预测任何正样本不存在误报但sklearn会返回精确率1、召回率0 这个点作为起点。一个特别值得记的结论PR 曲线的随机基线不是 0.5而是正样本比例。如果你的数据里正样本占比 5%那一个随机分类器的 PR 曲线大致会落在 P≈0.05 的水平线上。所以在不平衡数据上AP0.3 看起来很惨但如果基线只有 0.01那 0.3 已经是 30 倍的提升相当能打了。不报基线的 PR 指标没有任何意义这一点我在评审别人的报告时一定会问。ROC 的基线就简单得多永远是 0.5 那条对角线跟数据分布无关。这也是 ROC 用起来省心的地方。3.2 AP 的两种算法插值和不插值差很多PR 曲线下的面积一般叫 APAverage Precision但这个词在文献里有好几种算法互相之间不能直接比较这是个大坑。第一种梯形积分法。直接对 (recall, precision) 做数值积分import numpy as np from sklearn.metrics import precision_recall_curve, auc, average_precision_score precision, recall, thresholds precision_recall_curve(y_true, y_score) # 方式一梯形积分 ap_trapz auc(recall, precision) # 方式二阶梯法sklearn 的 average_precision_score ap_step average_precision_score(y_true, y_score)这两个数是不相等的而且差值可能不小。原因在于 PR 曲线本身是阶梯状的阈值每挪一次召回率可能一口气跳好几个百分点而精确率在跳变前后差异很大。梯形法会在跳变的两个点之间画一条斜线把中间那些其实并不存在的操作点也算进面积里所以通常会偏高。第二种阶梯法也叫非插值 AP。这才是average_precision_score的实现方式公式是AP Σ (R_n − R_{n−1}) × P_n意思是每当召回率往上走一段就用走完之后那一刻的精确率乘以这段召回率的增量累加起来。回到 2.1 节那个 8 样本的例子按这个公式算召回率 0→0.25此时精确率 1.0贡献 0.25 × 1.0 0.25召回率 0.25→0.50精确率 1.0贡献 0.25 × 1.0 0.25召回率 0.50→0.75精确率 1.0贡献 0.25 × 1.0 0.25召回率 0.75→1.00精确率降到 0.8贡献 0.25 × 0.8 0.20AP 0.95。而梯形法算出来的值会略高于这个数。所以报告里写 AP 的时候一定要标明用的是哪种算法否则跟别人的数字对不上是常事。还有第三种更老的算法11 点插值法在召回率 0、0.1、…、1.0 这 11 个点上取右侧最大精确率求平均。这是早期目标检测评测用的现在的标准已经改成全点插值了。如果看到论文里写 mAP0.5 但没说明插值方式多半是全点插值。3.3 什么情况下该用 PR 而不是 ROC我给一个比较实用的判断流程场景特征推荐主指标理由正负比例在 1:1 到 1:10 之间ROC-AUC曲线稳定解释直观正负比例超过 1:100PR-APROC 会被负样本数量稀释业务只关心 Top-K 结果PrecisionK / RecallK全局指标无法反映头部质量关心具体阈值下的表现精确率/召回率/F1定阈值曲线是全局视角不能代替点估计需要向非技术方汇报ROC-AUC 业务口径指标单一数字易沟通一个经常被忽略的事实是当正样本占比很低的时候PR 曲线本身的波动会很大。因为精确率的分母是预测正样本数早期可能只有 5 个、10 个多一个 FN 就能让精确率从 0.6 跳到 0.4。这种波动不是模型不稳定是统计噪声。我通常会配合 bootstrap 给 AP 算个置信区间如果区间宽度超过 0.05那这个数字就不该拿来比较模型优劣。4. 阈值怎么定从曲线回到具体决策前面所有讨论都是阈值无关的AUC 衡量排序AP 衡量排序都不涉及具体阈值。但模型上线必须定一个阈值这一刻所有全局指标都失效了你面对的是一个非常具体的取舍问题。我在项目里反复强调一句话阈值 0.5 是数学上的中点不是业务上的中点。模型输出的 0.5 到底代表什么取决于模型的校准程度。逻辑回归在均衡数据上训练完输出大概能当概率看但随机森林、XGBoost 在严重不平衡数据上训练完输出分数会整体偏低0.5 可能对应着 99 分位。这时候拿 0.5 当阈值等于自废武功。4.1 用代价矩阵把阈值算出来最直接的定量方法是期望代价最小化。假设误报一个的代价是 C_FP漏报一个的代价是 C_FN那么在阈值 t 下Cost(t) FP(t) × C_FP FN(t) × C_FN把所有候选阈值都扫一遍取代价最小的那个。实现起来二十行代码import numpy as np from sklearn.metrics import confusion_matrix def best_threshold(y_true, y_score, cost_fp1.0, cost_fn10.0, n_steps200): thresholds np.linspace(y_score.min(), y_score.max(), n_steps) best_t, best_cost 0.5, float(inf) for t in thresholds: y_pred (y_score t).astype(int) tn, fp, fn, tp confusion_matrix(y_true, y_pred, labels[0, 1]).ravel() cost fp * cost_fp fn * cost_fn if cost best_cost: best_cost, best_t cost, t return best_t, best_cost这里的关键是cost_fn和cost_fp怎么来。不要拍脑袋填 10 和 1得去问业务方。比如内容审核场景可以这么问漏放 100 条违规内容和误删 100 条正常内容哪个你更不能接受差多少倍 得到的倍数关系就是代价比。有些团队会把它换算成钱或者人力工时更容易达成共识。我在实际项目里踩过一个坑代价比不能只看单次成本还要看处理量。假设误报的代价是每条 5 元人工复核费漏报的代价是每条约 500 元的潜在损失看起来 1:100但如果你把阈值调低误报从每天 100 条涨到 5000 条人工团队直接排不过来这时候的代价就不是线性的了。所以算完理论最优阈值后还要做一次产能约束检查这个阈值下的日均触发量运营团队接不接得住。4.2 分位数阈值与产能约束当代价比很难量化时一个很实用的替代方案是按分位数定阈值。比如业务目标是每天处理 2000 条告警那就把当天所有样本的预测分数排序取第 99.8 分位对应的分数作为阈值。这样触发量天然可控不用反复调。这个做法的好处是自动适应分布漂移。模型上线后用户行为会变、样本分布会变如果阈值写死成 0.73可能过两个月触发量就翻十倍了。用分位数的话只要保证每天重算一次分位点触发量就稳住了。坏处也很明显它把模型发现了多少变成了我们只能处理多少。如果某天真的出现了大规模异常分位数阈值会把大量真实告警压下去。所以我一般建议双阈值方案主阈值用代价矩阵算出来的固定值触发就推送兜底阈值用分位数动态调整防止触发量爆炸两个阈值之间的样本进入低优先级队列不实时推送但保留记录。4.3 阈值上线后的分数漂移监控阈值定完不是结束而是开始。模型分数分布会随着时间漂移最常见的原因是特征的上游数据源发生了变化。比如某个统计类特征依赖近 7 天的行为数据节假日期间用户行为模式一变这个特征的分布就整个偏移了模型输出的分数也跟着偏。监控方法很简单每天算一次线上预测分数的分位数比如 P50、P90、P99和训练时的基准分位数对比。偏离超过一定幅度就告警。这个指标比监控特征分布更灵敏因为它直接把所有特征的偏移聚合到了一个维度上。我还会额外监控两个东西每天的判正率和每天的 TP 估算值。判正率就是预测为正的样本占比如果它在没有任何业务事件的情况下突然从 2% 跳到 8%那基本可以确定是模型或数据出了问题。TP 估算值需要一点人工抽检每天从判正的样本里随机抽 50 条人工核对算出精确率再乘以判正总量就能得到 TP 的近似值。这个数字比 AUC 更贴近业务因为它是实打实的今天抓对了多少个。5. 复现评估结果时最容易翻车的几个细节指标算错、曲线画歪很多时候不是理解问题而是流程上的细节没处理好。这一节我把自己和同事踩过的坑整理一下都是那种不看不知道一看就觉得蠢的类型但确实反复出现。5.1 交叉验证里的数据泄漏最常见也最致命的一类错误。看下面这段代码from sklearn.preprocessing import StandardScaler from sklearn.model_selection import cross_val_score # 错误做法先在全量数据上标准化再做交叉验证 scaler StandardScaler() X_scaled scaler.fit_transform(X) scores cross_val_score(model, X_scaled, y, cv5)问题在于fit_transform用到了全量数据的均值和方差包括每一折的验证集。验证集的信息提前参与了特征变换这在统计上就是泄漏。对于标准化这种线性变换泄漏的影响通常不大但如果换成SelectKBest做特征选择、或者用 SMOTE 做采样泄漏会非常严重。正确的做法是用Pipeline把预处理和模型打包让cross_val_score在每一折内部单独 fitfrom sklearn.pipeline import Pipeline from sklearn.feature_selection import SelectKBest, f_classif pipe Pipeline([ (select, SelectKBest(f_classif, k20)), (scaler, StandardScaler()), (clf, model), ]) scores cross_val_score(pipe, X, y, cv5, scoringroc_auc)还有一个更容易被忽略的泄漏来源分组信息。如果同一个用户有多条记录随机切分会让同一个用户同时出现在训练集和测试集里。模型只要记住这个用户的特征就能在测试集上蒙对。这种情况下应该用GroupKFold按用户 ID 分组切分。同理如果数据带时间维度必须用TimeSeriesSplit或者手动按时间切绝对不能随机打乱。我见过一个时序预测项目AUC 从 0.88 掉到 0.62团队查了两周最后发现是数据切分时打乱了时间顺序模型用到了未来信息。改成时间切分后那个 0.88 就再也没出现过。5.2 采样和评估的先后顺序处理类别不平衡时SMOTE 是最常用的手段。但很多人会在整个数据集上做完 SMOTE 再切分这直接导致测试集里的合成样本和训练集高度相关指标虚高到离谱。正确顺序永远是先切分只在训练集上采样测试集保持原始分布。from imblearn.pipeline import Pipeline as ImbPipeline from imblearn.over_sampling import SMOTE pipe ImbPipeline([ (smote, SMOTE(random_state42)), (clf, model), ])注意这里要用imblearn的Pipeline不是sklearn的因为imblearn的版本知道 SMOTE 只在 fit 阶段执行不做 transform 到测试集。还有一个更根本的问题值得思考测试集是否应该保持原始分布我的答案是必须保持。因为你上线后面对的就是这个分布如果测试集被人为平衡了那算出来的精确率、召回率全都是假的。有个例外是当你想评估模型在平衡场景下的纯判别能力那可以额外做一份平衡测试集但要明确标注这是参考值不能用于阈值决策。5.3 小样本下的指标波动有多大AUC 和 AP 都是点估计没有方差信息。测试集只有 200 条样本、其中 20 条正样本的时候AUC 的 95% 置信区间可能有 0.15 那么宽。这意味着两个模型的 AUC 差 0.05很可能只是噪声。估算区间最实用的方法是bootstrap对测试集做有放回重采样重复计算指标取 2.5% 和 97.5% 分位点。import numpy as np from sklearn.metrics import roc_auc_score def bootstrap_auc(y_true, y_score, n_boot1000, seed42): rng np.random.default_rng(seed) n len(y_true) aucs [] for _ in range(n_boot): 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])) return np.percentile(aucs, [2.5, 97.5]) lower, upper bootstrap_auc(y_true, y_score) print(fAUC 95% CI: [{lower:.3f}, {upper:.3f}])这里有个循环里的continue很关键——重采样有可能抽不到正样本或负样本这时候roc_auc_score会直接报错。这个细节很多现成的代码片段都漏了。我的经验是如果两个模型的 AUC 置信区间有明显重叠就不要急着宣布谁更好。要么扩样本要么做配对检验比如 DeLong 检验要么直接看它们在实际阈值下的业务指标差异。5.4 多分类场景下的平均方式二分类讲完了多分类的坑其实更深。sklearn的roc_auc_score支持average参数常见的有macro、weighted、micro三种算出来的值差别不小。macro每个类单独算 AUC然后算术平均。小类和大类等权重能反映模型对所有类一视同仁的表现。weighted按每类的样本数加权平均。大类主导更接近整体准确率的直觉。micro把所有类的预测展平后统一算实际上会被样本最多的类主导。选哪个取决于你的业务关心什么。如果小类才是重点比如罕见病识别必须用macro否则小类的糟糕表现会被大类掩盖。如果只是想知道整体效果weighted更合适。报告里写多分类 AUC 的时候一定要注明平均方式这是基本要求。还有个细节多分类的 ROC 曲线需要 one-vs-rest 展开对每个类画一条曲线。如果类别有十几个曲线图画出来会糊成一团这时候我一般只画 AUC 最高和最低的两个类或者干脆用柱状图展示各类 AUC比曲线清楚得多。5.5 报告里必须写清楚的那几件事最后列一份我自己用的评估报告检查清单每次交付前逐条过一遍测试集的样本量、正负比例、是否与线上分布一致数据切分方式随机 / 分层 / 分组 / 时序有没有泄漏风险所有指标的置信区间或多次重复实验的标准差ROC 和 PR 曲线同时给出并标明 AP 的计算方式关键阈值下的混淆矩阵而不只是全局指标与基线模型随机、现有线上系统的对比。这几条写清楚报告基本就不会被人挑出问题。反过来如果一份评估报告里这些信息一个都没有只甩一个 AUC0.93那这个数字到底可不可信就得打个问号。我个人在实操中的体会是模型评估这件事最难的不是算指标而是想清楚这个指标对应着哪个业务决策。AUC 对应不了任何具体动作它只是个排序质量的摘要真正指导行动的永远是在这个阈值下我会误伤多少人、漏掉多少人。所以每次做评估我都会逼自己往前走一步把这个模型放到真实流程里算一遍人工成本、算一遍漏检损失然后回头看那些曲线很多原本纠结的选择就变得很清楚了。