You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何sklearn的permutation_test_score与手动算ROC AUC结果不同

scikit-learn分类任务置换检验ROC AUC得分无法复现问题排查

问题描述

使用scikit-learn开展分类任务置换检验时,无法复现permutation_test_score返回的ROC AUC得分:手动调用predict_proba获取预测概率后,用roc_auc_score计算得到的AUC值与permutation_test_score输出存在明显差异,该差异会直接影响项目结果统计显著性(显著/非显著)的判定。

置换检验得分分布可视化

可视化标注说明:

  • (黄色标注)permutation_test_score输出得分 = 0.5256
  • (绿色标注)基于predict_proba手动调用roc_auc_score计算的得分 = 0.5416
  • (红色标注)代表p=0.05显著性阈值的97.5百分位参考线

复现代码

以下代码改编自scikit-learn官方permutation_test_score文档示例,其中grid.best_estimator_为经随机网格搜索得到的RandomForestClassifier,网格搜索采用的交叉验证策略与置换检验完全一致:

rskf = StratifiedKFold(n_splits=5)

n_permutations = 300
###### 传入最优估计器,在验证集上运行置换检验,验证集标签共置换n次
score_ofc, perm_scores_ofc, pvalue_ofc = permutation_test_score(
    grid.best_estimator_, 
    X_val, 
    y_val, 
    scoring="roc_auc", 
    cv=rskf, 
    n_permutations=n_permutations, 
    n_jobs=6, 
    random_state=42,
    verbose=1
)

###### 手动计算ROC AUC得分
y_pred_val = grid.best_estimator_.predict_proba(X_val)[:,1]
roc_auc_val = roc_auc_score(y_val, y_pred_val)
p_val_man = (np.sum(perm_scores_ofc >= roc_auc_val) + 1.0) / (n_permutations + 1)


##### 绘制置换得分分布
fig, ax = plt.subplots()
plt.figure(figsize = (5,5))

ax.hist(perm_scores_ofc, bins=20, density=True)
###### 对比手动计算AUC与permutation_test_score输出AUC
ax.axvline(roc_auc_val, ls="--", color="g", lw=3)
ax.axvline(score_ofc, ls="--", color="y", lw=3)
###### 绘制p=0.05显著性水平参考线
ax.axvline(np.percentile(perm_scores_ofc, 97.5), ls="-", color="r", lw=3)
ax.set_xlabel("ROC AUC score")
_ = ax.set_ylabel("Probability")

print('''
绿色线 = 手动调用predict_proba计算的原始数据得分
      = {}
p值 = {}

黄色线 = permutation_test_score自动计算的原始数据得分
      = {}
p值 = {}

97.5百分位阈值: {}
'''.format(roc_auc_val, p_val_man, score_ofc, pvalue_ofc, np.percentile(perm_scores_ofc, 97.5)))

plt.show()

已排查方向

此前已排查同类问题,已知部分得分差异来自scorer调用decision_function或predict_proba的逻辑不同,但RandomForestClassifier无decision_function属性,该原因无法解释本次差异。

补充:前置网格搜索完整代码

以下为前期构建Pipeline、执行随机网格搜索的完整代码,供问题排查参考:

pca = PCA()
pipe = Pipeline(
    [
        ('scaler', MaxAbsScaler()),
        ('pca', pca),
        ('classifier', RandomForestClassifier()),
    ]
)

param_grid = [
    {
        'classifier': [RandomForestClassifier(random_state=42, n_jobs=-1)],
        'classifier__max_depth' : [i for i in range(1, 8, 2)],
        'scaler': [RobustScaler()],
        'pca__n_components': [33],
        'classifier__n_estimators' : [250],
        'classifier__criterion' : ['gini'],
        'classifier__max_features' : [0.3],
        'classifier__min_samples_split': [12],
        'classifier__min_samples_leaf': [9]
    }
]
###################################################
### 网格搜索最优超参数 ###
# 评分指标 = ROC AUC

rskf = StratifiedKFold(n_splits=5)

grid = RandomizedSearchCV(pipe, 
                          param_grid,
                          n_iter=60,
                          random_state=42,
                          scoring='roc_auc',
                          cv=rskf,
                          refit=True,
                          return_train_score=True,
                          verbose=1,
                          n_jobs=6
                         ).fit(X_train, y_train)

差异成因

两个得分的计算逻辑存在本质区别,不存在计算错误:

  1. permutation_test_score的得分计算逻辑:该函数不会直接使用传入的已训练模型做预测,而是严格按照传入的交叉验证拆分策略,对每一轮拆分:
    • 用当前折的训练子集重新拟合传入的估计器
    • 在当前折的验证子集上计算ROC AUC
      最终返回的score_ofc是所有交叉验证折的AUC平均值,不是模型在整个验证集上的全量预测得分。
  2. 手动计算的得分逻辑:直接使用grid.best_estimator_(已经在完整训练集上refit完成的最终模型)对整个X_val验证集做预测,一次性计算全量样本的AUC,没有走交叉验证拆分、逐折拟合的流程。

补充:置换过程中生成的perm_scores_ofc也全部是交叉验证折平均得分,因此手动计算的全量AUC不能直接和置换得分分布做显著性比较,必须和permutation_test_score保持一致的交叉验证平均得分计算逻辑,得到的p值才是准确的。

复现官方得分的修正方法

如果要手动复现permutation_test_score返回的原始数据得分,需要和函数内部逻辑保持一致,手动实现交叉验证逐折拟合、逐折打分、取平均的流程:

from sklearn.base import clone
manual_cv_scores = []
# 逐折拆分
for train_idx, val_idx in rskf.split(X_val, y_val):
    # 克隆未训练的最优模型,避免数据泄露
    fold_model = clone(grid.best_estimator_)
    # 用当前折训练子集拟合模型
    fold_model.fit(X_val[train_idx], y_val[train_idx])
    # 用当前折验证子集预测正类概率
    fold_pred = fold_model.predict_proba(X_val[val_idx])[:,1]
    # 计算当前折AUC
    fold_auc = roc_auc_score(y_val[val_idx], fold_pred)
    manual_cv_scores.append(fold_auc)
# 交叉验证平均AUC,和permutation_test_score返回的score_ofc完全一致
manual_cv_mean_auc = np.mean(manual_cv_scores)

用这个manual_cv_mean_auc再和置换得分分布比较计算p值,结果就会和函数输出的pvalue_ofc完全匹配。


内容的提问来源于stack exchange,提问作者Joe K

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 17:54:30