用Python可视化AUC和AP从混淆矩阵到曲线绘制的完整代码指南在数据科学和机器学习的世界里构建一个模型只是第一步。真正考验我们功力的往往是那个看似简单却至关重要的环节——模型评估。你是否曾面对一堆预测概率和真实标签仅仅满足于sklearn.metrics里那个roc_auc_score函数返回的单一数字0.85的AUC值究竟意味着什么它和0.92的模型在实际业务中差距有多大当你的数据集正负样本比例严重失衡时这个数字还可靠吗这些问题恰恰是区分“会用工具”和“真正理解”的关键。AUC和AP这两个在二分类任务中如雷贯耳的评价指标其背后蕴含的是一整套关于模型决策边界、分类置信度以及在不同业务场景下权衡的艺术。仅仅知道它们的计算公式是远远不够的。真正的价值在于能够亲手将模型的“预测结果”这一抽象概念转化为直观可视的曲线并从中解读出模型行为的深层逻辑。这篇文章就是为你准备的。无论你是刚刚踏入数据科学大门的新手还是希望将模型评估工作做得更扎实、更专业的开发者我们都将一起从最基础的混淆矩阵出发一步步用Python和Matplotlib亲手绘制出ROC曲线和PR曲线计算并理解AUC与AP。我们将不止步于调用现成的库函数而是要深入到阈值滑动的动态过程、曲线的平滑处理、以及那些容易被忽略的可视化陷阱。当你能够清晰地看到模型在不同阈值下的表现如何变化并能自信地解释曲线上的每一个拐点时你对模型的理解将进入一个全新的维度。1. 理解评估基石从混淆矩阵到核心指标在开始写代码之前我们必须先夯实理论基础。所有的可视化都建立在清晰的数学定义之上。想象一下你训练了一个模型来预测客户是否会购买某产品正类为“购买”。模型对100个客户给出了预测概率你需要设定一个阈值比如0.5来做出最终判断概率高于0.5的预测为“购买”反之则“不购买”。这时一个混淆矩阵就自然而然地出现了。它是所有评估指标的源头。预测 \ 真实实际为正 (1)实际为负 (0)预测为正 (1)真正例 (TP)假正例 (FP)预测为负 (0)假负例 (FN)真负例 (TN)这个简单的2x2表格包含了评估所需的一切原始信息。基于它我们衍生出两个核心的“率”真阳性率TPR TP / (TP FN)。它衡量的是在所有实际的正样本中有多少被模型成功“召回”了。因此TPR也叫召回率。我们希望它越高越好。假阳性率FPR FP / (FP TN)。它衡量的是在所有实际的负样本中有多少被模型错误地“误伤”了。我们希望它越低越好。另一个重要的指标是精确率Precision TP / (TP FP)。它衡量的是在所有被模型预测为正的样本中有多少是真正的“正品”。在垃圾邮件过滤的场景中我们非常关心精确率——用户可不想看到重要的邮件被误判为垃圾。注意召回率和精确率是一对需要权衡的指标。追求高召回率尽可能不漏掉正样本往往需要降低阈值这会导致更多负样本被误判为正从而降低精确率。反之亦然。那么AUC和AP与这些指标有什么关系呢它们都不是在单一阈值下计算的而是通过让分类阈值从0到1连续变化观察这些指标如何随之变动从而对模型的整体性能做出一个与阈值无关的、综合性的评价。ROC曲线以FPR为横轴TPR为纵轴绘制出的曲线。曲线下的面积就是AUC。它描绘的是模型“区分正负样本的能力”即模型将正样本排在负样本前面的能力。一个完美的模型其ROC曲线会紧贴左上角FPR0 TPR1AUC为1。PR曲线以召回率为横轴精确率为纵轴绘制出的曲线。曲线下的面积就是AP。它更聚焦于正样本的表现尤其在正样本稀少类别不平衡的数据集上AP比AUC更能反映模型的实用价值。为了更直观地对比我们来看一个概念性的表格特性ROC曲线与AUCPR曲线与AP坐标轴(FPR, TPR)(Recall, Precision)关注点模型整体区分正负类的能力模型对正类的识别准确性和覆盖度对类别不平衡的敏感度相对不敏感非常敏感正类越少PR曲线越能暴露问题完美模型曲线紧贴左上角 (0,1)紧贴右上角 (1,1)随机模型曲线从(0,0)到(1,1)的对角线一条水平线高度等于正类样本比例适用场景正负样本成本相近需要综合评估正样本是关键且数量较少如疾病检测、欺诈识别理解了这些我们就可以动手从原始数据开始一步步构建出这些富有洞察力的可视化图表了。2. 环境准备与数据生成构建一个可复现的实验工欲善其事必先利其器。我们将在一个干净、可复现的Jupyter Notebook环境中进行所有操作。首先确保你安装了必要的库。如果你使用pip可以通过以下命令安装pip install numpy scikit-learn matplotlib ipywidgetsnumpy: 数值计算基础。scikit-learn: 机器学习库我们用它来生成数据、训练模型并获取标准指标进行对比验证。matplotlib: 绘图库的核心。ipywidgets: 用于在Jupyter中创建交互式滑块实现阈值滑动的动态演示。接下来我们生成一个模拟的二分类数据集。为了让实验更有趣我们故意制造一些类别不平衡并引入一定的噪声这样绘制出的曲线才更真实也更有分析价值。import numpy as np from sklearn.datasets import make_classification from sklearn.model_selection import train_test_split # 设置随机种子确保结果可复现 np.random.seed(42) # 生成数据集 # n_samples: 总样本数 # n_features: 特征数 # n_informative: 有效特征数 # n_redundant: 冗余特征数 # weights: [负类比例 正类比例]这里设置正类约占20%制造不平衡 # flip_y: 添加标签噪声的比例让问题更有挑战性 X, y make_classification( n_samples2000, n_features15, n_informative8, n_redundant2, n_clusters_per_class1, weights[0.8, 0.2], # 80%负样本20%正样本 flip_y0.05, # 5%的标签噪声 random_state42 ) # 查看类别分布 print(f数据集形状: {X.shape}) print(f正样本比例: {y.sum() / len(y):.2%}) # 划分训练集和测试集 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42 ) print(f训练集大小: {X_train.shape}, 测试集大小: {X_test.shape})运行这段代码你会得到类似以下的输出数据集形状: (2000, 15) 正样本比例: 20.05% 训练集大小: (1400, 15), 测试集大小: (600, 15)现在我们用一个简单的逻辑回归模型来训练并获取模型对测试集样本的预测概率而非硬标签。这是绘制ROC/PR曲线的关键输入。from sklearn.linear_model import LogisticRegression from sklearn.metrics import roc_auc_score, average_precision_score # 训练模型 model LogisticRegression(max_iter1000, random_state42) model.fit(X_train, y_train) # 预测测试集的概率我们关心正类即索引1的概率 y_scores model.predict_proba(X_test)[:, 1] # 形状 (n_samples,) # 使用sklearn快速计算AUC和AP作为我们后续手动计算的基准 sklearn_auc roc_auc_score(y_test, y_scores) sklearn_ap average_precision_score(y_test, y_scores) print(fSklearn计算 - AUC: {sklearn_auc:.4f}) print(fSklearn计算 - AP: {sklearn_ap:.4f})至此我们有了y_test真实标签0或1和y_scores预测为正类的概率介于0到1之间。接下来的所有可视化魔法都将基于这两个数组展开。3. 手动绘制ROC曲线与计算AUC虽然sklearn.metrics提供了roc_curve和auc函数但亲手实现一遍是理解其精髓的最佳方式。我们将分步进行第一步准备数据我们需要对所有测试样本根据预测概率y_scores从高到低排序。同时真实标签y_test也要跟着一起排序。# 将分数和标签组合并按分数降序排序 score_label_pairs list(zip(y_scores, y_test)) score_label_pairs.sort(keylambda x: x[0], reverseTrue) # 解压出排序后的分数和标签 sorted_scores, sorted_labels zip(*score_label_pairs) sorted_scores np.array(sorted_scores) sorted_labels np.array(sorted_labels) # 获取所有不重复的阈值。通常我们使用排序后的分数作为阈值候选。 # 在实际计算中阈值取遍每个样本的预测分数以及比最小分数更小的值如0和比最大分数更大的值如1。 thresholds np.unique(sorted_scores) thresholds np.append(thresholds, thresholds[-1] 0.01) # 添加一个略大于最大值的阈值 thresholds np.insert(thresholds, 0, -0.01) # 添加一个略小于0的阈值确保覆盖所有情况第二步计算每个阈值下的TPR和FPR我们将阈值从高到低滑动。对于每个阈值预测概率大于等于该阈值的样本被判定为正类。def calculate_tpr_fpr(labels, predictions): 根据真实标签和预测标签0/1计算TPR和FPR tp np.sum((predictions 1) (labels 1)) fp np.sum((predictions 1) (labels 0)) tn np.sum((predictions 0) (labels 0)) fn np.sum((predictions 0) (labels 1)) tpr tp / (tp fn) if (tp fn) 0 else 0.0 fpr fp / (fp tn) if (fp tn) 0 else 0.0 return tpr, fpr tpr_list [] fpr_list [] for thresh in thresholds: # 根据当前阈值生成预测标签 preds_at_thresh (sorted_scores thresh).astype(int) tpr, fpr calculate_tpr_fpr(sorted_labels, preds_at_thresh) tpr_list.append(tpr) fpr_list.append(fpr) # 转换为numpy数组 tpr_array np.array(tpr_list) fpr_array np.array(fpr_list)第三步绘制ROC曲线并计算AUCROC曲线上的点就是(fpr_array, tpr_array)。AUC可以通过梯形积分法计算。import matplotlib.pyplot as plt # 创建图形 fig, ax plt.subplots(1, 2, figsize(14, 5)) # 左图ROC曲线 ax[0].plot(fpr_array, tpr_array, b-, linewidth2, labelROC Curve (Manual)) ax[0].plot([0, 1], [0, 1], k--, linewidth1, labelRandom Classifier) # 随机线 ax[0].fill_between(fpr_array, 0, tpr_array, alpha0.3, colorskyblue) # 填充AUC区域 ax[0].set_xlabel(False Positive Rate (FPR), fontsize12) ax[0].set_ylabel(True Positive Rate (TPR / Recall), fontsize12) ax[0].set_title(Receiver Operating Characteristic (ROC) Curve, fontsize14, fontweightbold) ax[0].legend(loclower right) ax[0].grid(True, linestyle--, alpha0.6) ax[0].set_xlim([-0.02, 1.02]) ax[0].set_ylim([-0.02, 1.02]) # 计算手动AUC梯形法则 # 对FPR排序并相应调整TPR sorted_indices np.argsort(fpr_array) fpr_sorted fpr_array[sorted_indices] tpr_sorted tpr_array[sorted_indices] manual_auc np.trapz(tpr_sorted, fpr_sorted) # 梯形积分 # 右图与sklearn结果对比使用sklearn的roc_curve获得更平滑的曲线用于展示 from sklearn.metrics import roc_curve fpr_sk, tpr_sk, _ roc_curve(y_test, y_scores) auc_sk roc_auc_score(y_test, y_scores) ax[1].plot(fpr_sk, tpr_sk, r-, linewidth2, labelfROC Curve (Sklearn)\nAUC {auc_sk:.4f}) ax[1].plot([0, 1], [0, 1], k--, linewidth1) ax[1].fill_between(fpr_sk, 0, tpr_sk, alpha0.3, colorsalmon) ax[1].set_xlabel(False Positive Rate (FPR), fontsize12) ax[1].set_ylabel(True Positive Rate (TPR), fontsize12) ax[1].set_title(ROC Curve (Sklearn for Comparison), fontsize14, fontweightbold) ax[1].legend(loclower right) ax[1].grid(True, linestyle--, alpha0.6) ax[1].set_xlim([-0.02, 1.02]) ax[1].set_ylim([-0.02, 1.02]) plt.tight_layout() plt.show() print(f手动计算 AUC (梯形积分): {manual_auc:.4f}) print(fSklearn 计算 AUC: {sklearn_auc:.4f}) print(f两者差异: {abs(manual_auc - sklearn_auc):.6f})通过对比你会发现手动计算与sklearn的结果几乎一致微小差异源于阈值选取和积分方法的细微差别。这个练习让你彻底明白了AUC是如何从一个个阈值点累积而来的面积。4. 手动绘制PR曲线与计算APPR曲线的绘制逻辑与ROC类似但坐标轴换成了召回率和精确率。这里有一个关键点当召回率增加时阈值降低预测出更多正样本精确率可能会剧烈波动尤其是在样本量不大或正样本很少的时候。因此PR曲线常呈现“锯齿状”。第一步计算每个阈值下的精确率和召回率def calculate_precision_recall(labels, predictions): 根据真实标签和预测标签计算精确率和召回率 tp np.sum((predictions 1) (labels 1)) fp np.sum((predictions 1) (labels 0)) fn np.sum((predictions 0) (labels 1)) precision tp / (tp fp) if (tp fp) 0 else 1.0 # 当TPFP0时约定Precision为1 recall tp / (tp fn) if (tp fn) 0 else 0.0 return precision, recall precision_list [] recall_list [] # 使用与ROC相同的阈值序列 for thresh in thresholds: preds_at_thresh (sorted_scores thresh).astype(int) precision, recall calculate_precision_recall(sorted_labels, preds_at_thresh) precision_list.append(precision) recall_list.append(recall) precision_array np.array(precision_list) recall_array np.array(recall_list)第二步处理锯齿与计算APAP是PR曲线下的面积。由于PR曲线不是单调的计算其面积需要一点技巧。常见的方法是对召回率进行插值然后计算平均精度。sklearn默认使用一种更稳健的方法在多个召回率水平上取对应的最大精确率然后计算面积。# 方法1简单梯形积分可能因锯齿而不准确 simple_ap np.trapz(precision_array, recall_array) # 方法2模仿sklearn的average_precision_score逻辑更标准 # 将召回率从高到低排序并确保精确率是单调递减的取累积最大值 # 这是为了得到一条“包络”曲线消除锯齿。 sorted_indices_pr np.argsort(recall_array)[::-1] # 召回率从高到低排序 recall_sorted recall_array[sorted_indices_pr] precision_sorted precision_array[sorted_indices_pr] # 计算精确率的累积最大值从高召回率向低召回率看精确率应只增不减 precision_cummax np.maximum.accumulate(precision_sorted) # 计算AP梯形积分 manual_ap np.trapz(precision_cummax, recall_sorted) print(f简单梯形积分 AP: {simple_ap:.4f}) print(f手动计算 AP (平滑后): {manual_ap:.4f}) print(fSklearn 计算 AP: {sklearn_ap:.4f})第三步绘制PR曲线from sklearn.metrics import precision_recall_curve # 使用sklearn获取更密集、平滑的点用于绘图 precision_sk, recall_sk, _ precision_recall_curve(y_test, y_scores) # sklearn返回的recall_sk和precision_sk长度多1以(recall0, precision1)开始 fig, ax plt.subplots(1, 2, figsize(14, 5)) # 左图手动计算的原始及平滑PR曲线 ax[0].plot(recall_array, precision_array, b-, linewidth1, alpha0.7, labelRaw PR Curve) ax[0].step(recall_sorted, precision_cummax, r-, linewidth2, wherepost, labelSmoothed PR Curve) ax[0].fill_between(recall_sorted, 0, precision_cummax, alpha0.3, colorsalmon) ax[0].set_xlabel(Recall, fontsize12) ax[0].set_ylabel(Precision, fontsize12) ax[0].set_title(fPrecision-Recall Curve (Manual)\nAP {manual_ap:.4f}, fontsize14, fontweightbold) ax[0].legend(locupper right if manual_ap 0.7 else lower left) ax[0].grid(True, linestyle--, alpha0.6) ax[0].set_xlim([-0.02, 1.02]) ax[0].set_ylim([0.0, 1.02]) # 右图使用sklearn的结果绘图 ax[1].plot(recall_sk, precision_sk, g-, linewidth2, labelfPR Curve (Sklearn)\nAP {sklearn_ap:.4f}) ax[1].fill_between(recall_sk, 0, precision_sk, alpha0.3, colorlightgreen) # 绘制随机分类器的水平线其精确率等于正类比例 positive_ratio y_test.sum() / len(y_test) ax[1].axhline(ypositive_ratio, colork, linestyle--, linewidth1, labelfRandom (P{positive_ratio:.2f})) ax[1].set_xlabel(Recall, fontsize12) ax[1].set_ylabel(Precision, fontsize12) ax[1].set_title(Precision-Recall Curve (Sklearn), fontsize14, fontweightbold) ax[1].legend(locupper right if sklearn_ap 0.7 else lower left) ax[1].grid(True, linestyle--, alpha0.6) ax[1].set_xlim([-0.02, 1.02]) ax[1].set_ylim([0.0, 1.02]) plt.tight_layout() plt.show()观察左图你可以清晰地看到原始PR曲线的“锯齿”是如何被平滑成一条单调递减的包络曲线的。这条平滑曲线下的面积才是我们通常报告的AP值。右图展示了sklearn直接计算的结果它内部也进行了类似的平滑处理。5. 高级定制与实战技巧让图表会说话掌握了基础绘制方法后我们可以进行更深入的可视化定制让图表传递更多信息。技巧一动态阈值滑动演示使用ipywidgets创建一个交互式图表直观展示阈值变化如何影响混淆矩阵和曲线上的点。import ipywidgets as widgets from IPython.display import display, clear_output # 准备数据 fpr_sk, tpr_sk, thresholds_sk roc_curve(y_test, y_scores) precision_sk, recall_sk, thresholds_pr precision_recall_curve(y_test, y_scores) # 注意thresholds_pr比precision_sk和recall_sk少一个元素 # 创建交互控件 threshold_slider widgets.FloatSlider( value0.5, min0.0, max1.0, step0.01, description阈值:, continuous_updateFalse, layout{width: 500px} ) output widgets.Output() def update_plot(threshold): with output: clear_output(waitTrue) fig, axes plt.subplots(2, 2, figsize(14, 10)) # 1. 根据阈值计算当前预测标签和指标 current_preds (y_scores threshold).astype(int) tp np.sum((current_preds 1) (y_test 1)) fp np.sum((current_preds 1) (y_test 0)) tn np.sum((current_preds 0) (y_test 0)) fn np.sum((current_preds 0) (y_test 1)) tpr tp / (tp fn) if (tp fn) 0 else 0 fpr fp / (fp tn) if (fp tn) 0 else 0 precision tp / (tp fp) if (tp fp) 0 else 1 recall tpr # 召回率等于TPR # 2. 绘制ROC曲线并标记当前点 axes[0, 0].plot(fpr_sk, tpr_sk, b-, linewidth2, labelROC Curve) axes[0, 0].plot([0, 1], [0, 1], k--, linewidth1, alpha0.5) axes[0, 0].scatter([fpr], [tpr], colorred, s100, zorder5, labelfThreshold{threshold:.2f}) axes[0, 0].set_xlabel(FPR) axes[0, 0].set_ylabel(TPR (Recall)) axes[0, 0].set_title(fROC Curve (AUC{sklearn_auc:.3f})) axes[0, 0].legend() axes[0, 0].grid(True, alpha0.3) # 3. 绘制PR曲线并标记当前点 axes[0, 1].plot(recall_sk, precision_sk, g-, linewidth2, labelPR Curve) axes[0, 1].axhline(ypositive_ratio, colork, linestyle--, alpha0.5, labelRandom) axes[0, 1].scatter([recall], [precision], colorred, s100, zorder5, labelfThreshold{threshold:.2f}) axes[0, 1].set_xlabel(Recall) axes[0, 1].set_ylabel(Precision) axes[0, 1].set_title(fPR Curve (AP{sklearn_ap:.3f})) axes[0, 1].legend() axes[0, 1].grid(True, alpha0.3) # 4. 绘制混淆矩阵热图 cm np.array([[tn, fp], [fn, tp]]) im axes[1, 0].imshow(cm, interpolationnearest, cmapplt.cm.Blues) axes[1, 0].set_title(fConfusion Matrix (Threshold{threshold:.2f})) axes[1, 0].set_xticks([0, 1]) axes[1, 0].set_yticks([0, 1]) axes[1, 0].set_xticklabels([Pred 0, Pred 1]) axes[1, 0].set_yticklabels([True 0, True 1]) # 在格子中添加数字 thresh cm.max() / 2. for i in range(2): for j in range(2): axes[1, 0].text(j, i, format(cm[i, j], d), hacenter, vacenter, colorwhite if cm[i, j] thresh else black) axes[1, 0].set_ylabel(True Label) axes[1, 0].set_xlabel(Predicted Label) # 5. 显示当前指标 axes[1, 1].axis(off) metrics_text ( f当前阈值: {threshold:.2f}\n\n f真正例 (TP): {tp}\n f假正例 (FP): {fp}\n f真负例 (TN): {tn}\n f假负例 (FN): {fn}\n\n f真阳性率 (TPR/Recall): {tpr:.3f}\n f假阳性率 (FPR): {fpr:.3f}\n f精确率 (Precision): {precision:.3f}\n ) axes[1, 1].text(0.1, 0.5, metrics_text, fontsize12, verticalalignmentcenter, familymonospace) plt.tight_layout() plt.show() # 绑定事件 widgets.interactive(update_plot, thresholdthreshold_slider) # 显示控件 display(threshold_slider, output) # 初始绘制 update_plot(0.5)拖动滑块你会看到ROC和PR曲线上有一个红点随之移动同时下方的混淆矩阵和各项指标实时更新。这个动态演示能让你深刻理解阈值是如何在“抓住更多正样本”和“减少误判”之间进行权衡的。技巧二多模型对比可视化在实际项目中我们经常需要比较多个模型。将它们的ROC/PR曲线绘制在同一张图上是评估模型优劣的直观方法。from sklearn.ensemble import RandomForestClassifier from sklearn.svm import SVC # 训练另外两个模型 rf_model RandomForestClassifier(n_estimators100, random_state42) svm_model SVC(probabilityTrue, random_state42) # 需要probabilityTrue来获取概率 rf_model.fit(X_train, y_train) svm_model.fit(X_train, y_train) # 获取预测概率 y_scores_lr model.predict_proba(X_test)[:, 1] y_scores_rf rf_model.predict_proba(X_test)[:, 1] y_scores_svm svm_model.predict_proba(X_test)[:, 1] # 计算各模型的曲线数据 models { Logistic Regression: y_scores_lr, Random Forest: y_scores_rf, SVM (RBF): y_scores_svm } fig, axes plt.subplots(1, 2, figsize(15, 6)) # 绘制ROC曲线对比 for name, scores in models.items(): fpr, tpr, _ roc_curve(y_test, scores) auc roc_auc_score(y_test, scores) axes[0].plot(fpr, tpr, linewidth2, labelf{name} (AUC {auc:.3f})) axes[0].plot([0, 1], [0, 1], k--, alpha0.5) axes[0].set_xlabel(False Positive Rate) axes[0].set_ylabel(True Positive Rate) axes[0].set_title(ROC Curves - Model Comparison) axes[0].legend(loclower right) axes[0].grid(True, alpha0.3) # 绘制PR曲线对比 for name, scores in models.items(): precision, recall, _ precision_recall_curve(y_test, scores) ap average_precision_score(y_test, scores) axes[1].plot(recall, precision, linewidth2, labelf{name} (AP {ap:.3f})) axes[1].axhline(ypositive_ratio, colork, linestyle--, alpha0.5, labelfRandom (AP{positive_ratio:.3f})) axes[1].set_xlabel(Recall) axes[1].set_ylabel(Precision) axes[1].set_title(PR Curves - Model Comparison) axes[1].legend(locupper right if ap 0.7 else lower left) axes[1].grid(True, alpha0.3) plt.tight_layout() plt.show()通过这张对比图你可以一目了然地看出哪个模型在整体区分能力AUC上更优哪个模型在正样本识别AP上表现更好。例如随机森林可能因为集成学习的特性在AUC上略胜一筹而SVM在特定数据集上的PR曲线可能更凸出。技巧三处理“曲线锯齿”与美化输出当样本量较少时PR曲线会出现难看的锯齿。除了之前提到的平滑方法在绘图时也可以进行插值让曲线更美观。同时我们可以使用更专业的图表样式。from scipy import interpolate import matplotlib # 设置全局字体和样式 matplotlib.rcParams[font.size] 11 matplotlib.rcParams[axes.titlesize] 13 matplotlib.rcParams[axes.titleweight] bold # 假设我们有一个样本量较小的子集用于演示锯齿 np.random.seed(123) sample_idx np.random.choice(len(y_test), size80, replaceFalse) y_test_small y_test[sample_idx] y_scores_small y_scores[sample_idx] precision_raw, recall_raw, _ precision_recall_curve(y_test_small, y_scores_small) # 对召回率进行插值获得平滑曲线 recall_interp np.linspace(0, 1, 200) # 使用一维插值函数对精确率进行插值。由于原始PR点不是函数需要指定插值方式。 # 这里使用‘previous’方法相当于取每个召回率区间内的最大精确率与AP计算逻辑一致。 precision_interp np.interp(recall_interp, recall_raw[::-1], precision_raw[::-1]) # 注意sklearn返回的recall是递减的所以需要反转。 fig, ax plt.subplots(figsize(8, 6)) ax.plot(recall_raw, precision_raw, o-, linewidth1, markersize4, alpha0.7, labelRaw PR Curve (锯齿明显)) ax.plot(recall_interp, precision_interp, -, linewidth2, colordarkorange, labelInterpolated / Smoothed Curve) ax.fill_between(recall_interp, 0, precision_interp, alpha0.2, colordarkorange) ax.set_xlabel(Recall, fontsize12) ax.set_ylabel(Precision, fontsize12) ax.set_title(PR Curve: Raw vs. Smoothed (小样本示例), fontsize14, pad15) ax.legend(locupper right) ax.grid(True, linestyle:, alpha0.4) # 设置更精细的刻度 ax.set_xticks(np.arange(0, 1.1, 0.1)) ax.set_yticks(np.arange(0, 1.1, 0.1)) ax.set_xlim([-0.02, 1.02]) ax.set_ylim([0.0, 1.02]) plt.tight_layout() plt.show()这张图清晰地展示了原始锯齿曲线和平滑后的曲线。在正式报告或论文中使用平滑后的曲线能让图表更清晰、专业。记住平滑只是为了可视化美观计算AP时使用的平滑逻辑取累积最大值才是标准做法。走到这里你已经不仅仅是学会了如何调用sklearn.metrics.plot_roc_curve这样的便捷函数。你亲手从最原始的预测分数和真实标签开始构建了混淆矩阵遍历了所有可能的决策阈值计算了每一个关键指标并最终绘制出了揭示模型本质的ROC与PR曲线。更重要的是你理解了AUC和AP这两个数字背后是一幅幅随着阈值滑动而动态变化的模型性能图景。下次当你拿到一个AUC为0.9的模型时你脑海中浮现的不再只是一个孤立的数字而是一条从(0,0)蜿蜒至(1,1)的曲线你知道在FPR为0.1时模型能抓住多少正样本你也知道当业务要求召回率达到0.95时精确率会下降到什么程度。这种从抽象指标到具体场景的映射能力正是深入可视化带给你的最大财富。