为何sklearn的permutation_test_score与手动算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)
差异成因
两个得分的计算逻辑存在本质区别,不存在计算错误:
permutation_test_score的得分计算逻辑:该函数不会直接使用传入的已训练模型做预测,而是严格按照传入的交叉验证拆分策略,对每一轮拆分:- 用当前折的训练子集重新拟合传入的估计器
- 在当前折的验证子集上计算ROC AUC
最终返回的score_ofc是所有交叉验证折的AUC平均值,不是模型在整个验证集上的全量预测得分。
- 手动计算的得分逻辑:直接使用
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

