AUC计算python实现
一、AUC的理解
1、理解角度一
AUC 可以把它理解成:模型把正样本排在负样本前面的能力有多强。
或者
AUC = 随机抽一个正样本和一个负样本,模型把正样本分数排得更高的概率
更严谨地说,要加上“分数相同算对一半”:
[ AUC=P(score_{pos}>score_{neg})+ 0.5P(score_{pos}=score_{neg}) ]
所以若没有同分,AUC 就完全等于“正样本排在负样本前面的概率”。
以广告 CTR 预估为例:
- 正样本:用户点击了广告
- 负样本:用户没有点击广告
- 模型给每个广告一个预测点击分 pred
AUC 不关心模型预测的具体数值是不是 0.1、0.8,它主要问:
- 随机抽一个点击广告和一个未点击广告,模型有多大概率把点击广告的分数排得更高?
这个概率就是 AUC。
例如:
样本 是否点击 模型预估分
广告 A 1 0.90
广告 B 0 0.70
广告 C 1 0.60
广告 D 0 0.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。这就是代码里:
elif pre[i] == pre[j]:
auc += 0.5
的含义。
最后要区分两个概念:
- AUC 衡量排序能力:点击样本是否整体排得更靠前。
- 预估偏差:预估值为 0.1 的样本,真实点击率是否也接近 10%。
因此,一个模型可能 AUC 高但预估偏差高,也可能预估偏差低但排序能力弱。广告精排通常两个都需要关注。
2、理解角度2
“AUC 是随机抽一对正负样本,模型排对的概率”和“AUC 是曲线下面积”是 同一个指标的两种视角。
这里的曲线特指 ROC 曲线。
先假设模型给广告预测点击分数:
样本 真实标签 预测分
A 1 0.90
B 0 0.80
C 1 0.60
D 0 0.30
然后不断调一个“判为正样本”的阈值:
- 预测分 >= 阈值,就预测为正(比如认为会点击)。
每取一个阈值,就会得到两个量:
- TPR / 召回率:真实正样本中,有多少被判成正
[ TPR = \frac{TP}{TP+FN} ] - FPR / 假正率:真实负样本中,有多少被误判成正
[ FPR = \frac{FP}{FP+TN} ]
例如阈值设为 0.85,只有 A 被判为正:
A 是正样本且被选中:TP = 1
B、D 是负样本且没被选中:TN = 2
C 是正样本但没被选中:FN = 1
TPR = 1 / 2 = 0.5
FPR = 0 / 2 = 0
所以 ROC 曲线上有一点:
(FPR, TPR) = (0, 0.5)
把阈值从最高分慢慢降低,越来越多样本被判为正,得到一串点:
阈值从高到低
(0, 0) -> (0, 0.5) -> (0.5, 0.5) -> (0.5, 1) -> (1, 1)
横轴是 FPR,纵轴是 TPR,连起来就是 ROC 曲线:
TPR
1.0 | ┌──────●
| │
0.5 | ●────●
| │
0.0 |─────┴──────────── FPR
0 0.5 1
AUC 就是这条 ROC 曲线下方的面积。
为什么面积能衡量排序能力?
- 好模型会优先把正样本排在前面。
- 因此降低阈值时,先增加的是 TP,TPR 很快升高。
- 同时负样本较少被误选,FPR 仍较低。
- ROC 曲线就会尽可能靠近左上角,面积变大。
反过来:
随机模型:ROC 接近对角线,AUC ≈ 0.5
好模型:ROC 向左上角弯曲,AUC 接近 1
两种解释的对应关系是:
排序视角:
- 随机挑一个点击样本和一个未点击样本,
- 正样本得分更高的概率。
曲线视角:
- 遍历所有可能阈值后,
TPR-FPR 轨迹形成的 ROC 曲线下面积。
它们数学上相等:
[ AUC=P(score_{pos}>score_{neg})+ 0.5P(score_{pos}=score_{neg}) ]
即
1. score_pos > score_neg:模型把正样本排在前面,完全正确,记 1 分
2. score_pos = score_neg:无法区分正负样本,算猜对一半,记 0.5 分
3. score_pos < score_neg:模型排反了,记 0 分
所以,AUC 就是所有正负样本对的“平均得分”。
举例,有 2 个正样本和 2 个负样本:
正样本分数:0.9, 0.6
负样本分数:0.8, 0.6
一共有 2×2=4 个正负样本对:
0.9 vs 0.8:正样本更高,得 1
0.9 vs 0.6:正样本更高,得 1
0.6 vs 0.8:正样本更低,得 0
0.6 vs 0.6:两者相同,得 0.5
平均分是:
[ AUC = \frac{1+1+0+0.5}{4}=0.625 ]
将它写成概率:
正样本分更高:2 对,概率 = 2 / 4 = 0.5
正负样本同分:1 对,概率 = 1 / 4 = 0.25
正样本分更低:1 对,不贡献 AUC
因此:
0.5 :正样本排得更高的概率
0.5 × 0.25 = 0.125 :同分样本只能算“猜对一半”
[ AUC = 0.5 + 0.125 = 0.625 ]
0.5 乘同分概率的原因是:同分时模型无法比较谁更像正样本,等价于随机决定顺序,平均只有一半概率排对。
所以后面写的“两两正负样本比较”代码,本质上是在算 ROC 曲线下面积,只是没有显式地画出 ROC 曲线。
二、AUC代码实现【v1版本】
#!/usr/bin/env python
# coding=utf-8
import numpy as np
from sklearn.metrics import roc_curve
from sklearn.metrics import auc
from sklearn.metrics import roc_auc_score
def auc_calculate(labels,preds,n_bins=100):
postive_len = sum(labels)
negative_len = len(labels) - postive_len
total_case = postive_len * negative_len
pos_histogram = [0 for _ in range(n_bins)]
neg_histogram = [0 for _ in range(n_bins)]
bin_width = 1.0 / n_bins
for i in range(len(labels)):
nth_bin = int(preds[i]/bin_width)
if labels[i]==1:
pos_histogram[nth_bin] += 1
else:
neg_histogram[nth_bin] += 1
accumulated_neg = 0
satisfied_pair = 0
for i in range(n_bins):
satisfied_pair += (pos_histogram[i]*accumulated_neg + pos_histogram[i]*neg_histogram[i]*0.5)
accumulated_neg += neg_histogram[i]
return satisfied_pair / float(total_case)
def AUC(label, pre):
"""
适用于python3.0以上版本
"""
# 计算正样本和负样本的索引,以便索引出之后的概率值
pos = [i for i in range(len(label)) if label[i] == 1]
neg = [i for i in range(len(label)) if label[i] == 0]
auc = 0
for i in pos:
for j in neg:
if pre[i] > pre[j]:
auc += 1
elif pre[i] == pre[j]:
auc += 0.5
return auc / (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, thresholds = roc_curve(label, pred, pos_label=1)
print("-----sklearn:",auc(fpr, tpr)) # 0.5666666666666667
print("-----py1:",auc_calculate(label,pred)) # 0.5666666666666667
print("-----py2:", AUC(label, pred)) # 0.5666666666666667
# 最推荐这种,最简单
print("-----py3:", roc_auc_score(y_true=label, y_score=pred)) # 0.5666666666666667
二、AUC代码实现【v2版本】
上一节的代码在二分类、label 严格为 0/1、且正负样本都存在时,计算结果是正确的。它直接实现了 AUC 的正负样本两两比较定义,同分记 0.5 也正确。
主要问题在性能和健壮性。
- 时间复杂度是 O(N_pos * N_neg)。正负样本各 100 万时,需要约 10^12
次比较,实际不可用。 - 没检查 label、pre 长度是否一致。
- 全正或全负样本会除零;这类数据的 AUC 本身无定义。
- 只将标签 0/1 识别为负/正,其他标签会被静默忽略,建议显式校验。
可以保留“两两比较”的版本作为面试/验证实现,补齐边界:
def auc_pairwise(labels, preds):
if len(labels) != len(preds):
raise ValueError("labels and preds must have the same length")
pos = [i for i, y in enumerate(labels) if y == 1]
neg = [i for i, y in enumerate(labels) if y == 0]
if len(pos) + len(neg) != len(labels):
raise ValueError("labels must be binary: 0 or 1")
if not pos or not neg:
return None # AUC is undefined without both classes
correct_pairs = 0.0
for i in pos:
for j in neg:
if preds[i] > preds[j]:
correct_pairs += 1.0
elif preds[i] == preds[j]:
correct_pairs += 0.5
return correct_pairs / (len(pos) * len(neg))
大样本应改成 排序法,复杂度降为 O(N log N)。下面的实现还正确处理了同分:
def auc(labels, preds):
if len(labels) != len(preds):
raise ValueError("labels and preds must have the same length")
if any(y not in (0, 1) for y in labels):
raise ValueError("labels must be binary: 0 or 1")
n_pos = sum(labels)
n_neg = len(labels) - n_pos
if n_pos == 0 or n_neg == 0:
return None
pairs = sorted(zip(preds, labels), key=lambda x: x[0])
correct_pairs = 0.0
neg_seen = 0
i = 0
while i < len(pairs):
j = i
while j < len(pairs) and pairs[j][0] == pairs[i][0]:
j += 1
group = pairs[i:j]
pos_in_group = sum(y for _, y in group)
neg_in_group = len(group) - pos_in_group
# 当前分组的正样本胜过所有更低分的负样本;
# 与当前同分负样本各计 0.5。
correct_pairs += pos_in_group * neg_seen
correct_pairs += 0.5 * pos_in_group * neg_in_group
neg_seen += neg_in_group
i = j
return correct_pairs / (n_pos * n_neg)
更多推荐



所有评论(0)