3. PSI 计算步骤详解:特征分箱、计算实际占比与预期占比、计算每个箱的贡献值、求和得到 PSI
群体稳定性指标(PSI)的核心逻辑是将特征在两个时间窗口(预期 / 实际)的分布差异量化成一个可比较的数值。整个计算过程严格遵循“分箱 → 占比 → 贡献 → 求和”四步法。下面逐一拆解每个步骤的数学含义与工程实现细节。
3.1 第一步:特征分箱(Binning)
分箱的目的是将连续型特征或离散值过多的特征划分为有限个区间(箱),使分布比较在统一的粒度上进行。分箱方式直接影响 PSI 的敏感度与稳定性。
| 分箱方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 等距分箱 | 特征分布均匀,无极端值 | 实现简单,箱宽一致 | 易受异常值影响,箱内样本可能稀疏 |
| 等频分箱 | 特征分布偏态,需保证每箱样本量 | 每箱样本数相等,统计稳定 | 箱边界可能集中在密集区域 |
| 决策树分箱 | 需要与目标变量关联的分箱 | 分箱边界有业务解释性 | 计算成本高,可能过拟合 |
| 自定义分箱 | 业务规则明确(如年龄分段) | 完全可控,符合业务逻辑 | 依赖专家经验,主观性强 |
实战建议:风控模型监控中,推荐使用等频分箱(通常 10 箱)作为默认方案。等频分箱能保证每个箱在预期样本中都有足够的观测数,避免因某箱样本过少导致 PSI 计算不稳定。若特征取值较少(如枚举型),可直接将每个取值作为一个箱。
分箱完成后,需要记录每个箱的边界值(或类别值),并确保预期样本和实际样本使用完全相同的分箱规则。这是 PSI 计算正确性的前提。
3.2 第二步:计算实际占比与预期占比
在分箱基础上,分别统计每个箱在预期样本集和实际样本集中的样本占比。
- 预期占比(Pi):第 i 个箱中预期样本数 ÷ 预期样本总数
- 实际占比(Ai):第 i 个箱中实际样本数 ÷ 实际样本总数
占比计算必须使用相对比例而非绝对计数,以消除两个时间窗口样本总量不同带来的偏差。若某箱在实际样本中样本数为 0,则占比为 0%,但后续计算贡献值时需特殊处理(见步骤三)。
关键检查点:所有箱的预期占比之和必须等于 1(或 100%),实际占比之和也必须等于 1。若因四舍五入导致总和偏离 1,需做归一化处理。
3.3 第三步:计算每个箱的贡献值
每个箱对 PSI 的贡献值由以下公式给出:
贡献值_i = (A_i - P_i) × ln(A_i / P_i)
其中:
- Ai:实际占比
- Pi:预期占比
- ln(·):自然对数
该公式的数学含义是:当实际占比与预期占比完全相等时,贡献值为 0;差异越大,贡献值越大。由于对数函数在自变量趋近于 0 时发散,工程实现中需要处理两种边界情况:
| 边界情况 | 处理方式 | 原因 |
|---|---|---|
| Ai = 0 且 Pi > 0 | 贡献值 = Pi × ln(Pi / 极小值) | 避免 ln(0) 无定义,用极小正数(如 1e-10)替代 |
| Pi = 0 且 Ai > 0 | 贡献值 = Ai × ln(Ai / 极小值) | 同上,用极小正数替代分母 |
| Ai = 0 且 Pi = 0 | 贡献值 = 0 | 该箱在两个窗口均无样本,不贡献差异 |
常见错误:直接跳过 Ai = 0 的箱会导致 PSI 被低估。正确的做法是保留该箱并赋予一个极小占比(如 1e-6),使对数运算可执行,同时不显著影响最终结果。
3.4 第四步:求和得到 PSI
将所有箱的贡献值累加,即得到该特征的群体稳定性指标:
PSI = Σ (A_i - P_i) × ln(A_i / P_i) (i = 1, 2, ..., n)
其中 n 为总箱数。PSI 是一个非负实数,数值越大表示特征分布偏移越严重。业界通常使用以下阈值进行判断:
| PSI 范围 | 稳定性判断 | 建议操作 |
|---|---|---|
| PSI < 0.1 | 稳定 | 无需处理,可保留特征 |
| 0.1 ≤ PSI < 0.25 | 轻度偏移 | 需关注,建议监控趋势 |
| PSI ≥ 0.25 | 严重偏移 | 建议剔除或重新开发特征 |
3.5 完整计算流程图示
下图将上述四个步骤串联为一条可执行的计算流水线,帮助理解从原始特征到最终 PSI 值的完整路径。
3.6 计算示例(手工演算)
假设某特征“收入等级”分为 3 箱,预期样本 1000 个,实际样本 1200 个,分布如下:
| 箱编号 | 预期样本数 | 预期占比 Pi | 实际样本数 | 实际占比 Ai | Ai - Pi | ln(Ai / Pi) | 贡献值 |
|---|---|---|---|---|---|---|---|
| 低 | 300 | 0.300 | 420 | 0.350 | +0.050 | ln(0.350/0.300) = 0.154 | 0.050 × 0.154 = 0.0077 |
| 中 | 500 | 0.500 | 540 | 0.450 | -0.050 | ln(0.450/0.500) = -0.105 | -0.050 × -0.105 = 0.0053 |
| 高 | 200 | 0.200 | 240 | 0.200 | 0.000 | ln(1.000) = 0.000 | 0.000 |
| 合计 | 1.000 | 0.000 | — | PSI = 0.0130 | |||
该特征 PSI = 0.0130 < 0.1,属于稳定特征,无需处理。
3.7 工程实现要点
在 Python 中实现 PSI 计算时,建议遵循以下代码结构:
import numpy as np
def calculate_psi(expected, actual, bins=10):
"""
计算单个特征的 PSI 值
:param expected: 预期样本数组
:param actual: 实际样本数组
:param bins: 分箱数(等频分箱)
:return: PSI 值
"""
# 1. 基于预期样本确定分箱边界(等频)
percentiles = np.linspace(0, 100, bins + 1)
bin_edges = np.percentile(expected, percentiles)
bin_edges[0] = -np.inf
bin_edges[-1] = np.inf
# 2. 计算预期占比
expected_counts, _ = np.histogram(expected, bins=bin_edges)
expected_pct = expected_counts / len(expected)
# 3. 计算实际占比(使用相同分箱边界)
actual_counts, _ = np.histogram(actual, bins=bin_edges)
actual_pct = actual_counts / len(actual)
# 4. 处理边界情况:占比为 0 时替换为极小值
eps = 1e-10
expected_pct = np.where(expected_pct == 0, eps, expected_pct)
actual_pct = np.where(actual_pct == 0, eps, actual_pct)
# 5. 计算每个箱的贡献值并求和
psi = np.sum((actual_pct - expected_pct) * np.log(actual_pct / expected_pct))
return psi
代码要点:
- 使用
np.percentile实现等频分箱,确保每箱样本数大致相等。 - 将首尾边界设为 ±inf,确保所有样本都能落入某个箱。
- 用极小正数
eps替代零占比,避免对数运算报错。 - 最终 PSI 为所有箱贡献值的标量和。
3.8 常见避坑指南
- 分箱数选择:箱数过少会掩盖分布差异,箱数过多会导致每箱样本稀疏、PSI 波动大。推荐 10~20 箱,样本量较大时可适当增加。
- 预期样本与实际样本时间窗口:预期样本通常取模型开发时的训练集或验证集;实际样本取监控期(如月度、季度)的最新数据。两个窗口的时间跨度应保持一致。
- 特征缺失值处理:缺失值应单独作为一个箱参与计算,不能直接丢弃。缺失率的变化本身就是分布偏移的重要信号。
- PSI 的对称性:PSI 不具备对称性,即 PSI(预期→实际) ≠ PSI(实际→预期)。计算时务必明确哪个是基准分布。