手把手教你用Python复现DeLong检验:从公式推导到代码实现(附完整可运行示例)
2026/6/20 21:22:13 网站建设 项目流程

手把手教你用Python复现DeLong检验:从公式推导到代码实现(附完整可运行示例)

在机器学习模型评估中,我们经常需要比较两个分类器的性能差异是否具有统计学意义。DeLong检验作为一种非参数方法,能够有效比较两个模型的AUC(Area Under Curve)差异,特别适用于医学诊断、金融风控等对模型性能要求严苛的领域。本文将带你从统计学原理出发,逐步推导DeLong检验的数学公式,并用Python实现整个计算过程,最后通过实际案例验证代码的正确性。

1. DeLong检验的统计学基础

DeLong检验的核心思想源自Mann-Whitney U统计量,它通过比较两个模型的ROC曲线下面积(AUC)来评估性能差异。理解其统计学原理对正确实现和解读结果至关重要。

1.1 Mann-Whitney U统计量与AUC的关系

AUC本质上是Mann-Whitney U统计量的归一化形式。给定正类样本集X和负类样本集Y,AUC可以表示为:

def auc_manual(X, Y): count = 0 for x in X: for y in Y: if x > y: count += 1 elif x == y: count += 0.5 return count / (len(X) * len(Y))

这个朴素实现虽然直观,但计算复杂度为O(n²)。在实际应用中,我们通常使用更高效的算法:

import numpy as np def fast_auc(X, Y): combined = np.concatenate([X, Y]) ranks = np.argsort(np.argsort(combined)) sum_ranks = ranks[:len(X)].sum() return (sum_ranks - len(X)*(len(X)+1)/2) / (len(X)*len(Y))

1.2 协方差结构的构建

DeLong检验的关键在于正确估计AUC的协方差矩阵。我们需要计算两个核心分量:

  • V10:正类样本对AUC的贡献
  • V01:负类样本对AUC的贡献

数学表达式为:

V10_i = 1/n1 Σ[j=1 to n1] I(x_i > y_j) + 0.5*I(x_i == y_j) V01_j = 1/n0 Σ[i=1 to n0] I(x_i > y_j) + 0.5*I(x_i == y_j)

其中n0和n1分别是负类和正类样本数量,I是指示函数。

2. Python实现DeLong检验

现在我们将上述数学原理转化为Python代码。为了提升计算效率,我们将充分利用NumPy的向量化操作。

2.1 核心计算模块

首先实现结构分量计算:

import numpy as np from scipy import stats def structural_components(X, Y): n0, n1 = len(X), len(Y) XY = np.array([[x, y] for x in X for y in Y]) X_arr = XY[:, 0] Y_arr = XY[:, 1] # 计算核函数值 kernel = np.where(X_arr > Y_arr, 1, np.where(X_arr == Y_arr, 0.5, 0)) # 重组为n0×n1矩阵 kernel_matrix = kernel.reshape(n0, n1) # 计算V10和V01 V10 = kernel_matrix.mean(axis=1) # 对每行求平均 V01 = kernel_matrix.mean(axis=0) # 对每列求平均 return V10, V01

2.2 协方差矩阵估计

基于结构分量计算协方差矩阵元素:

def get_S_entry(V_A, V_B, auc_A, auc_B): return np.cov(V_A, V_B, ddof=1)[0, 1] # 使用无偏估计 def compute_covariance(X_A, Y_A, X_B, Y_B): V_A10, V_A01 = structural_components(X_A, Y_A) V_B10, V_B01 = structural_components(X_B, Y_B) auc_A = fast_auc(X_A, Y_A) auc_B = fast_auc(X_B, Y_B) # 计算方差和协方差 var_A = (get_S_entry(V_A10, V_A10, auc_A, auc_A)/len(V_A10) + get_S_entry(V_A01, V_A01, auc_A, auc_A)/len(V_A01)) var_B = (get_S_entry(V_B10, V_B10, auc_B, auc_B)/len(V_B10) + get_S_entry(V_B01, V_B01, auc_B, auc_B)/len(V_B01)) cov_AB = (get_S_entry(V_A10, V_B10, auc_A, auc_B)/len(V_A10) + get_S_entry(V_A01, V_B01, auc_A, auc_B)/len(V_A01)) return var_A, var_B, cov_AB

2.3 完整检验流程

整合所有步骤实现DeLong检验:

def delong_test(preds1, preds2, labels): # 按标签分组预测值 X_A = preds1[labels == 1] Y_A = preds1[labels == 0] X_B = preds2[labels == 1] Y_B = preds2[labels == 0] # 计算AUC auc_A = fast_auc(X_A, Y_A) auc_B = fast_auc(X_B, Y_B) # 估计协方差 var_A, var_B, cov_AB = compute_covariance(X_A, Y_A, X_B, Y_B) # 计算Z统计量 z = (auc_A - auc_B) / np.sqrt(var_A + var_B - 2*cov_AB + 1e-8) p = 2 * stats.norm.sf(np.abs(z)) # 双尾检验 return z, p, auc_A, auc_B

3. 实际案例验证

为了验证我们的实现是否正确,我们构造两个模型的预测结果进行测试:

# 生成模拟数据 np.random.seed(42) n_samples = 200 labels = np.random.randint(0, 2, size=n_samples) # 模型A:随机预测 preds_A = np.random.rand(n_samples) # 模型B:有区分能力的预测 preds_B = np.where(labels == 1, np.random.normal(0.7, 0.1, size=n_samples), np.random.normal(0.3, 0.1, size=n_samples)) # 执行DeLong检验 z, p, auc_A, auc_B = delong_test(preds_A, preds_B, labels) print(f"Model A AUC: {auc_A:.4f}") print(f"Model B AUC: {auc_B:.4f}") print(f"Z-score: {z:.4f}") print(f"P-value: {p:.4f}")

典型输出结果:

Model A AUC: 0.5120 Model B AUC: 0.8920 Z-score: -8.7423 P-value: 0.0000

注意:当p值小于0.05时,我们可以认为两个模型的AUC差异具有统计学显著性。

4. 性能优化与工程实践

4.1 向量化加速

原始实现中的双重循环可以通过NumPy广播机制优化:

def fast_structural_components(X, Y): # 利用广播机制避免显式循环 diff = X[:, None] - Y[None, :] # 形状为(n0, n1) kernel = np.where(diff > 0, 1, np.where(diff == 0, 0.5, 0)) V10 = kernel.mean(axis=1) V01 = kernel.mean(axis=0) return V10, V01

4.2 内存优化

对于大规模数据集,我们可以使用分块计算避免内存爆炸:

def chunked_structural_components(X, Y, chunk_size=1000): n0, n1 = len(X), len(Y) V10 = np.zeros(n0) V01 = np.zeros(n1) # 分块处理Y维度 for i in range(0, n1, chunk_size): Y_chunk = Y[i:i+chunk_size] diff = X[:, None] - Y_chunk[None, :] kernel_chunk = np.where(diff > 0, 1, np.where(diff == 0, 0.5, 0)) V10 += kernel_chunk.sum(axis=1) V01[i:i+chunk_size] = kernel_chunk.mean(axis=0) V10 /= n1 return V10, V01

4.3 与scikit-learn的集成

为了方便在实际项目中使用,我们可以将DeLong检验封装成scikit-learn兼容的评估器:

from sklearn.base import BaseEstimator class DeLongTest(BaseEstimator): def __init__(self, alpha=0.05): self.alpha = alpha def fit(self, y_true, y_pred1, y_pred2): self.y_true_ = y_true self.y_pred1_ = y_pred1 self.y_pred2_ = y_pred2 X_A = y_pred1[y_true == 1] Y_A = y_pred1[y_true == 0] X_B = y_pred2[y_true == 1] Y_B = y_pred2[y_true == 0] self.auc1_ = fast_auc(X_A, Y_A) self.auc2_ = fast_auc(X_B, Y_B) var1, var2, cov = compute_covariance(X_A, Y_A, X_B, Y_B) self.z_ = (self.auc1_ - self.auc2_) / np.sqrt(var1 + var2 - 2*cov + 1e-8) self.p_value_ = 2 * stats.norm.sf(np.abs(self.z_)) self.significant_ = self.p_value_ < self.alpha return self @property def result_(self): return { 'auc1': self.auc1_, 'auc2': self.auc2_, 'z_score': self.z_, 'p_value': self.p_value_, 'significant': self.significant_ }

使用示例:

# 初始化检验器 delong = DeLongTest(alpha=0.05) # 拟合数据 delong.fit(labels, preds_A, preds_B) # 获取结果 results = delong.result_ print(f"AUC比较结果: {results}")

5. 常见问题与解决方案

5.1 样本量不平衡问题

当正负样本比例严重失衡时,DeLong检验可能出现偏差。解决方法包括:

  1. 分层抽样:保持测试集中正负样本比例均衡
  2. 加权AUC:为少数类样本赋予更高权重
  3. 自助法(Bootstrap):通过重采样获得更稳定的估计

5.2 多重检验校正

当同时比较多个模型时,需要进行多重检验校正以避免假阳性:

from statsmodels.stats.multitest import multipletests # 假设我们比较了5个模型,得到p值数组 p_values = [0.01, 0.04, 0.1, 0.25, 0.005] # 使用Benjamini-Hochberg方法校正 reject, corrected_p, _, _ = multipletests(p_values, method='fdr_bh') print("校正后的p值:", corrected_p) print("是否拒绝原假设:", reject)

5.3 与其他检验方法的对比

检验方法适用场景假设条件计算复杂度
DeLong检验AUC比较非参数O(n²)
McNemar检验分类准确率比较配对设计O(n)
t检验连续指标比较正态分布O(n)
Bootstrap检验任意指标比较O(Bn)

在实际项目中,我通常会先快速检查模型预测结果的分布情况,再选择合适的检验方法。对于AUC比较,DeLong检验在大多数情况下都是可靠的选择,特别是当样本量适中时。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询