核心要点

  • 先说概率含义:AUC 是随机抽一正一负时,正样本得分更高的概率

  • 代码实现优先用排名法:排序后套 Mann-Whitney 公式,复杂度 O(N log N)

  • 同分必须用平均排名,否则正负样本分数相等时会算偏

  • 可以补一个阈值扫描法做交叉验证,展示你知道 ROC 曲线的来源

标准回答

一、先讲清 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)。

追问

追问 1复杂度是多少?为什么 AUC 对不平衡更稳健?

可以说:排名法复杂度是 O(N log N),主要花在排序上,比枚举所有正负样本对的 O(P·N) 好很多。AUC 看的是正负样本的相对排序,不直接受正负比例影响,所以比准确率更适合不平衡场景。但如果正样本极少、业务特别关心少数类,PR-AUC 往往更敏感

追问 2AUC 高就一定是好模型吗?

不一定。AUC 高只能说明排序能力不错,不代表概率校准好,也不代表某个业务阈值下 precision/recall 就好。比如风控、广告投放这种要定阈值的场景,还要看校准曲线、PR 曲线和实际工作点指标。

🔗 相似问题

同一考点的不同问法,换着练更稳

延伸学习

按主题分类的相关资源,便于系统复习