AUC计算python实现

发布时间:2026/8/8 17:30:57
AUC计算python实现 一、AUC的理解1、理解角度一AUC 可以把它理解成模型把正样本排在负样本前面的能力有多强。或者AUC 随机抽一个正样本和一个负样本模型把正样本分数排得更高的概率更严谨地说要加上“分数相同算对一半”[ AUCP(score_{pos}score_{neg}) 0.5P(score_{pos}score_{neg}) ]所以若没有同分AUC 就完全等于“正样本排在负样本前面的概率”。以广告 CTR 预估为例正样本用户点击了广告负样本用户没有点击广告模型给每个广告一个预测点击分 predAUC 不关心模型预测的具体数值是不是 0.1、0.8它主要问随机抽一个点击广告和一个未点击广告模型有多大概率把点击广告的分数排得更高这个概率就是 AUC。例如样本 是否点击 模型预估分 广告 A10.90广告 B00.70广告 C10.60广告 D00.20正样本是 A、C负样本是 B、D一共有 2 × 2 4 对正负样本A(0.90)vs B(0.70)正样本更高正确 A(0.90)vs D(0.20)正样本更高正确 C(0.60)vs B(0.70)正样本更低错误 C(0.60)vs D(0.20)正样本更高正确4 对里排对 3 对[ AUC 3 / 4 0.75 ]含义就是随机抽一对“点击/未点击”样本或者说 正负样本对 排列组合模型有 75% 的概率把点击样本排在前面。AUC 取值可以这样理解1.0所有正样本都排在所有负样本前完美排序。0.5和随机猜测差不多没有排序能力。 0.5排序方向基本反了将分数取反后可能变成大于 0.5。实际广告/推荐模型一般关注 AUC 的微小增量例如 0.001 的提升在大流量下也可能有业务价值。如果正负样本预测分相同例如正样本0.6负样本0.6无法说谁排得更前通常记为“猜对一半”贡献 0.5。这就是代码里elifpre[i]pre[j]:auc0.5的含义。最后要区分两个概念AUC 衡量排序能力点击样本是否整体排得更靠前。预估偏差预估值为 0.1 的样本真实点击率是否也接近 10%。因此一个模型可能 AUC 高但预估偏差高也可能预估偏差低但排序能力弱。广告精排通常两个都需要关注。2、理解角度2“AUC 是随机抽一对正负样本模型排对的概率”和“AUC 是曲线下面积”是 同一个指标的两种视角。这里的曲线特指 ROC 曲线。先假设模型给广告预测点击分数样本 真实标签 预测分 A10.90B00.80C10.60D00.30然后不断调一个“判为正样本”的阈值预测分 阈值就预测为正比如认为会点击。每取一个阈值就会得到两个量TPR / 召回率真实正样本中有多少被判成正[ TPR \frac{TP}{TPFN} ]FPR / 假正率真实负样本中有多少被误判成正[ FPR \frac{FP}{FPTN} ]例如阈值设为 0.85只有 A 被判为正A 是正样本且被选中TP1B、D 是负样本且没被选中TN2C 是正样本但没被选中FN1TPR1/20.5FPR0/20所以 ROC 曲线上有一点(FPR,TPR)(0,0.5)把阈值从最高分慢慢降低越来越多样本被判为正得到一串点阈值从高到低(0,0)-(0,0.5)-(0.5,0.5)-(0.5,1)-(1,1)横轴是 FPR纵轴是 TPR连起来就是 ROC 曲线TPR1.0|┌──────●|│0.5|●────●|│0.0|─────┴──────────── FPR00.51AUC 就是这条 ROC 曲线下方的面积。为什么面积能衡量排序能力好模型会优先把正样本排在前面。因此降低阈值时先增加的是 TPTPR 很快升高。同时负样本较少被误选FPR 仍较低。ROC 曲线就会尽可能靠近左上角面积变大。反过来随机模型ROC 接近对角线AUC ≈0.5好模型ROC 向左上角弯曲AUC 接近1两种解释的对应关系是排序视角随机挑一个点击样本和一个未点击样本正样本得分更高的概率。曲线视角遍历所有可能阈值后TPR-FPR 轨迹形成的 ROC 曲线下面积。它们数学上相等[ AUCP(score_{pos}score_{neg}) 0.5P(score_{pos}score_{neg}) ]即1.score_posscore_neg模型把正样本排在前面完全正确记1分2.score_posscore_neg无法区分正负样本算猜对一半记0.5分3.score_posscore_neg模型排反了记0分所以AUC 就是所有正负样本对的“平均得分”。举例有 2 个正样本和 2 个负样本正样本分数0.9,0.6负样本分数0.8,0.6一共有 2×24 个正负样本对0.9vs0.8正样本更高得10.9vs0.6正样本更高得10.6vs0.8正样本更低得00.6vs0.6两者相同得0.5平均分是[ AUC \frac{1100.5}{4}0.625 ]将它写成概率正样本分更高2对概率2/40.5正负样本同分1对概率1/40.25正样本分更低1对不贡献 AUC因此0.5正样本排得更高的概率0.5×0.250.125同分样本只能算“猜对一半”[ AUC 0.5 0.125 0.625 ]0.5 乘同分概率的原因是同分时模型无法比较谁更像正样本等价于随机决定顺序平均只有一半概率排对。所以后面写的“两两正负样本比较”代码本质上是在算 ROC 曲线下面积只是没有显式地画出 ROC 曲线。二、AUC代码实现【v1版本】#!/usr/bin/env python# codingutf-8importnumpyasnpfromsklearn.metricsimportroc_curvefromsklearn.metricsimportaucfromsklearn.metricsimportroc_auc_scoredefauc_calculate(labels,preds,n_bins100):postive_lensum(labels)negative_lenlen(labels)-postive_len total_casepostive_len*negative_len pos_histogram[0for_inrange(n_bins)]neg_histogram[0for_inrange(n_bins)]bin_width1.0/n_binsforiinrange(len(labels)):nth_binint(preds[i]/bin_width)iflabels[i]1:pos_histogram[nth_bin]1else:neg_histogram[nth_bin]1accumulated_neg0satisfied_pair0foriinrange(n_bins):satisfied_pair(pos_histogram[i]*accumulated_negpos_histogram[i]*neg_histogram[i]*0.5)accumulated_negneg_histogram[i]returnsatisfied_pair/float(total_case)defAUC(label,pre): 适用于3.0以上版本 # 计算正样本和负样本的索引以便索引出之后的概率值pos[iforiinrange(len(label))iflabel[i]1]neg[iforiinrange(len(label))iflabel[i]0]auc0foriinpos:forjinneg:ifpre[i]pre[j]:auc1elifpre[i]pre[j]:auc0.5returnauc/(len(pos)*len(neg))if__name____main__:label[1,0,0,0,1,0,1,0]pred[0.9,0.8,0.3,0.1,0.4,0.9,0.66,0.7]fpr,tpr,thresholdsroc_curve(label,pred,pos_label1)print(-----sklearn:,auc(fpr,tpr))# 0.5666666666666667print(-----py1:,auc_calculate(label,pred))# 0.5666666666666667print(-----py2:,AUC(label,pred))# 0.5666666666666667# 最推荐这种最简单print(-----py3:,roc_auc_score(y_truelabel,y_scorepred))# 0.5666666666666667二、AUC代码实现【v2版本】上一节的代码在二分类、label 严格为 0/1、且正负样本都存在时计算结果是正确的。它直接实现了 AUC 的正负样本两两比较定义同分记 0.5 也正确。主要问题在性能和健壮性。时间复杂度是O(N_pos * N_neg)。正负样本各 100 万时需要约 10^12次比较实际不可用。没检查 label、pre 长度是否一致。全正或全负样本会除零这类数据的 AUC 本身无定义。只将标签 0/1 识别为负/正其他标签会被静默忽略建议显式校验。可以保留“两两比较”的版本作为面试/验证实现补齐边界defauc_pairwise(labels,preds):iflen(labels)!len(preds):raiseValueError(labels and preds must have the same length)pos[ifori,yinenumerate(labels)ify1]neg[ifori,yinenumerate(labels)ify0]iflen(pos)len(neg)!len(labels):raiseValueError(labels must be binary: 0 or 1)ifnotposornotneg:returnNone# AUC is undefined without both classescorrect_pairs0.0foriinpos:forjinneg:ifpreds[i]preds[j]:correct_pairs1.0elifpreds[i]preds[j]:correct_pairs0.5returncorrect_pairs/(len(pos)*len(neg))大样本应改成 排序法复杂度降为O(N log N)。下面的实现还正确处理了同分defauc(labels,preds):iflen(labels)!len(preds):raiseValueError(labels and preds must have the same length)ifany(ynotin(0,1)foryinlabels):raiseValueError(labels must be binary: 0 or 1)n_possum(labels)n_neglen(labels)-n_posifn_pos0orn_neg0:returnNonepairssorted(zip(preds,labels),keylambdax:x[0])correct_pairs0.0neg_seen0i0whileilen(pairs):jiwhilejlen(pairs)andpairs[j][0]pairs[i][0]:j1grouppairs[i:j]pos_in_groupsum(yfor_,yingroup)neg_in_grouplen(group)-pos_in_group# 当前分组的正样本胜过所有更低分的负样本# 与当前同分负样本各计 0.5。correct_pairspos_in_group*neg_seen correct_pairs0.5*pos_in_group*neg_in_group neg_seenneg_in_group ijreturncorrect_pairs/(n_pos*n_neg)