标准回答
一、先讲清 AUC 的直觉
AUC 不只是“ROC 曲线下面积”,面试里更好说成:随机拿一个正样本和一个负样本,模型给正样本打分更高的概率。这个说法能直接连到代码实现。
二、实现上优先写排名法
把所有样本按 score 排名,拿正样本的 rank 之和,代入 Mann-Whitney 公式:AUC = (正样本 rank 和 − P(P+1)/2) / (P·N)。排序是瓶颈,所以复杂度是 O(N log N)。
三、最容易丢分的是同分处理
如果正负样本分数相同,不能随便按排序先后给名次,要用平均排名。否则 tie 会让 AUC 偏高或偏低。下面代码保留了排名法,也用阈值扫描法做交叉验证:
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)。
🔗 相似问题
同一考点的不同问法,换着练更稳
延伸学习
按主题分类的相关资源,便于系统复习
