4. PSI 计算步骤详解:数据分箱、计算实际占比与预期占比、计算 PSI 值
群体稳定性指标(PSI)的计算过程分为三个核心步骤:数据分箱、计算实际占比与预期占比、计算 PSI 值。每一步都有严格的数学定义和工程注意事项,下面逐一拆解。
4.1 步骤一:数据分箱(Binning)
分箱是将连续变量或离散变量划分为若干个区间(箱),这是 PSI 计算的基础。分箱方式直接影响 PSI 的敏感度和稳定性。
核心原则:预期样本(训练集)和实际样本(生产集)必须使用完全相同的分箱边界,否则 PSI 会失真。
4.1.1 常见分箱方法对比
| 分箱方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 等距分箱 | 变量分布均匀,无极端值 | 实现简单,边界直观 | 对长尾分布不友好,空箱风险高 |
| 等频分箱 | 大多数评分卡场景 | 每箱样本量均衡,PSI 稳定 | 边界可能落在密集区域 |
| 分位数分箱 | 变量分布偏态严重 | 适应数据实际分布 | 分箱数需手动调参 |
| 自定义分箱 | 业务规则明确(如信用评分分段) | 业务可解释性强 | 依赖专家经验,可能过拟合 |
工程建议:推荐使用 等频分箱,通常将数据分为 10 箱(deciles)。如果样本量较小(< 1000),可减少至 5 箱;如果变量取值离散且类别少,可直接使用原始类别作为箱。
4.1.2 分箱边界对齐示例
假设训练集(预期)中某特征“年龄”的等频分箱边界为:[18, 25, 35, 45, 55, 65, 100]。生产集(实际)必须使用完全相同的边界,即使生产集中没有 18 岁以下的样本,也要保留该箱(占比为 0)。
# Python 示例:使用 pandas 进行等频分箱
import pandas as pd
import numpy as np
# 训练集(预期)分箱
train_data = pd.DataFrame({'age': np.random.randint(18, 70, 1000)})
_, bins = pd.qcut(train_data['age'], q=10, retbins=True, duplicates='drop')
print("分箱边界:", bins)
# 生产集(实际)使用相同边界
prod_data = pd.DataFrame({'age': np.random.randint(20, 65, 500)})
train_data['age_bin'] = pd.cut(train_data['age'], bins=bins, include_lowest=True)
prod_data['age_bin'] = pd.cut(prod_data['age'], bins=bins, include_lowest=True)
4.2 步骤二:计算实际占比与预期占比
分箱完成后,分别计算每个箱中样本占总样本的比例。
- 预期占比(Expected Proportion, Ei):训练集(基准)中第 i 个箱的样本数 / 训练集总样本数。
- 实际占比(Actual Proportion, Ai):生产集(当前)中第 i 个箱的样本数 / 生产集总样本数。
避坑提示:如果某个箱在实际数据中样本数为 0,则 Ai = 0。此时 ln(Ai / Ei) 会因除以 0 而无穷大。工程上通常对占比做 平滑处理(如加一个小常数 0.001),或直接将该箱的 PSI 贡献置为 0。
4.2.1 占比计算示例表
| 箱编号 | 区间范围 | 预期样本数 | 预期占比 (Ei) | 实际样本数 | 实际占比 (Ai) |
|---|---|---|---|---|---|
| 1 | [18, 25) | 100 | 0.10 | 30 | 0.06 |
| 2 | [25, 35) | 100 | 0.10 | 80 | 0.16 |
| 3 | [35, 45) | 100 | 0.10 | 50 | 0.10 |
| 4 | [45, 55) | 100 | 0.10 | 40 | 0.08 |
| 5 | [55, 100] | 100 | 0.10 | 50 | 0.10 |
4.3 步骤三:计算 PSI 值
PSI 的数学公式为:
PSI = Σ (A_i - E_i) * ln(A_i / E_i) (i = 1 到 n)
其中 n 为箱总数。每个箱的贡献值 (A_i - E_i) * ln(A_i / E_i) 累加得到最终 PSI。
4.3.1 逐箱计算过程
以上表数据为例,计算每个箱的 PSI 贡献:
| 箱编号 | Ai | Ei | Ai - Ei | Ai / Ei | ln(Ai / Ei) | 贡献值 |
|---|---|---|---|---|---|---|
| 1 | 0.06 | 0.10 | -0.04 | 0.60 | -0.5108 | 0.0204 |
| 2 | 0.16 | 0.10 | 0.06 | 1.60 | 0.4700 | 0.0282 |
| 3 | 0.10 | 0.10 | 0.00 | 1.00 | 0.0000 | 0.0000 |
| 4 | 0.08 | 0.10 | -0.02 | 0.80 | -0.2231 | 0.0045 |
| 5 | 0.10 | 0.10 | 0.00 | 1.00 | 0.0000 | 0.0000 |
| PSI 总和 | 0.0531 | |||||
本例中 PSI = 0.0531,属于轻微漂移(通常阈值:PSI < 0.1 为稳定,0.1 ~ 0.25 为轻度漂移,> 0.25 为严重漂移)。
4.4 PSI 计算流程总览
4.5 完整 PSI 计算代码实现
以下 Python 函数实现了完整的 PSI 计算,包含空箱平滑处理:
import numpy as np
import pandas as pd
def calculate_psi(expected, actual, bins=10, epsilon=1e-4):
"""
计算群体稳定性指标 (PSI)
参数:
expected: array-like, 预期分布(训练集)
actual: array-like, 实际分布(生产集)
bins: int 或 array-like, 分箱数或自定义边界
epsilon: float, 平滑常数,防止除零
返回:
psi: float, PSI 值
psi_table: DataFrame, 逐箱明细
"""
# 1. 分箱
if isinstance(bins, int):
# 等频分箱
_, bin_edges = pd.qcut(expected, q=bins, retbins=True, duplicates='drop')
else:
bin_edges = bins
# 2. 计算各箱频数
expected_bins = pd.cut(expected, bins=bin_edges, include_lowest=True)
actual_bins = pd.cut(actual, bins=bin_edges, include_lowest=True)
expected_counts = expected_bins.value_counts().sort_index()
actual_counts = actual_bins.value_counts().sort_index()
# 3. 计算占比(加平滑)
expected_pct = (expected_counts + epsilon) / (expected_counts.sum() + epsilon * len(expected_counts))
actual_pct = (actual_counts + epsilon) / (actual_counts.sum() + epsilon * len(actual_counts))
# 4. 计算 PSI
psi_value = np.sum((actual_pct - expected_pct) * np.log(actual_pct / expected_pct))
# 5. 构建明细表
psi_table = pd.DataFrame({
'bin': expected_pct.index,
'expected_pct': expected_pct.values,
'actual_pct': actual_pct.values,
'contribution': (actual_pct - expected_pct) * np.log(actual_pct / expected_pct)
})
return psi_value, psi_table
# 使用示例
train_score = np.random.beta(2, 5, 10000) # 模拟训练集评分
prod_score = np.random.beta(2.5, 4.5, 5000) # 模拟生产集评分(有漂移)
psi, details = calculate_psi(train_score, prod_score, bins=10)
print(f"PSI = {psi:.4f}")
print(details.head())
工程最佳实践:
- 将 PSI 计算封装为独立函数,便于在 MLOps 流水线中重复调用。
- 每次计算时保存分箱边界(pickle 或 JSON),确保生产环境与训练环境边界一致。
- 设置 PSI 告警阈值:
PSI > 0.25触发紧急告警,0.1 < PSI <= 0.25触发观察告警。
4.6 常见问题与避坑指南
| 问题 | 原因 | 解决方案 |
|---|---|---|
| PSI 为无穷大 | 某箱实际占比为 0,ln(0) 无定义 | 加平滑常数 epsilon(如 1e-4) |
| PSI 为 0 但数据明显漂移 | 分箱太粗,掩盖了分布变化 | 增加分箱数(如从 5 箱增至 10 箱) |
| PSI 波动剧烈 | 样本量太小,统计不稳定 | 使用累积窗口(如 7 天滑动窗口)计算 |
| 分箱边界不一致 | 训练集与生产集分别分箱 | 固定边界,从训练集导出后复用 |
掌握以上三步计算逻辑和工程细节,你就能在生产环境中准确、稳定地计算 PSI,为数据漂移监控打下坚实基础。