核心要点
AUC 等价于「随机取一个正样本和一个负样本,正样本得分更高的概率」(Mann-Whitney U 统计量)
排名公式:AUC = (Σ rank(正样本) − P·(P+1)/2) / (P·N),P、N 为正负样本数
同分(tie)要用平均排名(rankdata average),否则结果偏差
另一实现是阈值扫描算 ROC 曲线再用梯形积分,两法结果应一致,可互相验证
标准回答
AUC 是 ROC 曲线下面积,概率含义是「正样本得分高于负样本的概率」。直接对得分排序,用 Mann-Whitney 公式即可 O(N log N) 算出,无需扫描所有阈值。关键是处理同分:得分相同的样本要赋予平均排名,否则正负样本得分相等的情形会被错误计入。下面给出排名法实现,并附阈值扫描法做交叉验证:
python
import numpy as np
def auc_rank(y_true, y_score):
"""基于排名的 AUC(Mann-Whitney U)。y_true: 0/1 标签;y_score: 预测分。"""
y_true = np.asarray(y_true)
y_score = np.asarray(y_score, dtype=float)
P = int((y_true == 1).sum())
N = int((y_true == 0).sum())
if P == 0 or N == 0:
return float('nan') # 单一类别无法定义 AUC
# 平均排名处理同分:先按分数排序,对 tie 取平均名次
order = np.argsort(y_score, kind='mergesort')
ranks = np.empty(len(y_score), dtype=float)
sorted_scores = y_score[order]
i = 0
while i < len(sorted_scores):
j = i
while j + 1 < len(sorted_scores) and sorted_scores[j + 1] == sorted_scores[i]:
j += 1
avg_rank = (i + j) / 2 + 1 # 名次从 1 开始
ranks[order[i:j + 1]] = avg_rank
i = j + 1
sum_pos_rank = ranks[y_true == 1].sum()
return (sum_pos_rank - P * (P + 1) / 2) / (P * N)
def auc_threshold(y_true, y_score):
"""阈值扫描 + 梯形积分,用于交叉验证。"""
y_true = np.asarray(y_true)
order = np.argsort(-np.asarray(y_score)) # 分数从高到低
yt = y_true[order]
P, Nn = yt.sum(), (1 - yt).sum()
tps = np.cumsum(yt); fps = np.cumsum(1 - yt)
tpr = np.concatenate([[0], tps / P]); fpr = np.concatenate([[0], fps / Nn])
# 梯形积分求 ROC 曲线下面积(不依赖 np.trapz,兼容 NumPy 2.x)
return float(np.sum((fpr[1:] - fpr[:-1]) * (tpr[1:] + tpr[:-1]) / 2))
if __name__ == '__main__':
rng = np.random.default_rng(0)
y = np.array([0, 0, 1, 1, 1, 0, 1, 0])
s = np.array([0.1, 0.4, 0.35, 0.8, 0.8, 0.2, 0.6, 0.4])
print('rank AUC =', round(auc_rank(y, s), 4))
print('thresh AUC =', round(auc_threshold(y, s), 4)) # 两者一致常见误区
⚠️ 常见踩坑
忽略同分处理,对 tie 用普通排序而非平均排名,会让 AUC 偏高或偏低;以及当只有单一类别(全正或全负)时 AUC 无定义,应返回 NaN 而非报错。阈值法里也别忘了在 ROC 曲线起点补 (0,0)。
延伸学习
与本题相关的知识库文章、术语、工具与行业资讯。