GlenTt

导航

AUC 的两种等价定义:从排序概率到 ROC 曲线的统一理解

AUC 的两种等价定义:从排序概率到 ROC 曲线的统一理解

在推荐系统与广告排序中,AUC 是最常用、也最容易被误解的离线评估指标之一。很多人同时接触过两种说法:
一种是“ROC 曲线下面积”,另一种是“正样本排在负样本前面的概率”。这并不是两种不同的指标,而是同一个指标的两种完全等价的定义

一、AUC 的本质:一个排序概率

1. 问题设定

假设我们面对的是一个二分类 / 排序问题:

  • 每个样本 \(x_i\) 有真实标签 \(y_i \in {0,1}\)
  • 模型给出一个连续预测分数 \(s_i \in \mathbb{R}\)
  • 分数越大,模型认为样本“越可能是正样本”

定义:

  • 正样本集合

    \[P = { i \mid y_i = 1 } \]

  • 负样本集合

    \[N = { j \mid y_j = 0 } \]

2. AUC 的概率定义(最本质定义)

AUC 的概率定义是:

从正样本集合中随机抽取一个样本,从负样本集合中随机抽取一个样本,
正样本的预测分数大于负样本预测分数的概率。

其数学形式为:

\[\mathrm{AUC} = \frac{1}{|P|\cdot|N|} \sum_{p \in P}\sum_{n \in N} \left[ \mathbb{I}(s_p > s_n) + \frac{1}{2}\mathbb{I}(s_p = s_n) \right] \]

其中:

  • \(\mathbb{I}(\cdot)\) 为指示函数
  • \(s_p = s_n\) 时记为 \(0.5\),表示随机打平

3. 这一点意味着什么?

  • AUC 不依赖任何阈值
  • AUC 不是一个分类指标,而是一个排序指标
  • AUC 衡量的是:
    模型是否倾向于把正样本整体排在负样本前面

这也是为什么在推荐系统中,即便最终没有明确的“正负分类决策”,AUC 依然是最核心的离线评估指标之一。

二、ROC 曲线定义:几何视角下的同一个量

1. ROC 曲线如何构造

给定一组预测分数 \(s_i\),我们引入一个阈值 \(\tau\)

  • \(s_i \ge \tau\),预测为正类
  • \(s_i < \tau\),预测为负类

在每一个阈值 \(\tau\) 下,可以计算:

  • 真阳性率(TPR)

    \[\mathrm{TPR}(\tau) = \frac{\mathrm{TP}}{\mathrm{P}} \]

  • 假阳性率(FPR)

    \[\mathrm{FPR}(\tau) = \frac{\mathrm{FP}}{\mathrm{N}} \]

当阈值 \(\tau\)\(+\infty\) 连续下降到 \(-\infty\) 时,
点对 \((\mathrm{FPR}(\tau), \mathrm{TPR}(\tau))\) 在平面上形成一条曲线,即 ROC 曲线

2. AUC 的 ROC 定义

AUC 定义为 ROC 曲线下方的面积:

\[\mathrm{AUC} = \int_0^1 \mathrm{TPR}(\mathrm{FPR}) , d(\mathrm{FPR}) \]

这是一个几何意义上的定义

三、两种定义为什么是完全等价的?

一个统计学习中的经典结论是:

\[\boxed{ \mathrm{AUC} = P(s^+ > s^-) } \]

其中 \(s^+\) 表示正样本分数,\(s^-\) 表示负样本分数。

直观解释如下:

  • ROC 曲线本质是在 按照 score 从高到低扫描排序结果
  • 每遇到一个正样本,TPR 增加
  • 每遇到一个负样本,FPR 增加
  • 某个正样本是否“早于”负样本被扫描到,正对应于

    \[s^+ > s^- \]

因此:

ROC 曲线下面积,等价于所有正负样本对中,排序正确的比例。

ROC 只是将“排序关系”用几何方式进行了表达。

四、一个完整、可手算的例子

1. 样本与预测分数

样本 label score
A 1 0.90
B 1 0.60
C 0 0.70
D 0 0.40
E 0 0.20
  • 正样本:A, B
  • 负样本:C, D, E
  • 正负样本对总数:\(2 \times 3 = 6\)

2. 按概率定义逐对比较

正样本 负样本 是否排序正确
A(0.90) C(0.70)
A(0.90) D(0.40)
A(0.90) E(0.20)
B(0.60) C(0.70)
B(0.60) D(0.40)
B(0.60) E(0.20)
  • 排序正确对数:5
  • 总对数:6

\[\mathrm{AUC} = \frac{5}{6} \approx 0.833 \]

五、对应的计算代码

下面给出两种 AUC 计算实现。

1. 概率定义(两两比较,定义直译)

def auc_pairwise(labels, scores):
    """
    基于 AUC 的概率定义(Pairwise Comparison)进行计算。

    输入:
    - labels: List[int] 或 1D array
        样本真实标签,取值为 {0, 1}
        1 表示正样本,0 表示负样本
    - scores: List[float] 或 1D array
        模型对每个样本给出的预测分数,分数越大表示越倾向正类

    输出:
    - auc: float
        AUC 值,取值范围 [0, 1]

    核心思想:
    随机取一个正样本 p 和一个负样本 n,
    统计 P(score_p > score_n) 的比例
    """

    # 提取正样本 (label=1) 的预测分数
    pos_scores = [s for l, s in zip(labels, scores) if l == 1]

    # 提取负样本 (label=0) 的预测分数
    neg_scores = [s for l, s in zip(labels, scores) if l == 0]

    # 排序正确的正负样本对数量(允许 0.5 的打平贡献)
    correct = 0.0

    # 正负样本对的总数量 |P| * |N|
    total = len(pos_scores) * len(neg_scores)

    # 对所有正负样本对进行两两比较
    for sp in pos_scores:        # sp: positive sample score
        for sn in neg_scores:    # sn: negative sample score
            if sp > sn:
                # 正样本分数严格大于负样本分数,排序正确
                correct += 1.0
            elif sp == sn:
                # 分数相等,按约定计为 0.5(随机打平)
                correct += 0.5
            # sp < sn 时不加分,表示排序错误

    # AUC = 排序正确的比例
    return correct / total

复杂度分析:

  • 时间复杂度:

    \[O(|P| \cdot |N|) \]

    其中 \(|P|\) 为正样本数,\(|N|\) 为负样本数
  • 空间复杂度:

    \[O(|P| + |N|) \]

适用场景:

  • 严格对应 AUC 的概率定义
  • 适合教学、理论验证、小规模数据
  • 不适用于工程和大规模离线计算

2. 排序 / Rank-based 实现

import numpy as np

def auc_rank(labels, scores):
    """
    基于排序(Rank / Mann–Whitney U)的 AUC 计算方法。

    输入:
    - labels: List[int] 或 1D numpy array
        样本真实标签,取值为 {0, 1}
    - scores: List[float] 或 1D numpy array
        模型预测分数,分数越大表示越可能为正样本

    输出:
    - auc: float
        AUC 值,取值范围 [0, 1]

    核心思想:
    1. 按预测分数从小到大排序
    2. 扫描排序后的样本序列
    3. 每遇到一个正样本,统计其前面已有多少负样本
       这些负样本都被该正样本“正确地排在后面”
    """

    # 转为 numpy array,便于排序和向量化操作
    labels = np.asarray(labels)
    scores = np.asarray(scores)

    # 获取按照 score 从小到大排序后的索引
    order = np.argsort(scores)

    # 按排序后的顺序重排标签
    labels_sorted = labels[order]

    # 正样本数量 |P|
    n_pos = np.sum(labels_sorted == 1)

    # 负样本数量 |N|
    n_neg = np.sum(labels_sorted == 0)

    # 已扫描到的负样本数量(前缀负样本计数)
    neg_count = 0

    # 排序正确的正负样本对数量
    correct = 0.0

    # 从低分到高分扫描
    for l in labels_sorted:
        if l == 1:
            # 当前是正样本:
            # 它前面的所有负样本都满足 score_neg < score_pos
            correct += neg_count
        else:
            # 当前是负样本,增加负样本计数
            neg_count += 1

    # AUC = 排序正确的正负样本对 / 总正负样本对
    return correct / (n_pos * n_neg)

复杂度分析:

  • 时间复杂度:

    \[O(n \log n) \]

    主要来自排序操作,其中 \(n = |P| + |N|\)
  • 空间复杂度:

    \[O(n) \]

工程说明:

  • 与 Mann–Whitney U 统计量完全等价
  • 是工业界离线 AUC 计算(Spark / MapReduce / Flink)的理论基础
  • 可自然扩展为分桶、分 user、分实验组的 AUC 统计

分桶统计(工程实现)


def calc_auc(score_list, label_list):
    """
    使用分桶法计算AUC(ROC曲线下面积)。
    
    将预测分数离散化到固定数量的桶中,统计每个桶的正负样本数,
    然后按分数从高到低遍历,使用梯形法则计算ROC曲线下面积。
    
    Args:
        score_list: 模型预测分数列表,取值范围应在[0, 1]之间
        label_list: 真实标签列表,正样本>0,负样本<=0
        
    Returns:
        tuple: (正样本数, 负样本数, AUC值)
            - 当正样本或负样本数为0时,返回(0, 0, 0.5)
            - AUC值范围为[0, 1],0.5表示随机猜测水平
    """
    scoredict = dict()
    numbin = 10000
    maxval = 1.0
    minval = 0.0
    step = (maxval-minval)/numbin
    inv_step = 1.0 / step
    total_pos = 0
    total_neg = 0
    for (score, label) in zip(score_list, label_list):
        c = 0
        v = 0
        if label > 0:
            c = 1
            total_pos += 1
        else:
            v = 1
            total_neg += 1
        score_bin = int((float(score) - minval) * inv_step)
        if scoredict.has_key(score_bin):
            scoredict[score_bin][0] += v
            scoredict[score_bin][1] += c
        else:
            scoredict[score_bin] = [v, c]
    if total_neg == 0:
        return 0, 0, 0.5
    if total_pos == 0:
        return 0, 0, 0.5
    sorted_list = sorted(scoredict.items(), key=lambda x:x[0], reverse=True)
    sum_pos = 0
    sum_neg = 0
    pretp = 0.0
    prefp = 0.0
    roc_auc = 0.0
    roc_curve = dict()    
    fpidx = 0
    for (binidx, value) in sorted_list:
        sum_neg += (float(value[0])/total_neg)
        sum_pos += (float(value[1])/total_pos)
        tp = float(sum_pos)
        fp = float(sum_neg)
        delta =(fp-prefp)*(pretp+tp)*0.5
        roc_auc += delta
        roc_curve[fpidx] = (fp, tp)
        fpidx += 1
        pretp = tp
        prefp = fp
    return total_pos, total_neg, roc_auc

把连续 score 离散成很多桶(bin)
统计每个桶里的正样本数和负样本数
按 score 从高到低扫描桶,构造 ROC 曲线并积分

posted on 2026-01-06 20:27  GlenTt  阅读(55)  评论(0)    收藏  举报