简介面向需要评估二分类模型性能的 Python 开发者和机器学习初学者这份 81KB 的 PDF 文档围绕 ROC 曲线和 AUC 值系统讲解二分类模型效果评估的原理、步骤与代码实现从基础概念到工程落地均有直观说明。压缩包共 1 个 PDF 文件内容覆盖 TPR、FPR 概念、不同阈值下坐标点构建、曲线绘制及 AUC 面积计算既演示 sklearn.metrics.roc_curve 与 auc 的快速调用也提供了可自行落地的自定义 AUC 计算脚本并说明了输入数据格式与适用边界适合在没有现成评估模块时参照实现。文档还特别指出自定义方法仅限二分类场景、逐样本遍历效率较低等注意事项并给出采样或等距划分阈值等优化思路。目前已有 16263 人学习下载若想深入掌握 ROC/AUC 评估原理并独立完成绘图与指标计算这是一份可直接阅读的 PDF 学习资料。1. 从二分类到模型体检ROC曲线和AUC值到底在衡量什么做分类模型的人迟早会撞上一个问题准确率90%的模型上线后却在正样本占比只有5%的业务里表现一塌糊涂。这不是模型退化而是评估指标选错了。ROC曲线和AUC值的价值在于它们不依赖固定阈值也不受正负样本比例波动的影响能在不调整业务规则的前提下给出模型对正负样本的排序能力评估。对于信贷风控、医疗诊断、推荐系统这类正负样本极不均衡的场景ROC和AUC几乎是标配的模型体检工具。用Python画ROC曲线并计算AUC值本质上只需要三样东西模型预测的概率值、真实标签、以及sklearn里两个函数。但要把曲线画对、把AUC算准、把多分类问题处理明白里面有不少细节值得展开。这篇文章从判别原理讲起给出最小可运行的代码再把阈值遍历、K折交叉验证、多分类处理这三个高频场景逐一拆开。2. 阈值、混淆矩阵与ROC曲线的绘制原理2.1 为什么固定阈值会掩盖模型的真实能力逻辑回归、随机森林、XGBoost这些分类模型输出的原始结果是连续概率而不是最终的0/1类别。业务上要得到类别必须设定一个阈值比如概率大于0.5判为正样本。这个阈值一改混淆矩阵里的TPR和FPR就会跟着变。准确率、精确率、召回率都是某个固定阈值下的快照换个阈值这些指标全部重算。这带来一个麻烦模型调参时我们很难说清楚一个阈值下的表现好到底是模型本身排序能力强还是阈值刚好选得巧。ROC曲线绕开了这个问题。它把阈值从1.0到0.0逐步遍历每一步算出一对FPR和TPR把所有点连成一条曲线。这条曲线不依赖具体阈值只反映模型对正负样本的排序能力。AUC就是这条曲线下的面积数值上等于随机抽一个正样本和一个负样本模型给正样本打更高分的概率。2.2 TPR和FPR的计算过程先看混淆矩阵的四个格子TP真实为正预测为正FP真实为负预测为正FN真实为正预测为负TN真实为负预测为负TPR真正率也叫召回率是TP除以TP加FN衡量的是所有正样本里有多少被找出来了。FPR假正率是FP除以FP加TN衡量的是所有负样本里有多少被误判为正。ROC曲线的横轴是FPR纵轴是TPR。极端情况下阈值设为1.0所有样本都判为负TPR和FPR都是0曲线从左下角开始阈值设为0.0所有样本都判为正TPR和FPR都是1曲线到右上角结束。模型如果没有任何区分能力曲线会贴着对角线走AUC约等于0.5。曲线越往左上角拱说明在同样的FPR水平下能拿到更高的TPRAUC越接近1.0。2.3 用sklearn画第一条ROC曲线的完整代码选一个不需要训练的分类器来演示画图逻辑会更清晰。这里直接用sklearn里的DummyClassifier配合随机概率或者用一个预先训练好的逻辑回归模型都行。下面是最小可运行的版本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, auc # 生成一个二分类数据集600个样本5个特征 X, y make_classification( n_samples600, n_features5, n_classes2, n_clusters_per_class1, random_state42 ) # 划分训练集和测试集保持类别比例 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, stratifyy, random_state42 ) # 训练逻辑回归模型 model LogisticRegression(max_iter1000) model.fit(X_train, y_train) # 获取测试集的正类概率取第二列 y_score model.predict_proba(X_test)[:, 1] # 计算ROC曲线的横纵坐标和阈值 fpr, tpr, thresholds roc_curve(y_test, y_score) roc_auc auc(fpr, tpr) # 绘制ROC曲线 plt.figure(figsize(8, 6)) plt.plot(fpr, tpr, colordarkorange, lw2, labelfROC curve (AUC {roc_auc:.4f})) plt.plot([0, 1], [0, 1], colornavy, lw2, linestyle--, labelRandom guess) plt.xlim([0.0, 1.0]) plt.ylim([0.0, 1.05]) plt.xlabel(False Positive Rate) plt.ylabel(True Positive Rate) plt.title(Receiver Operating Characteristic Curve) plt.legend(loclower right) plt.grid(alpha0.3) plt.show()这段代码的关键点predict_proba返回的是一个二维数组第一列是负类概率第二列是正类概率ROC曲线需要的是正类概率所以取[:, 1]。roc_curve函数返回三个值fpr和tpr是对应每个阈值的坐标点thresholds是实际被遍历到的阈值序列。auc(fpr, tpr)用的是梯形法则做数值积分也可以用roc_auc_score(y_test, y_score)一步到位。提示如果二分类标签不是0和1比如是字符串yes和noroc_curve会按字母序识别正类。更稳妥的做法是在调用前确认positvie_label参数。2.4 从thresholds数组反推业务阈值thresholds数组里有个容易被忽略的细节它的长度和fpr、tpr一样每个元素对应一个判定阈值。当模型输出概率大于等于这个阈值时判为正样本。数组的第一个值通常是无穷大inf对应无人被判为正的极端状态。这个数组的价值在于可以直接做业务侧的阈值选择。比如风控场景希望FPR控制在5%以内就去thresholds里找FPR最接近0.05的那个位置对应的阈值就是业务规则里要设的门槛# 找FPR最接近0.05的阈值位置 target_fpr 0.05 idx np.argmin(np.abs(fpr - target_fpr)) print(fFPR{fpr[idx]:.4f} 时TPR{tpr[idx]:.4f}阈值{thresholds[idx]:.4f})这样选出的阈值比固定0.5更符合业务约束。很多团队上线模型时只用0.5做默认阈值白白丢掉了对业务容错空间的掌控。3. 用Python手写AUC计算逻辑与sklearn结果对照3.1 为什么AUC等于秩和统计量sklearn的roc_auc_score底层不是画曲线再算面积而是用Mann-Whitney U统计量的变体。不展开公式只记结论把所有正样本的预测分数和所有负样本的预测分数两两比较统计正样本分数大于负样本分数的比例这就是AUC。如果正样本分数全高于负样本AUC就是1.0如果完全随机AUC接近0.5。这个定义的工程意义在于计算AUC不需要遍历阈值只需要对分数排序。排序的计算复杂度是O(n log n)而遍历阈值后逐点做梯形积分复杂度也是同一个量级但秩和法数值稳定性更好也不受阈值选取密度影响。3.2 纯手写版AUC计算为了确认AUC的真实语义可以用排序法手写一遍import numpy as np from sklearn.metrics import roc_auc_score def auc_by_rank(y_true, y_score): 用秩和法手写AUC y_true: 真实标签数组正样本为1负样本为0 y_score: 模型输出的正类概率 # 按预测分数排序分数高的排后面 order np.argsort(y_score) sorted_y y_true[order] # 统计正样本总数量和负样本总数量 n_pos np.sum(y_true 1) n_neg np.sum(y_true 0) # 特殊情况处理只有一类样本 if n_pos 0 or n_neg 0: return 0.5 # 正样本的秩之和秩从1开始 ranks np.arange(1, len(y_true) 1) pos_rank_sum np.sum(ranks[sorted_y 1]) # AUC (正样本秩和 - 正样本数*(正样本数1)/2) / (正样本数*负样本数) auc_value (pos_rank_sum - n_pos * (n_pos 1) / 2) / (n_pos * n_neg) return auc_value # 构造一组真实标签和预测分数 y_true np.array([1, 0, 1, 0, 1, 0, 0, 1]) y_score np.array([0.9, 0.3, 0.8, 0.4, 0.75, 0.2, 0.35, 0.85]) manual_auc auc_by_rank(y_true, y_score) sklearn_auc roc_auc_score(y_true, y_score) print(f手写AUC: {manual_auc:.6f}) print(fsklearn AUC: {sklearn_auc:.6f})手写版的逻辑是按分数排序后正样本的秩和越大说明正样本整体排在负样本后面AUC越高。公式里的n_pos * (n_pos 1) / 2是在减去正样本内部互相比较产生的基础秩和剩下的就是正样本在跨类别比较中赢过的次数。这个实现没有处理分数相等的情况实际数据里如果大量并列分数需要用平均秩处理sklearn内部已经做了这一步。3.3 什么时候手写比调库更合适绝大多数场景直接调roc_auc_score就够了。手写逻辑的意义在于排查异常结果。比如训练集和测试集分布差异大时AUC偏低先算一遍手写版确认不是函数调用的问题。再比如评分卡模型要求输出单调性验证需要手动给分数分箱后计算KS和AUC这时候理解秩和定义直接写SQL或pandas聚合比调sklearn更灵活。另一个值得关注的是样本量极小时AUC的方差。假设只有10个正样本10个负样本AUC可能因为一次排序变动产生很大波动。这种情况不要只看AUC数值要配合置信区间。自助法抽样2000次计算AUC的标准差比单次数值可靠得多。3.4 绘制ROC曲线时的中文显示与样式调整画ROC曲线最常见的翻车现场是中文标签乱码。matplotlib默认字体不含中文字符需要显式指定plt.rcParams[font.sans-serif] [SimHei, Arial Unicode MS] plt.rcParams[axes.unicode_minus] False第一行指定中文字体第二行修复负号显示异常。另外建议把线条加粗、对角线用虚线、网格透明度调到0.3这些在配色和可读性上对汇报材料很有帮助。曲线重叠严重时可以手动给每条线标注AUC数值比图例更直观。4. K折交叉验证下绘制ROC曲线与计算平均AUC4.1 为什么单次划分的ROC曲线不够可信单次train_test_split存在两个问题一是划分随机性导致AUC波动某次划分恰好把困难样本全分到测试集AUC就偏低二是样本量小时测试集太小曲线不平滑。K折交叉验证能充分利用数据。对每一折训练模型在验证集上得到一组fpr、tpr最终把所有折的曲线画在一起或者插值平均后画一条综合曲线。4.2 基于KFold的交叉验证ROC曲线完整代码import numpy as np import matplotlib.pyplot as plt from sklearn.datasets import make_classification from sklearn.model_selection import KFold from sklearn.linear_model import LogisticRegression from sklearn.metrics import roc_curve, auc # 生成数据 X, y make_classification( n_samples800, n_features8, n_classes2, n_clusters_per_class1, random_state7 ) # 5折交叉验证 kf KFold(n_splits5, shuffleTrue, random_state42) model LogisticRegression(max_iter2000) tprs [] aucs [] mean_fpr np.linspace(0, 1, 100) plt.figure(figsize(8, 6)) for fold, (train_idx, val_idx) in enumerate(kf.split(X), 1): X_train, X_val X[train_idx], X[val_idx] y_train, y_val y[train_idx], y[val_idx] model.fit(X_train, y_train) y_score model.predict_proba(X_val)[:, 1] fpr, tpr, _ roc_curve(y_val, y_score) roc_auc auc(fpr, tpr) aucs.append(roc_auc) # 插值到统一横坐标便于计算均值 interp_tpr np.interp(mean_fpr, fpr, tpr) interp_tpr[0] 0.0 tprs.append(interp_tpr) plt.plot(fpr, tpr, lw1, alpha0.4, labelfFold {fold} (AUC {roc_auc:.3f})) # 计算平均TPR和标准差 mean_tpr np.mean(tprs, axis0) mean_tpr[-1] 1.0 mean_auc auc(mean_fpr, mean_tpr) std_auc np.std(aucs) # 绘制平均ROC曲线 plt.plot(mean_fpr, mean_tpr, colorcrimson, lw2.5, labelfMean ROC (AUC {mean_auc:.3f} ± {std_auc:.3f})) # 绘制标准差范围带 std_tpr np.std(tprs, axis0) tprs_upper np.minimum(mean_tpr std_tpr, 1) tprs_lower np.maximum(mean_tpr - std_tpr, 0) plt.fill_between(mean_fpr, tprs_lower, tprs_upper, colorgrey, alpha0.2, label±1 std dev) plt.plot([0, 1], [0, 1], k--, lw1) plt.xlim([0.0, 1.0]) plt.ylim([0.0, 1.05]) plt.xlabel(False Positive Rate) plt.ylabel(True Positive Rate) plt.title(K-Fold Cross-Validation ROC) plt.legend(loclower right, fontsize8) plt.grid(alpha0.3) plt.show() print(f平均AUC: {mean_auc:.4f} ± {std_auc:.4f})这段代码有几个关键设计。np.interp把每一折的tpr插值到固定的100个fpr点上因为不同折的fpr采样点不一样直接求均值会错位。插值后再求均值、标准差最后用fill_between画置信带这样汇报时能直接说明模型稳定性。代码里每个fold单独一条淡色曲线平均曲线用深色加粗看的人能同时获得每折细节和整体趋势。提示KFold默认不做分层抽样。分类问题建议用StratifiedKFold保持每一折的正负样本比例与全量一致避免某折全是正样本导致AUC失真。4.3 交叉验证AUC与业务评价的衔接交叉验证得到的AUC标准差如果超过0.05说明模型在不同数据子集上表现波动大优先检查特征稳定性、是否存在时序泄漏、以及样本量是否过小。如果只关心线上效果预估可以直接用验证集的AUC均值。如果要进一步做模型比较可以在同一套交叉验证划分下对每个模型计算AUC再配对检验差异是否显著。sklearn里没有直接做配对检验的函数可以自己在fold层面构造差异序列用t检验或Wilcoxon符号秩检验样本量不大时后者更稳健。4.4 结合学习曲线的AUC变化趋势交叉验证的AUC如果偏低先区分是高偏差还是高方差。简单做法是绘制训练集大小与AUC的关系横轴是训练样本量纵轴是验证集AUC同时画出训练集AUC。两条曲线都低说明模型欠拟合需要增加特征或换更强的模型训练集AUC高而验证集低且差距大说明过拟合考虑正则化、剪枝或增加数据。这个诊断配合ROC曲线使用比单独看一个AUC数值更有把握。5. 多分类ROC曲线绘制与AUC计算的三种策略5.1 One-vs-Rest和One-vs-One的区别多分类问题的ROC曲线不能直接用二分类的那套函数sklearn的roc_curve只接受二分类标签。常见做法是拆成多个二分类任务。One-vs-Rest把每个类别当作正类其余全部当作负类几个类别就画几条曲线。One-vs-One则是在每两个类别之间做一次二分类曲线数量更多绘制成本更高。实际工程中用One-vs-Rest最常见因为曲线条数等于类别数解释起来直观。微平均micro-average把所有类别的TP、FP汇总后计算一个整体AUC适合类别不平衡严重的情况。宏平均macro-average对每个类别的AUC求算术平均更关注小类的表现。5.2 一对多策略下绘制多分类ROC的完整代码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, auc from sklearn.preprocessing import label_binarize # 生成三分类数据 X, y make_classification( n_samples500, n_features10, n_classes3, n_informative6, n_redundant2, random_state42 ) # 将标签二值化变成3列01矩阵 y_bin label_binarize(y, classes[0, 1, 2]) n_classes y_bin.shape[1] # 按多标签格式划分数据 X_train, X_test, y_train, y_test train_test_split( X, y_bin, test_size0.3, random_state42 ) model LogisticRegression(max_iter2000) model.fit(X_train, y_train) # 得到每个类别对应的预测概率 y_score model.predict_proba(X_test) # 分别计算每个类别的FPR、TPR和AUC fpr_dict {} tpr_dict {} roc_auc_dict {} for i in range(n_classes): fpr_dict[i], tpr_dict[i], _ roc_curve(y_test[:, i], y_score[:, i]) roc_auc_dict[i] auc(fpr_dict[i], tpr_dict[i]) # 绘制所有类别的ROC曲线 plt.figure(figsize(8, 6)) colors [blue, red, green] for i, color in enumerate(colors): plt.plot(fpr_dict[i], tpr_dict[i], colorcolor, lw2, labelfClass {i} (AUC {roc_auc_dict[i]:.3f})) plt.plot([0, 1], [0, 1], k--, lw1) plt.xlim([0.0, 1.0]) plt.ylim([0.0, 1.05]) plt.xlabel(False Positive Rate) plt.ylabel(True Positive Rate) plt.title(Multi-class ROC (One-vs-Rest)) plt.legend(loclower right) plt.grid(alpha0.3) plt.show() print(各类别AUC:, roc_auc_dict)label_binarize把原始标签转成三列01矩阵每一列对应一个类别。逻辑回归的predict_proba在多分类下返回的二维数组每一列是当前样本属于该类别的概率所以可以直接按列和y_test的对应列计算ROC。这里没有合并test集的概率roc_curve第三行返回的_是阈值多分类绘制时一般用不到。5.3 微平均与宏平均AUC的计算微平均的思路是先把所有类别的混淆矩阵分量累加再统一计算fpr和tpr。代码实现上可以用from sklearn.metrics import roc_auc_score配合averagemicro但画曲线需要手动构造from sklearn.metrics import roc_curve, auc import numpy as np # 将真实标签和预测概率都展平 flat_y_true y_test.ravel() flat_y_score y_score.ravel() fpr_micro, tpr_micro, _ roc_curve(flat_y_true, flat_y_score) auc_micro auc(fpr_micro, tpr_micro) # 宏平均AUC直接对各分类AUC取均值 auc_macro np.mean(list(roc_auc_dict.values())) print(fMicro AUC: {auc_micro:.4f}) print(fMacro AUC: {auc_macro:.4f})微平均把所有样本的预测结果汇总成一个大二分类问题受样本量大的类别主导。宏平均值对类别求平均每个类别权重相同。类别不平衡严重时两者差异能到0.1以上。如果业务目标是每个类别都尽量少犯错误用宏平均更合适如果整体准确率优先微平均更贴近实际效果。5.4 多分类中正类概率的取值方向多分类场景最容易踩的坑是搞错正类方向。二分类的roc_curve会自动选择正类多分类按列计算时如果某个类别在数据里的标签是2而label_binarize后的列顺序不匹配曲线会完全反掉。一个稳妥的验证方法打印model.classes_确认类别顺序再打印y_score每一列的均值均值最高的列对应的类别应该和classes_顺序一致。6. 生产线上的ROC概率校准、类别权重与可视化规范到了实际项目阶段AUC不止是模型评估的终点也是模型上线前的体检关口。最后落到几个高频工程细节。分别是类别不平衡时的AUC修正、概率校准对AUC的影响、以及汇报用ROC的标准化模板。6.1 类别不平衡时不要直接改标签做AUC修正正负样本比例差距过大时有人习惯对少数类过采样或多数类欠采样然后再算AUC。这种做法会让AUC反映的是采样后分布的排序能力和真实业务分布有偏差。正确的做法是保留原始测试集做评估训练时通过class_weightbalanced让模型自适应调整权重。逻辑回归和SVM都支持这个参数树模型可以给样本权重。这样做出的AUC更接近上线后的真实排序能力。6.2 概率校准不能提升AUC但能改善阈值设定如果模型输出的概率本身有偏比如预测值集中在0.3到0.6之间AUC不一定低因为排序仍然有效。但业务上需要设定阈值时偏差的概率分布会让阈值难选。用CalibratedClassifierCV做概率校准不会明显改变AUC却能让概率有统计意义的可靠性。生产流程里可以在交叉验证内部嵌套校准避免数据泄漏。6.3 ROC曲线汇报的格式规范面向团队或客户汇报时ROC曲线建议统一标准图标题包含数据来源和时间范围图例中每条曲线带AUC值和95%置信区间绘制对角线作为随机基准曲线平滑要在可视化阶段做不能用平滑后的曲线计算AUC置信区间可以用自助法计算代码不复杂from sklearn.utils import resample boot_aucs [] for _ in range(2000): idx resample(np.arange(len(y_test)), replaceTrue) if len(np.unique(y_test[idx])) 2: continue boot_aucs.append(roc_auc_score(y_test[idx], y_score[idx])) lower np.percentile(boot_aucs, 2.5) upper np.percentile(boot_aucs, 97.5) print(fAUC 95% CI: [{lower:.4f}, {upper:.4f}])这段代码对测试集做2000次有放回抽样每次计算AUC最后取2.5%和97.5%分位数作为置信区间。样本量小于50时自助法会低估方差此时考虑改用留一法或直接报告原始AUC并注明样本量限制。6.4 一组可复用的ROC绘制封装函数日常开发中把画ROC的流程封装成函数能省下不少重复代码。一个通用的版本是这样的def plot_roc_curve(y_true, y_score, titleROC Curve, figsize(8, 6)): 通用二分类ROC曲线绘制函数 fpr, tpr, _ roc_curve(y_true, y_score) roc_auc auc(fpr, tpr) plt.figure(figsizefigsize) plt.plot(fpr, tpr, colordarkorange, lw2, labelfROC (AUC {roc_auc:.4f})) plt.plot([0, 1], [0, 1], k--, lw1) plt.xlabel(False Positive Rate) plt.ylabel(True Positive Rate) plt.title(title) plt.legend(loclower right) plt.grid(alpha0.3) plt.show() return roc_aucy_true必须是二分类01数组y_score是正类概率。返回的AUC可以继续用于模型对比或日志记录。多分类版本把5.2节的循环逻辑包进去即可唯一要注意的是函数内不能每次都plt.figure否则循环画多条曲线时子图会被重置。需要把所有类别画在一张图上时把plt.figure移到循环外层。本文还有配套的精品资源点击获取 SEO 优化官网定制响应式建站教育培训建站