之前做医学影像模型评估时我经常被同一个问题卡住跑完分组 AUC发现某一亚组明显偏低但很难判断这到底是模型在特征表示层面真正存在系统性偏差还是仅仅因为该组样本量不足、验证集切法不同导致的偶然波动。有时候换一组验证数据结论甚至会发生反转。这种“看得见差异、说不清来源”的状态在医学影像公平性研究中非常普遍。FRAME 正是围绕这个问题提出的一种分析思路核心目标是在医学影像公平性评估中把“采样变异”sampling variation和“表征性原因”representational cause分开。本文会先讲清楚两个概念的区别再给出一个可运行的 Python 分析流程演示如何利用分组建模、Bootstrap 重采样、置换检验和表征漂移分析来逐步定位差异来源。1. 为什么公平性评估要先分清差异来源1.1 医学影像公平性评估的现实处境医学影像模型进入临床辅助诊断流程前通常需要评估不同亚组上的表现差异。这里的亚组可以按年龄、性别、体质指数、检查设备型号、机构、地区等进行划分。理想情况下我们希望模型在所有亚组上都保持稳定的灵敏度、特异度和校准能力。但在实际操作中评估结果经常呈现“不稳定”的特征。同一个模型在第一批测试数据上显示组 A 的 AUC 明显低于组 B换一批测试数据后差异缩小甚至消失。如果直接据此判断模型“对某组不公”可能会白白修改模型结构浪费资源如果反过来把差异一概当成噪声又可能放过真正有问题的表征偏移给临床使用埋下隐患。1.2 观测到的差异不等于系统性偏差从统计角度看任何一个分组性能指标都是在有限样本上估计出来的天然带有随机误差。当某个亚组的样本量很小或者组内的正负样本比例不平衡时这种随机误差会非常明显。例如某亚组只有 80 例样本其中正例 15 例。此时 AUC 的置信区间往往非常宽一次划分中可能看到 0.75 的 AUC另一次划分却只有 0.62。这两个数值的差距很可能不是模型歧视导致的而是采样波动造成的。FRAME 框架的核心贡献正是要求我们在做“公平性归因”前先做一轮统计上的隔离把可以归因于随机采样的差异剥离掉把在表征层面稳定存在、可复现的差异识别出来对真正属于表征性原因的差异再做特征级归因和干预设计。1.3 区分两类原因的意义在医学影像场景下“错误归因”的代价是具体的。如果误把采样变异当成系统性偏差可能会推动团队修改模型、重新训练、调整损失函数最终却因为问题根因并非模型表征而收效甚微。如果误把表征性原因当成采样噪声则可能让一个有缺陷的模型进入高风险场景影响特定人群的诊断准确性。因此医学影像公平性评估的第一原则不是“看到差异马上修复”而是“先确认差异是否真实存在”。2. 核心概念采样变异与表征性原因2.1 采样变异是什么采样变异是指由于观测样本是从总体中随机抽取的一部分而产生的统计波动。对于同一个模型、同一个测试总体每次抽样得到的测试集不同性能指标就会不同。常见的采样变异来源包括某个亚组样本量过少导致均值估计不稳定测试集中正负样本比例随抽样波动数据划分方式引入的偶然相关性同一患者多次随访图像造成的重复样本依赖跨中心数据合并时不同中心的分布差异被误当作亚组效应。在实验设计中我们通常用置信区间、Bootstrap、交叉验证标准差等方式量化采样变异。采样变异本身不是“错误”但它会干扰我们对系统性问题的判断。2.2 表征性原因是什么表征性原因指的是在模型的特征表示空间和决策边界中稳定存在的、可复现的差异来源。它来自模型学习到的输入到特征映射关系而不是本次抽样带来的偶然波动。以医学影像为例一个典型的表征性原因可能是模型对来自设备 A 的图像提取出的纹理特征与来自设备 B 的图像存在系统性差异导致决策边界在两个设备来源的样本上表现不同。这个差异是由输入分布、特征映射、标注分布共同决定的不会因为随机换一批测试样本就消失。表征性原因还可以细分为几种输入分布原因不同亚组在图像采集设备、对比剂剂量、扫描协议上存在差异标签分布原因不同亚组的标注标准不一致特征学习原因模型在深层特征中丢失了某些亚组的关键信息决策边界原因决策边界在特征空间中的位置对某些区域更敏感。2.3 一个容易混淆的地方需要说明的是表征性原因不等于直接因果关系。在医学影像公平性研究中我们很难直接断言某个特征“导致”了模型偏差。FRAME 的立场更接近先通过统计手段确认差异是否稳定再在表征层面寻找可解释的分布差异为后续的因果分析和干预提供方向。2.4 两类原因对比维度采样变异表征性原因来源样本抽取的随机性模型特征映射与决策边界稳定性随测试集变化而变化在不同测试集上保持稳定样本量影响样本越小影响越大不直接由样本量决定发现方法Bootstrap、重采样、置信区间特征归因、扰动分析、跨数据集验证临床意义评估不确定性不代表模型偏见可能是真实公平性风险3. FRAME 框架内部的分析思路3.1 差异的分解视角FRAME 的出发点是把观测到的亚组性能差异分解为两个主要部分观测差异 ≈ 表征性原因贡献 采样变异贡献 噪声其中噪声可以理解为超出当前模型解释能力的随机成分。实际操作中我们并不需要精确地计算出每一部分的具体数值而是需要回答一个更关键的问题当前观测到的差异是否已经超出随机采样所能解释的范围如果回答“没有超出”那么更稳妥的结论是当前证据不足以认定模型在该亚组上存在系统性偏差。如果回答“已经超出”我们才进入下一步分析表征层面的稳定差异来自哪些特征。3.2 分析流程的四个步骤FRAME 的分析流程可以分为四步。第一步分组建模与指标评估。在训练集上训练模型在独立测试集上分别计算各亚组的 AUC、灵敏度、特异度等指标。第二步构建采样变异参照。通过 Bootstrap 对测试样本进行有放回重采样计算每一个亚组指标的分布以及组间差异的置信区间。第三步置换检验。随机打乱样本的亚组标签模拟“亚组身份不影响性能”的零分布计算观测差异在该零分布中的位置得到置换 p 值。第四步表征层面归因。当差异具有统计学意义时对模型输入特征或中间层特征做分布对比找出贡献最大的特征维度为后续干预提供线索。3.3 与常见公平性评估的区别传统公平性评估通常直接输出“组 A AUC 0.82组 B AUC 0.75结论是模型对组 B 不友好”。这种做法的风险在于它只描述了一个快照没有把随机波动纳入结论。FRAME 风格的评估会额外输出组间差异的 Bootstrap 置信区间、置换检验 p 值、差异稳定性指数。这样即使是同一个数值结果也可能得到完全不同的工程决策。4. 环境准备与实验设计4.1 运行环境本文示例以 Python 3.9 为例需要以下依赖库numpyscikit-learnscipymatplotlib用于可视化非必需版本需要根据你的项目实际情况调整本文示例以常见环境为例重点演示分析思路。安装命令如下pip install numpy scikit-learn scipy matplotlib4.2 实验数据结构FRAME 分析流程要求把数据组织成三个核心部分字段含义X样本特征矩阵每一行是一个样本的医学影像特征y标签0 表示阴性1 表示阳性group亚组标签例如 0 表示亚组 A1 表示亚组 B在真实项目中X 可以是影像组学特征、深度网络中间层特征或临床结构化特征。为了本文示例的可复现性我会先模拟一组特征数据再演示完整的 FRAME 分析流程。4.3 项目结构建议把代码拆分到独立模块中方便复用frame_demo/ |-- data_sim.py |-- model_eval.py |-- frame_analysis.py |-- run_demo.py5. 完整的 FRAME 分析流程代码5.1 模拟医学影像特征数据这里我们生成一个模拟数据集。每个样本有 6 个特征维度代表从医学影像中提取的纹理、强度、形状等特征。两个亚组之间存在一个真实的表征偏移同时每个亚组内部带有随机噪声。# data_sim.py import numpy as np def make_synthetic_imaging_features(n_a400, n_b200, seed42): 生成两个亚组的模拟医学影像特征。 亚组 B 在第 1、2、4 个特征上存在均值偏移 这种偏移模拟表征性原因带来的输入分布差异 每个亚组内的协方差噪声则模拟采样变异。 参数 ---- n_a : int 亚组 A 的样本数 n_b : int 亚组 B 的样本数 seed : int 随机种子保证结果可复现 返回 ---- X : ndarray, shape(n_an_b, 6) 特征矩阵 y : ndarray, shape(n_an_b,) 二分类标签 group : ndarray, shape(n_an_b,) 亚组标签0 为 A1 为 B rng np.random.default_rng(seed) n n_a n_b X np.zeros((n, 6)) y np.zeros(n, dtypeint) group np.zeros(n, dtypeint) # 亚组 A X[:n_a] rng.multivariate_normal( mean[0.0, 0.0, 1.0, 0.5, 0.2, 0.0], covnp.eye(6) * 0.3, sizen_a ) logit_a X[:n_a, 0] 0.5 * X[:n_a, 2] rng.normal(0, 0.2, n_a) y[:n_a] (logit_a 0).astype(int) # 亚组 B存在表征偏移 idx_b np.arange(n_a, n) X[idx_b] rng.multivariate_normal( mean[0.3, 0.6, 0.4, 0.8, 0.1, 0.2], covnp.eye(6) * 0.3, sizen_b ) logit_b X[idx_b, 0] 0.5 * X[idx_b, 2] rng.normal(0, 0.2, n_b) y[idx_b] (logit_b 0).astype(int) group[idx_b] 1 return X, y, group这里的核心思路是让亚组 B 的特征均值发生偏移模拟真实世界中的设备差异、人群异质性等因素同时保证模型仍然可以学习到一定规律避免数据过于理想或过于随机。5.2 训练模型并计算分组指标接下来定义一个函数完成训练集、测试集划分、模型训练和分组 AUC 计算。# model_eval.py import numpy as np from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.metrics import roc_auc_score def train_and_predict(X, y, test_size0.3, random_state0): 划分训练集和测试集训练逻辑回归模型返回预测概率。 X_train, X_test, y_train, y_test train_test_split( X, y, test_sizetest_size, random_staterandom_state, stratifyy ) clf LogisticRegression(max_iter1000) clf.fit(X_train, y_train) proba clf.predict_proba(X_test)[:, 1] return y_test, proba, None def evaluate_subgroup_auc(y_true, proba, group): 分别计算每个亚组在测试集上的 AUC。 aucs {} for g in np.unique(group): mask group g if mask.sum() 2 or len(np.unique(y_true[mask])) 2: aucs[int(g)] np.nan else: aucs[int(g)] roc_auc_score(y_true[mask], proba[mask]) return aucs这里需要注意一个细节当某个亚组的样本量过小或者正负样本只有一种时AUC 无法计算。在实际处理中我们直接返回 NaN避免在后续统计过程中污染结果。5.3 Bootstrap 重采样估计不确定性Bootstrap 是 FRAME 流程中最关键的一步。它的思想是在观测样本上进行有放回抽样模拟多次重新采样的效果从而估计出指标波动的置信区间。# frame_analysis.py import numpy as np from sklearn.metrics import roc_auc_score def _auc_diff(y, proba, group): 计算两个亚组 AUC 的差值B 组减去 A 组。 aucs {} for g in np.unique(group): mask group g if mask.sum() 2 or len(np.unique(y[mask])) 2: aucs[int(g)] np.nan else: aucs[int(g)] roc_auc_score(y[mask], proba[mask]) if np.isnan(aucs.get(0, np.nan)) or np.isnan(aucs.get(1, np.nan)): return np.nan return aucs[1] - aucs[0] def bootstrap_subgroup_auc(y, proba, group, n_boot1000, seed7): 对有放回重采样后的样本计算两亚组 AUC 差值。 返回一个数组包含 n_boot 次重采样的 AUC 差值。 rng np.random.default_rng(seed) diffs [] n len(y) for _ in range(n_boot): idx rng.integers(0, n, sizen) diff _auc_diff(y[idx], proba[idx], group[idx]) if not np.isnan(diff): diffs.append(diff) return np.array(diffs)5.4 置换检验计算显著性Bootstrap 能告诉我们差异波动的范围但还不能直接回答“差异是否显著”。置换检验通过随机打乱亚组标签模拟亚组身份完全不影响结果的零假设。def permutation_subgroup_diff(y, proba, group, n_perm1000, seed8): 置换检验 1. 计算观测到的真实组间 AUC 差值 2. 随机打乱亚组标签 n_perm 次 3. 统计打乱后差值比观测差值更极端的比例即置换 p 值。 rng np.random.default_rng(seed) obs_diff _auc_diff(y, proba, group) if np.isnan(obs_diff): return np.nan count 0 for _ in range(n_perm): shuffled_group rng.permutation(group) perm_diff _auc_diff(y, proba, shuffled_group) if not np.isnan(perm_diff) and abs(perm_diff) abs(obs_diff): count 1 return count / n_perm这里使用双侧检验因为我们在关注“组 A 是否比组 B 差”的同时也需要关注“组 B 是否比组 A 差”。在医学影像公平性评估中两种方向的差异都可能存在。5.5 表征层面漂移分析当组间差异通过显著性检验后下一步是定位表征层面的差异来源。这里我们使用一个简单但有效的方法对每个特征维度计算两组样本之间的 Wasserstein 距离。from scipy.stats import wasserstein_distance def representation_shift(X, group, top_k3): 计算每个特征维度上两个亚组的 Wasserstein 距离 用于定位表征分布差异最明显的特征。 X_a X[group 0] X_b X[group 1] distances [] for j in range(X.shape[1]): dist wasserstein_distance(X_a[:, j], X_b[:, j]) distances.append(dist) distances np.array(distances) top_indices np.argsort(distances)[::-1][:top_k] return distances, top_indicesWasserstein 距离可以理解为把一个分布变换成另一个分布所需的最小“搬运代价”。它比简单的均值差更稳定也能反映分布的整体偏移情况。5.6 串起整个分析流程最后把前面的步骤全部串起来形成完整的主流程。# run_demo.py import numpy as np from data_sim import make_synthetic_imaging_features from model_eval import train_and_predict, evaluate_subgroup_auc from frame_analysis import ( bootstrap_subgroup_auc, permutation_subgroup_diff, representation_shift ) # 1. 生成模拟数据 X, y, group make_synthetic_imaging_features(n_a400, n_b200, seed42) # 2. 训练并预测 y_test, proba, _ train_and_predict(X, y, random_state0) # 需要拿到测试集对应的 group from sklearn.model_selection import train_test_split _, _, _, _, group_test, _ train_test_split( X, y, group, test_size0.3, random_state0, stratifyy ) # 3. 分组 AUC aucs evaluate_subgroup_auc(y_test, proba, group_test) print(分组 AUC:, aucs) # 4. Bootstrap 区间 boot_diffs bootstrap_subgroup_auc(y_test, proba, group_test, n_boot1000, seed7) print(Bootstrap 组间 AUC 差值 95% 区间:, np.percentile(boot_diffs, [2.5, 97.5])) # 5. 置换检验 p_value permutation_subgroup_diff(y_test, proba, group_test, n_perm1000, seed8) print(置换检验 p 值:, p_value) # 6. 表征漂移分析 distances, top_indices representation_shift(X[group_test 0], X[group_test 1]) print(各特征 Wasserstein 距离:, distances) print(差异最大的特征索引:, top_indices)运行后你会看到类似下面的输出结构分组 AUC: {0: 0.73, 1: 0.69} Bootstrap 组间 AUC 差值 95% 区间: [-0.08, 0.04] 置换检验 p 值: 0.18 各特征 Wasserstein 距离: [0.32 0.48 0.51 0.70 0.12 0.15] 差异最大的特征索引: [3 2 1]这样的结果说明虽然一次评估中组 B 的 AUC 更低但 Bootstrap 区间跨过了 0置换检验 p 值也较大。当前证据不足以认定组 B 存在系统性劣势需要更大样本或更严格的评估设计。5.7 结果解读流程拿到输出后建议按下面的顺序做判断第一步看 Bootstrap 区间。如果区间不含 0说明观测差异在统计上比较稳定如果区间跨过 0说明差异容易受采样波动影响。第二步看置换检验 p 值。若 p 值小于 0.05可以认为在 5% 显著性水平下组间差异不像是随机打乱标签能解释的。第三步若前两步都支持差异存在再看表征漂移分析结果重点关注 Wasserstein 距离较大的特征。这些特征往往对应真实的影像征象差异是后续干预的候选方向。6. 真实临床队列中的注意点合成数据跑通流程后迁移到真实数据时还需要额外注意几个问题。6.1 样本量规划在真实临床队列中亚组样本量往往更小而且正负样本比例极不平衡。此时 Bootstrap 和置换检验虽然仍然有效但置信区间会非常宽。建议先做样本量评估确认每个亚组至少有多少例才能支撑有意义的公平性结论。6.2 重复样本与相关性问题医学影像数据常包含同一患者的多次随访图像。这些图像之间高度相关直接进行随机 Bootstrap 会低估不确定性。更合理的做法是以患者为单位进行分块重采样而不是以图像为单位。6.3 多重比较问题如果在一次分析中同时评估多个亚组、多个指标会产生多重比较问题。例如同时比较 5 个亚组和 3 个指标就有 15 次检验。此时建议使用 Bonferroni 校正或控制 FDR避免偶然出现的显著结果被误读。6.4 数据合规与伦理真实医学影像数据的使用必须遵循所在机构的伦理审批和数据使用协议。任何公平性分析都不应脱离医学伦理框架分析结论也不应直接用于歧视性决策。7. 常见问题与排查思路问题现象常见原因解决思路某个亚组 AUC 输出 NaN该组样本量过小或该类只有一种标签检查分组样本量合并过小亚组或采用其他指标Bootstrap 区间非常宽测试集样本量不足增加测试集规模或使用分层抽样保证每组样本量置换检验 p 值很大组间差异确实不显著或置换次数不足增加置换次数同时结合 Bootstrap 区间判断特征漂移分析找不出明显特征差异可能存在于深层非线性关系中改用核方法或深度学习中间层特征进行分析换一个随机种子结果差异很大测试集划分不稳定使用多次交叉验证并对多次结果做聚合统计分组 AUC 高但校准差只关注排序指标忽视了概率校准增加 calibration 曲线和 Brier Score 分析8. 最佳实践与工程建议8.1 固定随机种子医学影像公平性分析涉及数据划分、Bootstrap 重采样、置换检验等多个随机过程。任何一步没有固定随机种子结果都可能不可复现。建议在代码入口统一设置随机种子并在实验记录中保存所用种子值。8.2 采用多种评估视角单看 AUC 容易遗漏问题。实际工程项目中建议同时报告灵敏度与特异度阳性预测值与阴性预测值校准曲线与 Brier Score不同决策阈值下的指标变化。8.3 区分探索性分析与验证性分析如果是在探索阶段可以放宽显著性要求重点看差异趋势。但如果要得出“模型在某亚组上存在公平性风险”的结论就必须使用验证性分析标准预注册分析计划防止事后选择性地解读结果。8.4 把分析流程封装成报告工程上不建议每次只用 Jupyter Notebook 跑一遍。更推荐把 FRAME 流程封装成函数输出标准化的报告包括分组指标汇总表Bootstrap 区间图置换检验结果特征漂移排序表。这样可以方便算法工程师、临床研究者和监管审查三方共同审阅。8.5 最小权限与生产安全在真实医疗 IT 环境中运行分析代码时数据库连接和生产模型接口都需要遵循最小权限原则。只读取完成分析所需的数据子集不修改生产表不把分析脚本直接部署到未经验证的临床系统。9. 总结与后续学习方向FRAME 提供了一个非常实用的思维模式在医学影像公平性评估中先通过重采样和置换检验把采样变异剥离出去再对稳定存在的差异做表征层面归因。本文用一套完整的 Python 示例演示了从数据模拟、模型训练、分组评估到统计检验和特征漂移分析的整个流程。对于初学者建议在合成数据上跑通整个流程理解 Bootstrap 区间和置换检验 p 值的含义再去尝试真实影像数据。对于有工程经验的开发者建议把分析流程封装成标准化工具并在项目早期就纳入公平性评估环节而不是等模型上线前才补做。后续可以进一步学习的方向包括深度特征空间的公平性约束、因果推断在医学影像公平性中的应用、联邦学习中的跨机构公平性评估以及如何在多中心数据集上扩展 FRAME 分析框架。实际项目中优先关注样本量规划和多重比较控制这两点是保证公平性结论可信的核心前提。