深夜整理笔记的时候,正好把Day 17的内容过完一遍,主题是ROC曲线和PR曲线。这两个名字在机器学习里出现频率极高,但说实话,刚接触时我一度分不清它们到底有什么区别,更别说在什么场景下该用哪个。前两天我跟着浙大那边的课程资料刷了一遍分类模型的评估,越往深看越觉得,ROC曲线和PR曲线不是“画个图”那么简单,它们背后其实是两种看待模型的方式,搞懂了再去做样本不平衡、阈值选择、模型对比,整个人会通透很多。
这篇文章不是复读机式的概念科普,我按自己踩坑和验证的思路把ROC曲线和PR曲线从原理到代码到使用场景完整拆了一遍,顺便附上了可以直接跑的Python代码,以及几个网上教程不太会提的细节坑。适合正在学机器学习、做过分类任务但对评估指标还比较模糊的朋友,看完你可以直接拿这套逻辑去评估自己的模型。
1. 从“只看准确率”到“看曲线”——先搞清这些基础指标
聊ROC曲线和PR曲线之前,得先把它们背后的地基搭好。很多人拿到分类模型,第一反应就是看accuracy,也就是准确率,然后觉得80%、90%就万事大吉。但真实业务里这是最容易翻车的习惯,尤其是正负样本比例不平衡的时候,准确率会给你一种“模型很牛”的错觉。
我举个例子你马上就能明白。假设我们要识别用户是不是高风险逾期用户,真实数据里逾期的人可能只占1%,剩下99%都是正常用户。这时候我写一个“无脑预测全部正常”的模型,准确率是多少?99%。听上去特别厉害对吧?但它是废物,因为它一个逾期用户都抓不出来。这就是为什么我们不能只看准确率,而要看更细的指标。
1.1 混淆矩阵:一切曲线的地基
ROC曲线和PR曲线的所有计算,本质上都来自混淆矩阵。这个矩阵长得像一张四象限表,横轴是预测类别,纵轴是真实类别。四个格子分别叫:
- TP(True Positive):真实为正,预测也为正
- TN(True Negative):真实为负,预测也为负
- FP(False Positive):真实为负,预测为正,也就是“误报”
- FN(False Negative):真实为正,预测为负,也就是“漏报”
这四个值单独拿出来都没什么感觉,但它们一组合,就能算出我们关心的各种比率。网上很多公式写得复杂,其实核心就几行。真正要记住的是,TP、FP、FN、TN会随着分类阈值的变化而变化,所以基于它们算出来的指标也全都依赖于阈值。
这里有个细节我得特别强调:混淆矩阵里的“正”和“负”,一定要先明确谁是正。比如逾期预测里,逾期是正样本,正常是负样本;垃圾邮件识别里,垃圾邮件是正样本,正常邮件是负样本。正样本的定义直接决定后面所有曲线怎么画,定义反了曲线就会反着跑,代码虽然能跑通,但结果完全是错的。
1.2 四个基础指标:TPR、FPR、Precision、Recall
从混淆矩阵出发,四个最核心的指标分别是:
- TPR(True Positive Rate),也叫召回率Recall、敏感性Sensitivity,公式是TP / (TP + FN),表达的是“真实的正样本里,模型成功预测出了多少”。这个指标关心的是别漏掉正样本。
- FPR(False Positive Rate),公式是FP / (FP + TN),表达的是“真实的负样本里,有多少被模型误判成了正样本”。这个指标关心的是别误伤负样本。
- Precision(精确率),公式是TP / (TP + FP),表达的是“模型预测为正的样本里,真正是正样本的比例”。这个指标关心的是预测结果可信不可信。
- Accuracy,公式是(TP + TN) / 总样本数,虽然它最常用,但样本不平衡时最不靠谱。
这四个指标里,TPR和FPR是ROC曲线的横纵坐标,Precision和Recall是PR曲线的横纵坐标。所以你可以把这两条曲线理解为“不同视角下的模型体检报告”,一个从正样本命中率与负样本误伤率的博弈关系切入,一个从精确与召回的关系切入。
当时我看到这里的时候,脑子里冒出来的一个类比是:TPR和FPR像是一个安检系统的“抓坏人能力和误抓好人概率”,你当然希望坏人抓得多,同时好人别被冤枉;Precision和Recall像是一个搜索系统“搜出来的结果准不准”和“想要的结果有没有被搜全”,这两个有时是矛盾的,提升一个另一个就会掉。后面所有曲线,本质上都是在描述这种此消彼长的权衡。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. ROC曲线与AUC:分类器的“全阈值体检报告”
很多人第一次接触ROC曲线时会觉得它很玄,横轴FPR,纵轴TPR,曲线左下角到右上角,然后看曲线下的面积。其实它的思路特别朴素:任何一个分类模型在输出预测结果前,内部都会算出一个分数,比如概率0.7、0.3、0.9,然后我们再拿一个阈值去切分,比如阈值设0.5,大于等于0.5判为正,小于0.5判为负。
问题来了,这个阈值是谁定的?为什么一定要是0.5?其实0.5只是默认习惯,并不一定适合所有场景。有些场景里,我们宁肯误报也要把正样本找出来,那阈值就要调低;有些场景里误报代价极大,那阈值就得调高。ROC曲线的精髓在于:它把所有可能的阈值都跑了一遍,然后把每个阈值下的FPR和TPR画在同一个坐标轴上,这样我们就可以抛开某一个特定阈值,从全局去看这个模型的分类能力。
2.1 ROC曲线到底在画什么
ROC曲线的横轴是FPR(假正率),纵轴是TPR(真正率)。每给定一个阈值,我们就能算出一组(FPR, TPR),把它作为曲线上的一个点。把所有阈值从高到低遍历一遍,就能连出一条从(0,0)到(1,1)的曲线。
为什么从(0,0)开始?因为当阈值设成最高的时候,模型啥也不预测为正,TP和FP都是0,所以FPR和TPR都是0。为什么到(1,1)结束?因为当阈值设到最低的时候,所有样本都被预测为正,TPR=1,FPR也=1。中间的过程就是“逐步放开手,让更多样本被预测为正”,观察TPR涨得快还是FPR涨得快。
这里有个关键认知:ROC曲线长得什么样,完全取决于模型给正样本和负样本打分时的分布分离程度。如果模型足够好,正样本的分数普遍高、负样本的分数普遍低,那么在某个阈值下,正样本大量被识别出来,同时负样本还没被误伤多少,曲线就会明显弓向左上角。如果模型就是个摆设,分数分布完全重叠,那不管怎么调阈值,TPR和FPR都是同步增长,曲线就贴在对角线上。
所以判断模型好坏的标准也随之而来:曲线越靠近左上角越好,越靠近对角线越差。但肉眼看曲线总归不够量化,于是就有了AUC。
2.2 阈值滑动与AUC的统计学意义
AUC,全称Area Under the ROC Curve,就是ROC曲线下的面积,取值范围在0到1之间。AUC=1是完美模型,AUC=0.5相当于随机猜,AUC<0.5说明模型还不如随机,通常是因为正负标签定义反了或者模型学反了。
但AUC的另一个解释我觉得更值得记住:AUC实际上等于“随机抽一个正样本和一个负样本,模型给正样本打的分比负样本高的概率”。这个解释我第一次看到时觉得特别惊艳,它把曲线下的面积变成了一个很有物理意义的事件概率,理解了这一点,你会突然明白为什么AUC对样本不平衡不那么敏感——因为它看的是排序关系,而不是绝对分数。
实际操作中,sklearn已经帮我们封装好了一切,我们根本不需要手动去遍历阈值。在Python里几行代码就搞定:
python复制import numpy as np
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, roc_auc_score
# 生成一份模拟数据,设置好样本比例
X, y = make_classification(
n_samples=1000,
n_features=10,
n_informative=8,
n_redundant=2,
weights=[0.90, 0.10], # 10%的正样本,制造不平衡
random_state=42
)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42, stratify=y
)
# 训练一个逻辑回归
model = LogisticRegression(max_iter=1000)
model.fit(X_train, y_train)
# 注意:这里要的是预测概率,尤其是正类那一列的概率
y_score = model.predict_proba(X_test)[:, 1]
# 计算ROC曲线的坐标以及AUC
fpr, tpr, thresholds = roc_curve(y_test, y_score)
auc_score = roc_auc_score(y_test, y_score)
# 绘制ROC曲线
plt.figure(figsize=(7, 6))
plt.plot(fpr, tpr, label=f'ROC curve (AUC = {auc_score:.3f})')
plt.plot([0, 1], [0, 1], 'k--', label='Random guess')
plt.xlabel('False Positive Rate (FPR)')
plt.ylabel('True Positive Rate (TPR)')
plt.title('ROC Curve')
plt.legend()
plt.grid(alpha=0.3)
plt.show()
这段代码基本是每次做二分类评估时我都会先用的一把尺子。生成一份含10%正样本的不平衡数据,训练逻辑回归,拿到预测概率,然后画ROC曲线。跑完之后你大概率会看到AUC在0.9上下,曲线明显凸起。这里要注意predict_proba返回的是一个二维数组,第一列是负类概率,第二列才是正类概率,很多人第一次写的时候直接[:, 1]没注意,结果拿反了,曲线会反过来,画出来像是对角线的镜像,AUC也会小于0.5。
2.3 多分类场景下的ROC拓展
上面的代码只针对二分类,但实际工作中多分类任务也特别常见,比如识别手写数字0-9、对新闻做分类。多分类怎么画ROC曲线?答案是把问题拆成“一对多”的二分类,然后算每个类别下的ROC和AUC,再聚合。
聚合方式常见的有两种。一种是macro,先把每个类别的TPR和FPR分别算出来,然后直接取平均;另一种是micro,把所有类别的TP、FP、FN、TN加起来,再用合并后的值统一算TPR和FPR。两种方式各有倾向,macro对每个类别一视同仁,适合类别分布相对均匀的情况;micro受样本量大的类别影响更大,适合样本极不平衡的情况。Sklearn提供了一个很便捷的函数roc_auc_score,只要传入多分类标签和预测概率矩阵,再指定multi_class='ovr'或者multi_class='ovo'即可,后者是两两配对的计算方式。
不过我心里一直有一个观点:多分类ROC曲线虽然能画,但解释起来难度是直线上升的,图上同时画10条曲线,非技术背景的人基本看不懂。所以如果是做业务汇报,我一般更倾向输出每个类别的AUC表格,再挑几个重点类别单独可视化,效果反而更好。
3. PR曲线:不平衡数据下的“另一双眼睛”
前面我说ROC曲线在样本不平衡时“相对稳定”,但它也有一个被诟病多年的盲区:当负样本非常多时,FPR的分母会变得极大,导致FPR的变化被稀释。什么意思?比如10000个负样本里,模型多误判了10个,FPR从0.001涨到0.002,在ROC曲线上几乎看不出变化,可这10个误判在实际业务里可能就意味着10个正经用户被冤枉了。所以一旦你面对的场景里负样本远多于正样本,或者你更关心“预测为正的人里真正有几个是正的”,PR曲线会更合适。
3.1 PR曲线的构成与解读
PR曲线的横轴是Recall(召回率),纵轴是Precision(精确率)。它描绘的是“当我们想抓住越来越多正样本时,预测结果的精确程度会如何变化”。
曲线一开始,阈值很高,模型只敢把最像正样本的少量样本判为正,此时Precision通常很高,但Recall很低。随着阈值降低,越来越多的样本被判为正,Recall不断提升,但混进来的负样本也在增加,Precision就会下降。PR曲线的形状可以很直观地说明这个模型在高召回区域的表现怎么样:如果曲线下降平缓,说明模型在保证召回的同时还能维持不错的精确率,业务端就敢放心用;如果曲线掉得飞快,说明模型在高召回区域会产生大量误报,落地时要谨慎。
PR曲线上还有一个非常关键的参照线,就是“无技能模型线”,它是一条水平线,位置等于正样本占比。比如正样本占10%,那么随便乱猜的模型Precision永远只有10%,PR曲线就会贴着这条水平线附近波动。样本越不平衡,这条线越低,PR曲线可进步的空间就越大。这也是PR曲线和ROC曲线一个极大的区别:ROC曲线的基准线永远是固定的对角线,而PR曲线的基准线会随着正样本占比变化而移动。
3.2 什么时候该用PR曲线而不是ROC曲线
我给一个很实用的经验判断方法:当正样本占比很小或者你非常关心正样本识别效果时,优先看PR曲线;当正负样本比例相对均衡、且需要全面评估排序能力时,用ROC更稳妥。但这里要说一个容易让人困惑的点:是不是正样本占比小就一定用PR曲线?其实不是绝对的,更需要看业务目标。
举两个极端例子。第一个是垃圾邮件识别,正样本是垃圾邮件,可能只占2%,但误报一封正常邮件比漏掉一封垃圾邮件的代价更高,这时候你更关心的是“模型预测为垃圾邮件的样本里,有多少真是垃圾邮件”,那Precision就是第一指标,PR曲线更适合。第二个是金融风控里的欺诈检测,正样本是欺诈交易,占比极低,漏掉一笔欺诈可能损失很大,这时候你可能更关心“能不能把欺诈交易都找出来”,也就是Recall优先,但同时误报会造成客诉,所以Precision、Recall要一起看,PR曲线能帮你直观权衡。
反过来,如果业务场景里正负样本比例是1:1,或者我们只关心模型排序能力的整体表现,那ROC曲线加上AUC依然是最清晰直观的选择。所以说,ROC和PR不是谁替代谁的关系,而是互补的关系,一个从全局排序能力出发,一个从正样本识别精确度出发。
下面是一张我在笔记里反复用的对照表,我自己觉得特别好用:
| 对比维度 | ROC曲线 | PR曲线 |
|---|---|---|
| 横轴 | FPR(假正率) | Recall(召回率) |
| 纵轴 | TPR(真正率) | Precision(精确率) |
| 受样本不平衡影响 | 较小 | 较大 |
| 随机模型基准线 | 对角线 | 正样本占比水平线 |
| 更关注点 | 正负样本排序能力 | 正样本识别的精确与召回权衡 |
| 典型适用场景 | 业务整体效果好坏的评估 | 严重不平衡、关心正样本误判代价 |
3.3 PR曲线代码实现与AP值
PR曲线的代码和ROC曲线几乎同构,区别在于换了两个指标,在sklearn里也对应不同的函数。我习惯把ROC和PR的代码写在同一个notebook里,方便对比同一个模型在两条曲线下的表现差异。
python复制from sklearn.metrics import precision_recall_curve, average_precision_score
# 计算PR曲线的坐标
precision, recall, thresholds = precision_recall_curve(y_test, y_score)
# 计算AP值:PR曲线下的面积
ap_score = average_precision_score(y_test, y_score)
# 正样本占比,作为无技能模型基准线
baseline = y_test.mean()
plt.figure(figsize=(7, 6))
plt.plot(recall, precision, label=f'PR curve (AP = {ap_score:.3f})')
plt.axhline(y=baseline, color='gray', linestyle='--', label=f'Random (Positive ratio = {baseline:.2f})')
plt.xlabel('Recall')
plt.ylabel('Precision')
plt.title('Precision-Recall Curve')
plt.legend()
plt.grid(alpha=0.3)
plt.show()
跑完这段代码,你大概率会看到一个很有意思的现象:同一个模型,ROC曲线下面积极高、看起来很完美,但PR曲线却“缩”在右下角,或者曲线下的面积没那么亮眼。这种现象在正样本特别少时尤其明显,比如正样本只占1%,随机水平线就压在y=0.01的位置,模型就算再努力,PR曲线也不会飞到天上去。这不是模型坏了,而是两个指标看待模型的角度不同。
关于AP值,它全称是Average Precision,可以理解为PR曲线下的面积。和AUC一样,AP也是把二维曲线压缩成一个数字,但它的计算方式和AUC不完全相同,它对Precision在高Recall区域的表现更敏感。Sklearn里的average_precision_score其实是对每个阈值点的Precision做加权平均,具体来说就是以Recall的变化量作为权重来加权Precision,召回变化越大的地方权重越重。实操中我不一定每次都手动算AP,但做算法对比时,AP和AUC并列输出几乎成了我的默认选项。
4. 冷门但关键的细节:随机基准线、多分类与易错点
写到这里,核心原理和代码都已经讲完了,但我还是想把几个特别容易踩的坑单独拎出来说。这些坑在教科书里很少被强调,但在实际跑代码和解释结果时几乎必然遇到。
4.1 随机基准线为什么不一样
很多人第一次画PR曲线时都会疑惑:为什么ROC曲线的基准线是默认对角线,而PR曲线的基准线不是?这其实不是哪个对哪个错的问题,而是两种曲线定义的数学性质不同。
ROC曲线的横轴FPR和纵轴TPR,两个指标都是一种“率”,取值范围都是0到1,对于随机分类器来说,不管阈值怎么调,TPR和FPR总是同步变化,所以随机模型会落在这条对角线上。PR曲线则不一样,Precision和Recall之间并不是简单的线性关系。随机模型的Recall可以随着阈值降低从0涨到1,但Precision会一直稳定在正样本占比附近,所以随机模型在PR曲线上是一条水平线,而不是对角线。这就造成了一个结果:样本分布一旦发生变化,PR曲线的基准线就会跟着变,而ROC曲线不会。在写论文或者做汇报时,如果拿两个不同数据集上的PR曲线互相比较,一定要先说明正样本占比,否则对比是不公平的。
4.2 多分类曲线别硬画
前面提到多分类可以用一对多的策略分别画ROC曲线,但说句心里话,当类别超过5个的时候,我基本不推荐画“万箭齐发”的曲线图。原因有三:一是图面信息量过大,每根曲线之间的距离和重叠关系很难用肉眼判断;二是宏平均微平均的取值逻辑在汇报时要额外解释,非专业人员容易一头雾水;三是曲线多起来了之后,少数类的曲线波动会特别剧烈,画出来毛刺很多,视觉上也不好看。
更务实的做法是输出一个类别维度的AUC或AP表,按数值降序排列,一眼就能看出哪些类别容易被混淆、哪些类别已经学得不错。如果真的想可视化,我建议挑一个最有业务价值的类别单独画,再配一张混淆矩阵就够了。这样信息密度高,又不至于让图形变成一团乱麻。
4.3 实操中容易踩的坑清单
这一块是我最想分享的,因为每个坑都是我实际代码跑歪之后才明白的。整理成清单放在下面:
- 用了
decision_function的结果去画曲线而不是predict_proba。很多模型例如SVM默认输出的是距离,不是概率,正样本的距离可能是正数,负样本是负数,如果你把距离直接传给roc_curve,曲线大概率也能画出来,但阈值含义和概率完全不同,auc_score可能让你误以为模型很差。用SVM时建议显式设置probability=True,再取概率。 - 正样本是1还是0搞反。
roc_curve和precision_recall_curve都默认把标签数值较大的类别当作正类,如果你自己构造的标签是-1和1,或者正样本标记为0,sklearn可能直接报错或者行为异常。我的习惯是统一把正样本重映射成1,负样本映射成0。 - 交叉验证时直接拼接所有fold的预测概率来计算AUC/AP。这个操作会把不同fold的预测分数混在一起,而不同fold的模型校准程度可能不一致,算出来的AUC会偏乐观。正确做法是在每个fold内部单独计算对应指标,再取均值。
- 极度不平衡时只看AUC不看AP。举个例子,正样本占0.1%,模型把所有样本都预测为负,AUC其实还是0.5,但如果你用一个稍微能识别一点正样本的模型,AUC可能飙升到0.98。这看起来很强,但AP可能只有0.001,说明实际效果依然拉胯。这种时候AP才是那个说实话的指标。
- 画PR曲线时横纵坐标写反。因为ROC习惯了横轴FPR纵轴TPR,画PR曲线时很容易顺手把Precision写到横轴。一旦写反,曲线形状会让人误判模型效果,我在刚开始学习时犯过不止一次。
这些坑单独拎出来都特别小,但任中一个踩中,后续的所有结论都可能跑偏。所以我现在跑评估代码时,都会先做一个最小数据集的“冒烟测试”,用几十条样本确认曲线形状符合预期,再放手跑全量数据。
最后再分享一点心得
说实话,在学习ROC曲线和PR曲线的过程中,我最大的感悟不是“记住了几个公式”,而是学会了一个习惯:评估模型之前,先问自己业务到底关心什么。你是怕漏掉坏人,还是怕冤枉好人?是想提升整体排序能力,还是想在正样本极少的场景里压榨出每一分识别能力?这两个问题的答案,直接决定了你应该看ROC还是PR。
Day 17这一期的内容,看起来只是两个评估指标的小节,但对我后面理解阈值选择、模型对比、样本不均衡处理都起了很大的作用。而且这两条曲线虽然名字不同,底层却共享同一套混淆矩阵的逻辑,把地基打牢了,后面学多分类评估、校准曲线、提升曲线都会轻松很多。希望这篇笔记能帮正在学分类模型评估的朋友少走几个弯路。
