
在机器学习模型的公平性评估中Disparate Impact差异化影响简称 DI是出场率最高的指标之一。它通常被定义为一个受保护组群的预测通过率与参考组群预测通过率之比。很多团队习惯用“DI 必须大于 0.8”这类硬性阈值来判断模型是否达标。但在实际项目里这种单点阈值很容易让几乎所有模型评审都变成“默认不通过”。原因不是 DI 没有意义而是小样本、标签噪声、决策阈值变化以及分组定义不清晰都会让这个比值失真。下面从 DI 的定义开始再到可复用的评估流程说明为什么不能只靠一个比值下结论以及如何用工程方法把误报降下来。1. 先理解 Disparate Impact 是什么1.1 从“不同分组的通过率”讲起DI 的计算不依赖模型内部结构只依赖模型输出的最终决策。假设一个信用评分模型对用户输出“通过”或“拒绝”分组字段是地区、年龄区间或系统内部定义的用户群体。DI 比较的是某一组用户的“通过率”与另一组用户的“通过率”之间的比例。公式可以写成DI 受保护组的通过率 / 参考组的通过率如果两组用户的通过率完全一致DI 等于 1.0。如果受保护组通过率不到参考组的八成也就是 DI 小于 0.8很多评审流程会直接判定为“存在明显差异”。这就是常被提到的“80% 规则”的简化版本。用一段 Python 代码可以快速算出这个值import pandas as pd import numpy as np def selection_rate(df: pd.DataFrame, group_col: str, positive_col: str, group: str) - float: sub df[df[group_col] group] if len(sub) 0: return np.nan return sub[positive_col].mean() def disparate_impact( df: pd.DataFrame, group_col: str, positive_col: str, protected: str, reference: str ) - float: protected_rate selection_rate(df, group_col, positive_col, protected) reference_rate selection_rate(df, group_col, positive_col, reference) if reference_rate 0: return np.nan return protected_rate / reference_rate这里有一个需要提前确认的问题受保护组和参考组分别指什么。不同项目里的定义可能完全不同。有的场景把人数较少的一方看成受保护组有的场景把业务统计中处于弱势的一方看成受保护组。计算 DI 之前必须把分组定义、正样本定义和参考组定义写清楚否则同一个模型会算出完全不同的指标。1.2 为什么只看一个比率会失真DI 本质上是两个比例的比值它只能反映“分组与预测结果之间是否存在强的相关性”不能直接说明“模型是否在歧视某个群体”。一个典型的失真场景是受保护组本身的正样本真实基率就低于参考组。比如注册用户中A 组用户历史逾期率确实比 B 组高模型根据历史风险特征输出较低的通过率这会导致 DI 小于 0.8。但模型可能只是把历史规律学了出来未必是故意设置了一个不公平的分组规则。反过来有些模型预测概率分布接近但分数越高集中在某一组。此时不同决策阈值下的 DI 会剧烈变化。阈值从 0.3 调整到 0.7DI 可能从合格变成不合格。于是DI 好与坏有时候不是模型本身的稳定属性而是评审者选择的“决策边界”在起作用。所以在引入任何严格阈值之前要先明确DI 是一个用来发现差异的警示信号不是最终裁决。它适合放在评估报告的前半段用来提示团队“这里值得继续往下查”。2. 用最小数据集跑通指标计算2.1 构造一份带预测概率和分组的样例数据为了看清楚 DI 的计算过程先造一组最简单的数据。这里有两组用户 A 和 B模型输出预测概率prob默认决策阈值为 0.5。np.random.seed(42) n_a 1000 n_b 1000 df pd.DataFrame({ group: [A] * n_a [B] * n_b, prob: np.concatenate([ np.random.normal(0.55, 0.15, n_a), np.random.normal(0.45, 0.15, n_b) ]) }) df[positive] (df[prob] 0.5).astype(int)这份数据中A 组和 B 组用户数量相同但 A 组预测概率均值偏高。这样设置是为了让 DI 在阈值 0.5 时小于 1方便观察计算结果。2.2 计算选择率和 DI调用前面定义的函数得到两组通过率以及 DIrate_a selection_rate(df, group, positive, A) rate_b selection_rate(df, group, positive, B) di disparate_impact(df, group, positive, A, B) print(fA 组通过率: {rate_a:.4f}) print(fB 组通过率: {rate_b:.4f}) print(fDI: {di:.4f})正常输出可能接近A 组通过率: 0.6320 B 组通过率: 0.3680 DI: 1.7174如果拿 A 组当受保护组、B 组当参考组得到的是反向比值。实际业务中一般会固定方向比如只计算“关注组 / 参考组”或者“少数群体 / 多数群体”。建议在计算函数中明确传参避免不同同学跑出相反结果。2.3 用 Bootstrap 看 DI 的置信区间单点 DI 无法反映随机波动。更稳的做法是用自助抽样Bootstrap重复计算多次得到 DI 的置信区间。def bootstrap_di( df: pd.DataFrame, group_col: str, positive_col: str, protected: str, reference: str, n_iter: int 2000, seed: int 42 ) - dict: rng np.random.default_rng(seed) values [] for _ in range(n_iter): sample df.sample(frac1.0, replaceTrue, random_staterng) val disparate_impact(sample, group_col, positive_col, protected, reference) if not np.isnan(val): values.append(val) return { lower: np.quantile(values, 0.025), median: np.quantile(values, 0.5), upper: np.quantile(values, 0.975) } result bootstrap_di(df, group, positive, A, B) print(result)输出示例{lower: 1.5401, median: 1.7183, upper: 1.9195}如果置信区间横跨 0.8 或 1.0说明当前样本量不足以支撑“差异是否真实存在”的结论。此时直接给模型打上“不通过”的标签依据并不充分。3. 指标误报的工程原因3.1 小样本会让 DI 像随机数这是评审流程里最容易被忽视的问题。假设某个受保护组一共只有 40 个样本其中 4 个被预测为正类通过率是 10%。参考组有 10000 个样本通过率是 20%。计算出来的 DI 是 0.5看起来非常“严重”。但只看数据量就知道40 个样本里的 4 个正类只要其中一个样本翻转通过率就会从 10% 变成 12.5%DI 也随之变化。更极端时如果受保护组里只有 1 个正类样本DI 的稳定性几乎为零。下面的表格展示了不同样本量下同样“10% 对 20%”的比例可能带来的判断差异受保护组样本量正类样本数受保护组通过率参考组通过率DI判断建议40410%20%0.50样本不足建议继续观察2002010%20%0.50需要结合置信区间判断200020010%20%0.50差异方向值得深入排查生产环境中如果模型评估样本不够先不要讨论阈值而是先补数据、延长观察周期或者使用分层抽样保证每个分组都能获得足够样本。3.2 决策阈值移动会影响 DI 结论很多模型输出的是预测概率而不是最终决策。业务方在 0.5 处切一刀DI 是 0.75把阈值改成 0.4DI 可能变成 0.82改成 0.6DI 又可能掉到 0.7。用同一份数据扫描不同阈值可以很清楚看到这个现象for threshold in [0.3, 0.4, 0.5, 0.6, 0.7]: df[decision] (df[prob] threshold).astype(int) di_value disparate_impact(df, group, decision, A, B) print(fthreshold{threshold:.1f}, DI{di_value:.4f})输出类似threshold0.3, DI1.4950 threshold0.4, DI1.6032 threshold0.5, DI1.7174 threshold0.6, DI1.8703 threshold0.7, DI2.0562这个例子中 DI 随阈值升高而增大说明 A 组的高分样本相对更集中。换个业务场景也可能出现 DI 随阈值升高而下降的情况。因此DI 报告必须同时写明“使用的是哪个阈值”否则线上决策阈值一旦调整之前的评估结论就失效了。3.3 标签噪声和基率差异会污染对比基准真实业务里的标签并不是准确无误的。用户投诉、人工审核、标注规则变化都会引入噪声。如果 A 组的标签噪声远高于 B 组模型学到的“规律”就会失真计算出的选择率也会失真。另外两个组的正样本基率天然不同时不能直接期望模型输出完全相同的通过率。举例来说一个风险模型面对的两个用户群体一个群体平均收入更高、违约率低另一个群体平均收入偏低、违约率高。只要模型接入的收入、负债、历史行为特征存在差异预测结果就不可能完全一样。此时 DI 偏低是“风险分布不同”的结果不能简单归因于模型有偏见。正确的做法是先把数据按照业务关键变量做分层比如收入区间、城市等级、产品类型。分层之后再计算各组 DI看看差异是存在于所有层还是只存在于某个层。只有后者才更可能指向模型或特征层面的问题。4. 建立可复用的公平性评估流程4.1 先确定数据契约再计算任何公平性指标都应建立在一份明确的数据契约上。建议在评估前记录以下内容模型名称与版本。评估数据集来源与时间窗口。正样本定义例如“放款通过”“优惠券点击”“调用成功”。受保护组字段和取值。参考组字段和取值。决策阈值。样本量要求。这些字段不需要全部展示在报告里但必须写进模型卡或评估配置文件中。否则三个月后复查同一份模型很可能发现 DI 对不上因为当时用的阈值或样本过滤条件已经没人记得了。4.2 用多指标报告代替单点判断只报告一个 DI 很容易引发争论。更实用的做法是同时输出一组基础指标让评审者看到“通过率差异”之外的信息。指标计算方式在报告中解决什么问题各组样本量分组计数判断结论是否可信各组通过率正类数量 / 组内样本量展示差异幅度DI受保护组通过率 / 参考组通过率快速发现异常比例统计显著性置换检验或卡方检验判断差异是否可能是随机波动特征分布差异特征均值、分位数对比定位差异来源报告里应该把 DI 放在“提示”位置而不是“结论”位置。结论部分要回答三个具体问题差异是否存在、差异有多大、差异是否来源于数据或业务结构。4.3 通过置换检验判断差异是否显著Bootstrap 给出置信区间置换检验则帮助回答“如果分组和结果没有关系观察到当前差异的概率是多少”。def permutation_test(df: pd.DataFrame, group_col: str, positive_col: str, n_iter: int 5000, seed: int 42) - float: rng np.random.default_rng(seed) observed disparate_impact(df, group_col, positive_col, A, B) count 0 for _ in range(n_iter): shuffled df.copy() shuffled[group_col] rng.permutation(shuffled[group_col].values) permuted disparate_impact(shuffled, group_col, positive_col, A, B) if permuted observed: count 1 return count / n_iterp 值表示在“分组与结果无关”的原假设下看到当前水平差异的概率。如果样本量很小p 值可能并不显著这时更应该谨慎下结论。4.4 所有结果写入模型卡模型卡不需要写成长文档只要把关键上下文固化下来即可。下面是一份 YAML 示例model: name: credit_score_v2 version: 2025.06.01 evaluation: dataset: apply_2025_05 threshold: 0.5 positive_label: approved group_col: city_tier protected: tier_3 reference: tier_1 disparate_impact: value: 0.76 ci_lower: 0.71 ci_upper: 0.81 sample_size_protected: 3200 sample_size_reference: 12000 permutation_pvalue: 0.002 review: conclusion: need_deeper_analysis owner: ml_platform_team有了这份文件后续任何一个同学都可以复现当天看到的结果。生产环境里模型卡应该随着数据集版本和模型版本一起归档。5. 从评估到决策什么时候该调整模型5.1 区分统计差异与业务合理差异当 DI 低于 0.8 时不要立刻想着调整阈值或换特征。第一步是把差异拆开看。常见的做法是加入业务分层变量例如城市等级、收入区间、渠道来源。如果 DI 在每个层内都普遍偏低说明差异可能是系统性的。如果只在某个层内偏低比如只有“低收入且有夜间申请记录”的用户通过率低那么问题可能来自该层的数据质量或特征覆盖不足。还可以查看特征重要性分布。如果某个强特征本身与分组高度相关比如“是否拥有房产证”和城市等级强相关那么剔除该特征不一定能消除差异反而可能削弱模型效果。工程上要做的是解释特征对差异的贡献而不是简单地删特征。5.2 不要一上来就移动决策阈值一些团队为了把 DI 拉回 0.8会直接调整分组阈值比如给受保护组降低通过线。这样做的风险很大它可能只是提高了表面上的通过率并没有改变模型内部学到的风险排序还可能让高风险用户进入后续环节。更值得尝试的方向有三类样本重加权在训练时提高受保护组样本权重让模型对分组差异更敏感。特征工程修正去掉数据收集偏差较大的特征或者加入能解释业务合理差异的上下文特征。后处理约束在部署层对输出概率做分组约束但必须评估对整体业务指标的影响。每一种方法都要以实验的方式验证不能只盯着 DI 一个指标看还要观察准确率、召回率、线上转化率、坏账率等业务结果。5.3 生产环境要做持续监控离线评估通过不代表线上永远安全。用户结构会变化数据分布会漂移业务规则也会调整。上线后的监控至少应该覆盖参与评估的分组样本量是否显著下降。DI 随时间是否出现趋势性变化。模型输出概率分布是否整体偏移。线上人工审核结果和模型预测是否背离。监控指标可以接入现有的特征平台和指标报警系统一旦 DI 的日波动超过历史置信区间就自动触发告警让算法工程师回溯数据变更和特征版本。6. 常见问题与排查清单6.1 常见问题排查表问题现象可能原因检查方式处理建议DI 一直低于 0.8决策阈值设置不统一检查评估代码里的 threshold 是否与线上一致固定阈值并记录在模型卡中分组样本量差异过大受保护组样本太少打印分组计数和正类样本数延长观察周期或采用分层抽样DI 在训练和测试集差异大数据拼接或时间窗口不一致对比两份数据集的用户分布统一特征取值逻辑和日期筛选条件同一模型两次评估结果不同随机种子未固定检查是否有随机采样和未固定 seed固定种子并保留生成数据的脚本线上通过率和离线评估不一致线上决策逻辑不同对比离线阈值、规则和后处理逻辑将线上决策函数沉淀为离线可调用的公共模块6.2 排查顺序建议遇到 DI 异常时按下面的顺序定位确认数据时间窗口和用户过滤条件一致。确认正样本定义一致。确认决策阈值一致。确认分组字段的缺失值和脏数据已处理。计算各组样本量和正类样本数排除小样本干扰。加入置信区间和置换检验判断差异是否显著。分层交叉验证定位差异来源。回归到业务方确认差异是否有合理性解释。这八个步骤可以避免大多数“指标报警但找不到原因”的现场问题。7. 最佳实践与扩展方向单一 DI 不应当成为模型评审的一票否决项。它更适合作为触发深入分析的前置信号。真正可靠的公平性评估体系要同时具备稳定的数据契约、多指标视图、显著性检验和业务解释流程。在日常项目中建议把以下几点固化为团队规范所有公平性指标计算脚本统一版本不允许每人在本地各写一份。评估数据集和模型版本必须一一对应。报告里至少包含样本量、置信区间、决策阈值和置换检验结果。业务方需要参与差异结论的解释算法团队不能单独下最终判断。模型卡随模型上线发布后续监控指标变化时同步更新。扩展方向上可以继续学习 Equalized Odds、Calibration 等更细粒度的公平性指标也可以研究因果推断方法分析“如果改变某个特征结果会如何变化”。对于准备把公平性评估做成平台能力的团队还可以把 DI 计算、置信区间、监控告警和模型卡生成封装成通用服务让不同业务线复用同一套评估逻辑。DI 的作用不是让所有模型都是坏的而是让团队在差异出现时能迅速、可复现地定位原因。只要把采样、阈值、置信区间和业务分层纳入评估流程原本让人头疼的“默认不通过”就会变成一个能被解释、能处理、能闭环的工程问题。