最早让我对 ROC 曲线产生敬畏感的,是一次信贷风控模型的评审会。当时模型在测试集上的准确率做到了 92%,团队都觉得挺满意,结果评审老师第一句话就问:“AUC 多少?曲线画了吗?”那一刻我才意识到,我已经在一个二分类任务里把准确率当成唯一标杆,却完全忽略了阈值才是影响业务口径的真正开关。后来自己把这套工具彻底用熟了,才发现 ROC 曲线本质上回答的问题非常简单:一个分类模型在“所有可能的判别阈值”下,到底有没有把好坏样本分开的本事?
这篇文章就把 ROC 曲线的含义、为什么它比准确率可靠、以及从零到一怎么画出来,一次讲透。如果你是刚接触机器学习的同学,或者已经上线过模型但还没认真看过曲线细节的工程师,这都是一份值得收藏的资料。后面我尽量不堆公式,多讲场景和可复现的代码,争取你看完就能直接在自己的项目里用起来。
1. 为什么需要ROC曲线:准确率不够用的时候
1.1 二分类结果背后藏着一个“阈值旋钮”
先回到我上面提到的信贷评审场景。一个二分类模型,无论是逻辑回归、XGBoost 还是神经网络,输出结果通常介于 0 和 1 之间,它表示“样本属于正类的概率”。业务落地时不能直接拿着 0.72 去审批贷款,得先把概率和某个阈值做比较,比如大于 0.5 就判断为“会逾期”,小于等于 0.5 就判断为“不会逾期”。这里的 0.5 就是阈值,也是很多模型默认使用的分类边界。
但阈值一旦移动,整个业务结果都会跟着变化。把阈值调低到 0.3,更多人会被归进“逾期”,坏账确实能拦下来不少,但大量好客户也会被误伤;把阈值调高到 0.7,误伤少了,漏掉的坏客户却显著增加。问题在于,这个取舍关系不是固定死的,它是连续的、动态的一条轨迹。只盯着准确率,就等于只看了某个特定阈值下的静止画面,模型的真实能力被严重压缩了。
我见过不少刚入门的朋友,训练完模型直接打印 accuracy,看到 90% 就欢呼,这是很危险的。准确率高,可能只是因为你把阈值调到了“消极保护”的位置,或者数据本身不平衡。真正上线之后,业务方问一句“这个准确率对应的阈值是多少,调低一点还能保证坏账率吗”,你如果答不上来,模型验收大概率过不了。
1.2 模型真正该关心的是排序能力
第二个我想强调的观点,可能一开始会反直觉:一个分类模型,更本质的能力是“排序能力”,而不是“准确给出概率的能力”。
比如风控系统给 5000 个用户打分,我们把得分最高的 200 人拉出来做人工复核,里面命中坏客户的比率越高,说明模型把“风险高的排到前面”这件事做得越好。你不需要模型把概率算得分毫不差,只要它能把危险样本相对靠前、安全样本相对靠后,模型就有了实用价值。推荐系统里把点击率最高的内容排到顶部,医疗筛查里把最可疑的影像报告优先交给医生复核,本质上都是在利用这种排序能力。
ROC 曲线盯着的就是这件事。它让每个可能的输出位置都能回答:“从这条线切下去,你截住了多少坏事,同时误伤了多少好人?”一旦理解了这一点,你就明白为什么在评估模型时,ROC 远比准确率更能反映真实水平。
1.3 准确率会失效,但ROC不会
不平衡数据的场景里,准确率的失效特别典型。比如某个风控数据集里正样本只占 1%,模型只要把所有样本都判为负类,准确率就能到 99%。这个数字漂亮到能骗过绝大多数只看结果不看逻辑的人,可模型毫无业务价值,因为它一个坏人也没抓出来。准确率之所以失效,是因为它默认“预测正确的成本是均等的”,但现实中漏掉一个坏客户和冤枉一个好客户,成本往往差着数量级。
ROC 用 TPR 和 FPR 两个指标同时展示“抓到了多少”和“误伤了多少”,把成本和偏好留给你去换算,而对模型自身能力的评估反而是全面且稳定的。所以总结下来,这些场景特别适合画 ROC 曲线:需要对比多个分类模型;需要给新模型选一个业务阈值;需要在不平衡数据上评估模型真实本领;需要向开发团队或业务方解释模型好坏。接下来我们一步步拆解它的原理和画法。
2. TPR、FPR和阈值:ROC曲线的数学骨架
2.1 从混淆矩阵说起
理解 ROC,第一步永远是过一遍混淆矩阵。二分类问题里,把预测结果和真实标签一比对,会出现四种情况:
| 预测为正类 | 预测为负类 | |
|---|---|---|
| 真实为正类 | TP(真正例) | FN(假负例) |
| 真实为负类 | FP(假正例) | TN(真负例) |
- TP:真实是正类,模型也预测成正类,抓对了。
- FN:真实是正类,模型预测成负类,漏掉了。
- FP:真实是负类,模型预测成正类,误伤了。
- TN:真实是负类,模型也预测成负类,放对了。
ROC 曲线的两个坐标轴,就是由这四个格子里的数字算出来的比例,一个叫 TPR,一个叫 FPR。
TPR = TP / (TP + FN),也叫召回率、灵敏度、查全率。它回答的问题是:所有真实正样本里,模型成功抓回来多少?在医疗场景里,就是“真有病的人里,有多少被筛出来了”。
FPR = FP / (FP + TN),也叫误报率、假正例率。它回答的问题是:所有真实负样本里,有多少被错判成了正类?在医疗场景里,就是“健康人里,有多少被冤枉成有病了”。
这两个指标最妙的地方在于,TPR 只看正样本内部,FPR 只看负样本内部,互相独立。所以 ROC 曲线对不同类别的样本数量并不特别敏感,这也是它能扛住一定程度的类别不平衡的原因。不过这个话题我们放到后面细说,先继续把骨骼搭完。
2.2 阈值扫描到底在扫什么
理解“画法”的本质,关键在于理解“扫描阈值”。给定一个测试集,每个样本都会有一个预测概率。我们把阈值从大到小扫一遍,每换一个阈值,样本就会被重新划分成两个阵营,相应地算出一组 TP、FP、FN、TN,再折算成一个 TPR 和一个 FPR。
光说公式太干,我们举个例子。假设有 10 个样本,真实标签和模型输出的预测概率如下表:
| 样本 | 真实标签 | 预测概率 |
|---|---|---|
| 1 | 1 | 0.90 |
| 2 | 1 | 0.75 |
| 3 | 0 | 0.60 |
| 4 | 1 | 0.55 |
| 5 | 0 | 0.50 |
| 6 | 0 | 0.40 |
| 7 | 1 | 0.35 |
| 8 | 0 | 0.30 |
| 9 | 0 | 0.20 |
| 10 | 1 | 0.10 |
这个数据集里,正样本一共 4 个,负样本一共 6 个。阈值取 0.5 时,预测概率大于等于 0.5 的样本有 1、2、3、4、5,其中真实标签为正的是 1、2、4,所以 TP = 3,FN = 1;真实标签为负却被判成正类的是 3、5,所以 FP = 2,TN = 4。于是 TPR = 3 / (3+1) = 0.75,FPR = 2 / (2+4) = 0.333。
阈值下调到 0.3,被判成正类的样本变多,TP 和 FP 会同时上升;阈值上调到 0.9,几乎没有样本能越过线,TPR 和 FPR 都趋于 0。把每个阈值下算出的(FPR,TPR)点连成一条线,就是 ROC 曲线。它本质上是一种“把模型在不同严苛程度下的表现全部摊开给你看”的可视化方式。
2.3 两个端点和那条对角线
先说端点。阈值取 1,也就是比所有样本概率都高,没有任何样本会被判成正类,TPR 和 FPR 都是 0,对应坐标轴左下角。阈值取 0,所有样本都被判成正类,所有真实正样本都被抓回来,所有真实负样本也全被冤枉,TPR 和 FPR 都等于 1,对应右上角。所以 ROC 曲线无论什么模型画出来,都一定从左下角出发、到右上角结束。
再说对角线。对角线代表“完全随机猜测”的水平。一个没有任何区分能力的模型,它判断正类的概率和误伤负类的概率是同步上升的,画出来就是一条 45 度直线。因此,比较模型时,曲线越往上凸、越贴近左上角,说明区分能力越强;如果曲线落在对角线以下,说明模型的预测方向基本是反的,还不如不判。
我给你一个特别生活化的类比:想象一个房间里混着坏人和好人,让你闭着眼睛随机指认坏人。你抓到坏人的比例,永远等于你冤枉好人的比例,这就是对角线。如果你的判断真的有效,抓到坏人的比例会明显高于冤枉好人的比例,曲线就会拱起来。ROC 曲线的画像,其实就是这个“你抓坏人能力强不强”的直观展示。
3. 实操:用Python从零画一条ROC曲线
3.1 最关键的一步:拿预测概率,别拿类别标签
画 ROC 最容易被忽略的起点,是用模型输出的连续概率,而不是直接拿预测出的 0/1 类别。我见过不止一次,有人把 model.predict() 的结果喂给画图函数,然后到处问为什么曲线不是弯的。原因很简单,当模型已经把所有样本硬切成正类和负类时,测试集上只剩下一个固定点,TPR 和 FPR 各有一个值,曲线自然退化成单点,AUC 也没有意义。
正确做法是用 predict_proba() 输出的正类概率,或者 decision_function() 输出的置信分数。无论你用的是逻辑回归、随机森林还是 XGBoost,都可以找到对等的接口。如果模型实在不输出概率,只能输出类别标签,那就没法画完整 ROC,最多只能画一个业务决策点。
3.2 手写一遍阈值扫描,建立直觉
用现成库画 ROC 当然很快,但我强烈建议新手先自己手写一遍,这样你会对“扫描阈值”这件事有真正的肌肉记忆。就用上面 10 个样本的例子,写一个最简单的循环:
import numpy as np # 真实标签和预测概率 y_true = np.array([1, 1, 0, 1, 0, 0, 1, 0, 0, 1]) y_score = np.array([0.9, 0.75, 0.6, 0.55, 0.5, 0.4, 0.35, 0.3, 0.2, 0.1]) # 手动扫三个阈值 for threshold in [0.7, 0.5, 0.3]: y_pred = (y_score >= threshold).astype(int) tp = ((y_pred == 1) & (y_true == 1)).sum() fp = ((y_pred == 1) & (y_true == 0)).sum() fn = ((y_pred == 0) & (y_true == 1)).sum() tn = ((y_pred == 0) & (y_true == 0)).sum() tpr = tp / (tp + fn) fpr = fp / (fp + tn) print(f"threshold={threshold:.1f}, TPR={tpr:.3f}, FPR={fpr:.3f}")运行结果很直观:
threshold=0.700, TPR=0.500, FPR=0.000 threshold=0.500, TPR=0.750, FPR=0.333 threshold=0.300, TPR=1.000, FPR=0.667你会发现一个非常规律的走向:阈值越低,TPR 越高,FPR 也越高;阈值越高,误伤率低了,抓回率也低了。ROC 曲线的每一个点,都是这种“严苛程度”下的业务结果快照。把这个感觉建立起来以后,再去看那些封装好的画图函数,你会觉得它们做的事其实非常简单。
3.3 用 sklearn 和 matplotlib 画标准曲线
真实项目里当然不用自己写那么多行,直接用成熟库就行。我用 sklearn 造一个模拟二分类数据,训练一个简单的逻辑回归,然后把完整的绘图流程走一遍:
import matplotlib.pyplot as plt from sklearn.datasets import make_classification from sklearn.model_selection import train_test_split from sklearn.linear_model import LogisticRegression from sklearn.metrics import roc_curve, auc # 1. 生成模拟二分类数据 X, y = make_classification( n_samples=2000, n_features=10, n_informative=5, n_redundant=2, random_state=42, class_sep=0.8 ) # 2. 划分训练集和测试集,保持正负比例一致 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=42, stratify=y ) # 3. 训练模型,拿到测试集预测概率 model = LogisticRegression(max_iter=500) model.fit(X_train, y_train) y_prob = model.predict_proba(X_test)[:, 1] # 4. 计算 FPR、TPR 和阈值 fpr, tpr, thresholds = roc_curve(y_test, y_prob) roc_auc = auc(fpr, tpr) # 5. 绘制 ROC 曲线 plt.figure(figsize=(8, 6), dpi=120) plt.plot(fpr, tpr, color='#1f77b4', lw=2, label=f'Logistic Regression (AUC = {roc_auc:.3f})') plt.plot([0, 1], [0, 1], color='gray', lw=1.5, linestyle='--', label='Random Guess (AUC = 0.5)') plt.xlabel('False Positive Rate (FPR)') plt.ylabel('True Positive Rate (TPR)') plt.title('ROC Curve') plt.legend(loc='lower right') plt.grid(alpha=0.3) plt.tight_layout() plt.show() print(f'ROC AUC: {roc_auc:.3f}')这里有几个细节值得留意。第一,stratify=y是分层抽样,让训练集和测试集里的正负比例保持一致,对曲线平滑度和评估稳定性都有正面帮助。第二,roc_curve会返回三个数组:fpr、tpr 和 thresholds,阈值的排列是从高到低,最早的阈值会把所有样本都判成负类,所以曲线的起点自然落在 (0,0)。第三,auc(fpr, tpr)本质上是用梯形积分去算曲线下面积,你也可以直接用roc_auc_score(y_test, y_prob)得到同一个结果。第四,我在图里加了网格和灰色对角线,这一步虽然简单,但汇报时能让人一眼看出模型比随机猜测好多少。
3.4 同一张图对比多个模型
实际业务里只画单条 ROC 通常不够,因为“曲线好不好”必须有参照物。我一般会把随机猜测对角线、上一个版本模型、当前模型三条线放同一张图,用颜色区分,AUC 写进图例。代码其实只需要套一层循环:
fig, ax = plt.subplots(figsize=(8, 6), dpi=120) models = { 'Logistic Regression': model_lr, 'Random Forest': model_rf, } for name, model in models.items(): prob = model.predict_proba(X_test)[:, 1] fpr, tpr, _ = roc_curve(y_test, prob) roc_auc = auc(fpr, tpr) ax.plot(fpr, tpr, lw=2, label=f'{name} (AUC = {roc_auc:.3f})') ax.plot([0, 1], [0, 1], ls='--', color='gray', label='Random Guess') ax.set_xlabel('False Positive Rate (FPR)') ax.set_ylabel('True Positive Rate (TPR)') ax.set_title('ROC Curves Comparison') ax.legend(loc='lower right') ax.grid(alpha=0.3) plt.tight_layout() plt.show()多模型对比时,我很想提醒一句:如果两条曲线交叉,AUC 的大小就不能代表一切。比如模型 A 在高召回区间有明显优势,模型 B 在低误报率区间更稳,到底选哪个,要回到业务场景里决定。你是宁愿接受更多误伤来挽回更多坏人,还是宁可漏掉一些坏人也不想打扰好客户?ROC 对比图把这种动态平衡摆在了桌面上,决策就不再是拍脑袋。
4. 读懂曲线和AUC:评估模型好坏的真正读法
4.1 AUC 的面积到底在说什么
AUC 是 ROC 曲线下的面积,网上最常见的解释是“面积越大模型越好”。这话没错,但它到底好在哪里,很多文章没说透。我用一个更容易记的解释:想象模型每次给一个正样本和一个负样本分别打分,如果正样本得分比负样本高,就计一次正确排序。AUC 在数值上等于“随机抽一个正样本和一个负样本,正样本得分比负样本高的概率”。
举个例子,AUC = 0.9,意味着随机抽一组正负样本对,模型有 90% 的概率能把正样本排到负样本前面。这个解释远比“面积”两个字直观,而且给业务方讲的时候,对方也更容易理解:原来我们在看的不是某个评分,而是模型把好坏样本分开的把握有多大。
按经验,AUC 的数值范围大致可以这样解读:
| AUC 范围 | 判断 |
|---|---|
| 0.5 | 没有区分能力,和抛硬币差不多 |
| 0.5 - 0.7 | 区分能力较弱,只适合粗筛类任务 |
| 0.7 - 0.8 | 有明显区分能力,大多数实际业务模型落在这个区间 |
| 0.8 - 0.9 | 区分能力很强,特征和算法通常都比较到位 |
| 0.9 以上 | 很强,但要先警惕过拟合、数据泄漏、测试集分布偏差 |
最后一行真想单独强调一下:真实业务里,AUC 冲到 0.95 以上,而且没有明显业务约束的场景,我第一反应不是兴奋,而是去查特征里有没有把标签泄漏进去,或者训练集和测试集是不是有交集。我见过的坏案例里,这个原因出现频率最高。
4.2 曲线形状透露的信息
AUC 是个汇总数,曲线形状才是过程细节。同一张图里,有些曲线一开始就陡峭向上,说明模型在高分段的正负样本分离度很好;有些曲线前半段平坦、后半段突然爬升,说明高分段的负样本混了很多,只能在低阈值时把正样本捞回来;还有些曲线中间出现明显的“台阶”,通常对应的场景是预测概率存在大量重复分箱,或者评估样本量太小。
如果你把多条 ROC 曲线放在一起,可能看到交叉。交叉点左侧的赢家,适合“高精确率优先”的业务;交叉点右侧的赢家,适合“高召回率优先”的业务。只看 AUC 会抹掉这个信息,所以做模型选型时,我除了记录 AUC,还会在交叉点旁边注明业务含义。比如某个信贷模型在 FPR 低于 0.1 时优势明显,那它对“宁可漏掉一点风险,也别轻易拒绝优质客户”的策略就更合适。
4.3 什么场合改用PR曲线
一个高频问题:数据极度不平衡时,ROC 还可靠吗?我的答案分两层。ROC 不会因为正负比例变化而系统性地移位,因为 TPR 和 FPR 都是条件概率,一个只跟正样本内部有关,一个只跟负样本内部有关;但不平衡很严重时,比如正样本只有 1%,负样本基数巨大,FPR 哪怕从 1% 掉到 0.1%,画在图上也只是往左上角挪一点点,肉眼看起来会“过于乐观”。这时候,PR 曲线往往是更好的补充。
PR 曲线的横轴是召回率(也就是 TPR,代表抓到了多少正样本),纵轴是精确率(预测为正类的样本里,有多少是真正类),曲线下面积叫 AP。它极度关注少数类:如果模型把大量负样本误判成正类,精确率会迅速掉下来,PR 曲线会非常诚实地告诉你模型在少数类上的真实表现。我的个人习惯是:普通二分类任务用 ROC + AUC 汇报;风控、反欺诈、故障检测这类正类极少的业务,ROC 和 PR 两条都画,一个证明整体排序能力,一个说明少数类抓取能力。两者一起看,比单看任何一条都稳妥得多。
5. 曲线画完以后:怎么用ROC选阈值
5.1 约登指数:最快的默认选择
模型上线前,必须挑一个真正使用的业务阈值。很多人以为 ROC 画完就结束了,实际上曲线本身不会替你定阈值,它只是把所有决策后果摆出来。没有明显成本偏好时,最常用的选法是约登指数。
约登指数公式是 J = TPR - FPR,我们遍历所有阈值,找到让 J 最大的那个点。它的含义可以理解为“在抓住正样本和冤枉负样本之间,取得最大净值”。用 sklearn 实现非常简单:
import numpy as np # fpr, tpr, thresholds 来自 roc_curve j_scores = tpr - fpr best_idx = np.argmax(j_scores) best_threshold = thresholds[best_idx] print(f'Best threshold by Youden Index: {best_threshold:.3f}') print(f'TPR={tpr[best_idx]:.3f}, FPR={fpr[best_idx]:.3f}')有一点提醒:有些版本的 sklearn 或者自定义实现里,thresholds 的长度可能和 fpr、tpr 不完全一致,稳妥做法是用min(len(tpr), len(thresholds))先切齐,再做最大索引计算。我在老版本环境里碰到过这个问题,当场排查了很久才发现是长度没对上。
5.2 最靠近左上角的点法
另一种常见方法是找 ROC 空间里距离理想点 (0, 1) 最近的位置:
D = sqrt((1 - TPR)^2 + FPR^2)
理想状态下,我们当然希望 TPR 尽量接近 1,FPR 尽量接近 0,离左上角越近越完美。代码和约登指数很像:
distances = np.sqrt((1 - tpr) ** 2 + fpr ** 2) best_idx = np.argmin(distances) best_threshold = thresholds[best_idx]这个方法与约登指数通常会给出接近的结果,但在样本分布不均匀、曲线不够平滑时会有细微差别。给业务方展示时,我习惯把这两个点都标在 ROC 图上,让他们直接看到“这个位置对应的 FPR 是多少,业务能不能接受”。种可视化沟通方式,往往比讲半天公式有效得多。
5.3 把业务成本写进阈值选择
如果你在风控、医疗这些场景,“漏掉坏人”和“冤枉好人”的代价通常差着数量级,那选阈值就不能用通用公式,必须把你自己的成本结构放进去。比如信贷审批场景,每误拒一个优质客户,损失的是潜在利息收入;每误放一个坏客户,损失的是本金。假设误拒一个优质客户的代价是 c1,误放一个坏客户的代价是 c2,给定某批次样本中正样本数 P 和负样本数 N,我们可以在每个阈值下计算总成本:
Cost(threshold) = c1 * (1 - TPR) * P + c2 * FPR * N
代码上,只需要把前面扫出来的每对 TPR 和 FPR 都代入这个公式,取总成本最小的阈值作为上线阈值。举个例子:某电商风控任务里,P = 100,N = 9900,c1 = 50 元,c2 = 3000 元。FPR 每上升 1%,成本增加 3000 * 9900 * 0.01 = 297000 元;而 TPR 每下降 1%,成本只增加 50 * 100 * 0.01 = 50 元。在这个设定下,FPR 的成本权重大得惊人,系统自然会倾向选择更保守、更高一点的阈值。当然了,真实成本得按自己业务精算,我只是用这个例子说明:ROC 不会替你做决策,但它把所有决策后果都摆在了你面前,选不选、怎么选,是业务的事。
6. 绘制和应用中的常见坑
6.1 用硬标签画ROC曲线会退化
前面已经强调过,再补一个自查办法。如果你画出来的 ROC 图形状很怪,比如只有一个拐点、几乎不像曲线,先检查自己是不是用了分类标签而不是 predict_proba。用 0/1 硬标签画 ROC,测试集上每个样本只有一个固定的正负判定,TPR 和 FPR 各只有一个值,AUC 自然也没有意义。这也是面试里最容易被问住的点之一,建议所有打算用 ROC 的人都先把这点刻在脑子里。
6.2 反复用同一份测试集调参导致AUC虚高
另一个很普遍的操作是:在小项目里反复用测试集画 ROC、调阈值、调特征、重新训练,最后测试集 AUC 越来越好看,模型一上线就立刻打回原形。这就是典型的测试集信息泄漏。正确做法是,数据尽量切分成训练集、验证集、测试集三份,或者使用交叉验证。如果样本量有限,可以在交叉验证的每个折上都算出 fpr、tpr,再取平均 AUC 并标注标准差。我自己在正式项目里,至少会画一条带均值曲线和正负标准差的交叉验证 ROC,这样比单次划分出的曲线可信得多。
6.3 评估集正负比例被改过,选阈值会偏
ROC 对类别比例相对不敏感,但如果你为了建模而对负样本做了大量降采样,那么测试集里的负样本比例就和真实业务分布不一致,选阈值时成本计算公式里的 N 就不真实。比如业务真实负正比是 100:1,建模时可能因为计算资源限制把负样本下采样成 1:1,这时画出的 ROC 曲线上点的坐标本身没错,但你用约登指数或最近距离点法选出的阈值,放到真实数据里 FPR 的分母会变,实际误伤规模立刻不一样。所以我的建议是:评估模型排序能力时可以用下采样数据,但最终定阈值时,一定回到真实业务分布下的测试集去验证,或者至少用真实 N 重新计算成本。
6.4 把AUC当成概率校准指标
AUC 高只说明模型排序准,不代表模型输出的概率是真实概率。概率校准是另一件事:模型可能把所有正样本都排在负样本前面,AUC 很漂亮,但预测概率整体偏高或偏低,直接拿去做额度定价、损失精算就会出问题。我之前遇到过一个模型,AUC 0.86,表面蒸蒸日上,最后发现概率严重偏置,几乎全挤在 0.5 到 0.6 之间,用来计算期望损失差点出事。所以记住:AUC 只管排序,不管绝对数值准不准。如果业务需要依赖概率本身,请另外画校准曲线,必要时做温度缩放或者保序回归校准。
6.5 多分类问题怎么画ROC
常见的标题搜索词“ROC”很容易让人误以为只能二分类用。多分类其实也能画,但思路是把每个类别分别拿出来,做 one-vs-rest 的二分类。也就是说,有三个类别时,把类别 0 当成正类、其余当负类画一条;再对类别 1、类别 2 各画一条。最终可以画多条曲线叠在一起,也可以算平均 AUC。如果要用 sklearn,可以用label_binarize把标签转成二值矩阵,再用OneVsRestClassifier包一层模型:
from sklearn.preprocessing import label_binarize from sklearn.multiclass import OneVsRestClassifier from sklearn.metrics import roc_curve, auc # 假设 y_test 是 0/1/2 多分类标签 y_test_bin = label_binarize(y_test, classes=[0, 1, 2]) model_ovr = OneVsRestClassifier(LogisticRegression(max_iter=500)) model_ovr.fit(X_train, y_train) y_score = model_ovr.predict_proba(X_test) for i in range(3): fpr, tpr, _ = roc_curve(y_test_bin[:, i], y_score[:, i]) roc_auc = auc(fpr, tpr) plt.plot(fpr, tpr, label=f'Class {i} (AUC = {roc_auc:.3f})')汇报时,我优先给出所有类别的 macro AUC,同时单独标出样本量最少的那个类别的 PR 表现,因为少数类往往才是业务最关心的对象。只用一条平均曲线,容易掩盖少数类上的糟糕表现。
6.6 忘了基线对比
最后一个小坑,其实是汇报风格问题。只画一条孤零零的 ROC 曲线,不加随机猜测对角线,也不加基准模型,很容易被挑战:它确实拱起来了,但拱了多少?所以在任何正式报告里,至少要画一条灰色对角线,最好再跑一个简单的 DummyClassifier 作为对照。如果评估样本量非常小,还可以用 bootstrap 方法重复抽样,计算 AUC 的置信区间,画成阴影带放在图里。这个小动作会显著提高你汇报内容的说服力,属于投入小、收益高的细节。
最后分享一个我在实际项目里培养出来的习惯:拿到一条 ROC 之前,我先不急着看图,而是打印 fpr、tpr、thresholds 数组的开头和结尾,看看样本量、阈值分布是否正常。然后我会在图上标出业务成本最低的那个阈值点,而不是默认放一个 0.5。最近一次信贷模型验收,我甚至把十几个候选阈值对应的 FPR 绝对数量打印出来,拿给业务方看,他们现场就明白了为什么阈值不能定得太低,也理解了 ROC 曲线不是一个学术摆设,而是每一个业务决策点都能落地的操作地图。希望你下一次画 ROC 时,也能把它用到这个份上。