3. PSI计算步骤详解:分箱策略、分布占比、对数变换与求和
群体稳定性指标(PSI)的计算严格遵循“分箱 → 计算占比 → 对数变换 → 加权求和”四步流程。每一步的精度直接影响最终漂移判断的可靠性。本节将拆解每个步骤的数学原理与工程实现。
核心公式回顾:
PSI = Σ (实际占比i - 期望占比i) × ln(实际占比i / 期望占比i)
其中 i 表示第 i 个分箱,实际占比 = 当前样本数 / 当前总样本数,期望占比 = 基准样本数 / 基准总样本数。
PSI = Σ (实际占比i - 期望占比i) × ln(实际占比i / 期望占比i)
其中 i 表示第 i 个分箱,实际占比 = 当前样本数 / 当前总样本数,期望占比 = 基准样本数 / 基准总样本数。
3.1 第一步:分箱(Binning)策略
分箱是PSI计算的基础,决定了后续分布对比的粒度。评分卡场景下,分数通常为连续变量(如300~850分),必须离散化为有限个区间。
| 分箱策略 | 方法描述 | 适用场景 | 注意事项 |
|---|---|---|---|
| 等距分箱 | 将分数范围等分为K个区间(如每50分一箱) | 分数分布均匀、业务解释性强 | 极端值可能导致空箱 |
| 等频分箱 | 按基准样本的分数分位数划分,每箱样本数大致相等 | 分布偏态严重、需要避免空箱 | 边界值可能跨整数分数 |
| 业务分箱 | 按评分卡实际使用的分数段划分(如A/B/C/D/E等级) | 生产环境监控、与业务规则对齐 | 需保证基准与当前使用相同边界 |
| 决策树分箱 | 利用目标变量(如好坏标签)进行有监督分箱 | 建模阶段、特征筛选 | PSI监控中较少使用 |
工程建议: 生产环境推荐使用等频分箱(通常10~20箱),以基准样本的分数分位点为固定边界。这样既能避免空箱,又能保证每箱有足够的统计样本量(建议每箱≥100个样本)。
分箱边界一旦确定,必须固定复用到所有后续监控周期。改变分箱边界会导致PSI值不可比。
3.2 第二步:计算实际分布与期望分布占比
分箱完成后,分别计算基准样本(期望分布)和当前样本(实际分布)在每个箱中的占比。
数学定义:
- 期望占比(Expected Proportion, Ei) = 基准样本中落入第 i 箱的样本数 / 基准总样本数
- 实际占比(Actual Proportion, Ai) = 当前样本中落入第 i 箱的样本数 / 当前总样本数
避坑要点:
- 若某箱在基准或当前样本中样本数为0,直接计算会导致 ln(0) 或除以0错误。常见处理方式:对占比做微小平滑(如加0.001),或将该箱与相邻箱合并。
- 占比必须使用百分比(小数)而非绝对计数,否则PSI值会随样本量放大而失真。
以下示例展示一个10箱等频分箱的占比计算过程:
| 分箱编号 | 分数区间 | 基准样本数 | 期望占比 Ei | 当前样本数 | 实际占比 Ai |
|---|---|---|---|---|---|
| 1 | 300~350 | 1,000 | 0.100 | 1,200 | 0.120 |
| 2 | 351~400 | 1,000 | 0.100 | 1,100 | 0.110 |
| 3 | 401~450 | 1,000 | 0.100 | 950 | 0.095 |
| 4 | 451~500 | 1,000 | 0.100 | 900 | 0.090 |
| 5 | 501~550 | 1,000 | 0.100 | 850 | 0.085 |
| 6 | 551~600 | 1,000 | 0.100 | 800 | 0.080 |
| 7 | 601~650 | 1,000 | 0.100 | 750 | 0.075 |
| 8 | 651~700 | 1,000 | 0.100 | 700 | 0.070 |
| 9 | 701~750 | 1,000 | 0.100 | 650 | 0.065 |
| 10 | 751~850 | 1,000 | 0.100 | 600 | 0.060 |
3.3 第三步:对数变换与求和
得到每箱的期望占比 Ei 和实际占比 Ai 后,执行核心计算:
- 计算差值: (Ai - Ei)
- 计算对数比: ln(Ai / Ei)
- 计算单箱贡献: (Ai - Ei) × ln(Ai / Ei)
- 求和: PSI = Σ 单箱贡献
继续使用上表数据,计算过程如下:
| 箱号 | Ai | Ei | Ai - Ei | Ai / Ei | ln(Ai / Ei) | 单箱贡献 |
|---|---|---|---|---|---|---|
| 1 | 0.120 | 0.100 | +0.020 | 1.200 | +0.1823 | 0.00365 |
| 2 | 0.110 | 0.100 | +0.010 | 1.100 | +0.0953 | 0.00095 |
| 3 | 0.095 | 0.100 | -0.005 | 0.950 | -0.0513 | 0.00026 |
| 4 | 0.090 | 0.100 | -0.010 | 0.900 | -0.1054 | 0.00105 |
| 5 | 0.085 | 0.100 | -0.015 | 0.850 | -0.1625 | 0.00244 |
| 6 | 0.080 | 0.100 | -0.020 | 0.800 | -0.2231 | 0.00446 |
| 7 | 0.075 | 0.100 | -0.025 | 0.750 | -0.2877 | 0.00719 |
| 8 | 0.070 | 0.100 | -0.030 | 0.700 | -0.3567 | 0.01070 |
| 9 | 0.065 | 0.100 | -0.035 | 0.650 | -0.4308 | 0.01508 |
| 10 | 0.060 | 0.100 | -0.040 | 0.600 | -0.5108 | 0.02043 |
| PSI = Σ | 0.06621 | |||||
本例计算得到 PSI ≈ 0.066,属于中等漂移(通常阈值:PSI < 0.1 为轻度,0.1~0.25 为中度,> 0.25 为重度),需要触发告警并排查原因。
对数变换的数学意义:
ln(Ai / Ei) 具有对称性——当 Ai 偏离 Ei 相同比例时,无论方向如何,贡献的绝对值相同。例如 Ai=0.12 与 Ei=0.10 的贡献,和 Ai=0.10 与 Ei=0.12 的贡献一致。这保证了PSI对分布偏移方向不敏感,只度量整体分布差异程度。
ln(Ai / Ei) 具有对称性——当 Ai 偏离 Ei 相同比例时,无论方向如何,贡献的绝对值相同。例如 Ai=0.12 与 Ei=0.10 的贡献,和 Ai=0.10 与 Ei=0.12 的贡献一致。这保证了PSI对分布偏移方向不敏感,只度量整体分布差异程度。
3.4 PSI计算流程总览
以下SVG图完整展示了从原始分数到PSI值的四步计算流水线:
3.5 Python代码实现
以下代码实现了完整的PSI计算函数,包含空箱平滑处理:
import numpy as np
import pandas as pd
def calculate_psi(expected, actual, bins=10, method='quantile', epsilon=1e-6):
"""
计算群体稳定性指标(PSI)
Parameters:
-----------
expected : array-like
基准样本分数
actual : array-like
当前样本分数
bins : int
分箱数量(等频分箱时有效)
method : str
分箱方法:'quantile'(等频), 'equal_width'(等距)
epsilon : float
平滑因子,防止除以0或ln(0)
Returns:
--------
psi : float
PSI值
bin_df : DataFrame
每箱的详细计算过程
"""
# 1. 确定分箱边界(基于基准样本)
if method == 'quantile':
# 等频分箱:使用基准样本的分位数
percentiles = np.linspace(0, 100, bins + 1)
# 处理极端值:使用1%和99%分位数作为边界
low = np.percentile(expected, 1)
high = np.percentile(expected, 99)
# 在[low, high]范围内计算分位数
edges = np.percentile(
np.clip(expected, low, high),
percentiles[1:-1]
)
edges = np.concatenate([[low], edges, [high + 1]])
elif method == 'equal_width':
# 等距分箱
low = np.min(expected)
high = np.max(expected)
edges = np.linspace(low, high + 1, bins + 1)
else:
raise ValueError("method must be 'quantile' or 'equal_width'")
# 2. 计算每箱样本数
expected_counts, _ = np.histogram(expected, bins=edges)
actual_counts, _ = np.histogram(actual, bins=edges)
# 3. 计算占比(加平滑)
expected_pct = (expected_counts + epsilon) / (np.sum(expected_counts) + epsilon * bins)
actual_pct = (actual_counts + epsilon) / (np.sum(actual_counts) + epsilon * bins)
# 4. 计算单箱贡献
ratio = actual_pct / expected_pct
contribution = (actual_pct - expected_pct) * np.log(ratio)
# 5. 求和得到PSI
psi = np.sum(contribution)
# 6. 构建结果DataFrame
bin_df = pd.DataFrame({
'bin_lower': edges[:-1],
'bin_upper': edges[1:],
'expected_count': expected_counts,
'actual_count': actual_counts,
'expected_pct': expected_pct,
'actual_pct': actual_pct,
'ratio': ratio,
'log_ratio': np.log(ratio),
'contribution': contribution
})
return psi, bin_df
# 使用示例
np.random.seed(42)
expected_scores = np.random.normal(600, 100, 10000)
actual_scores = np.random.normal(580, 120, 10000) # 模拟漂移
psi_value, detail_df = calculate_psi(expected_scores, actual_scores, bins=10)
print(f"PSI = {psi_value:.4f}")
print(detail_df.head())
代码要点说明:
- 使用
np.percentile实现等频分箱,自动处理边界值 - 通过
epsilon平滑因子避免空箱导致的数学错误 - 返回的
detail_df包含每箱完整计算过程,便于审计和排查 - 建议将分箱边界持久化存储(如数据库或配置文件),确保跨周期一致性
3.6 常见计算陷阱与规避
| 陷阱 | 现象 | 解决方案 |
|---|---|---|
| 空箱问题 | 某箱样本数为0,导致 ln(0) 或除以0 | 加微小平滑(如1e-6)或合并相邻箱 |
| 分箱边界不一致 | 不同周期使用不同分箱,PSI不可比 | 固定分箱边界,存储为配置文件 |
| 样本量差异过大 | 基准100万,当前1万,占比波动大 | 使用占比而非绝对计数,或对当前样本做重采样 |
| 极端值干扰 | 个别极端分数拉宽分箱区间 | 使用1%~99%分位数截断后分箱 |
| 对数底数混淆 | 使用log10或log2代替自然对数 | 统一使用自然对数 ln(numpy.log) |
掌握以上四步计算流程和工程实现细节,即可在生产环境中稳定、准确地计算PSI,为后续的漂移告警阈值设定和自动化监控奠定基础。