4. PSI 计算基础:分箱方法(等距、等频、分位数)与实际分布与预期分布的计算
群体稳定性指标(PSI)的核心在于比较两个分布之间的差异。在量化风控的高频行情场景中,我们通常将当前监控周期的数据分布称为实际分布,将基准周期(如模型开发期、上线首周)的数据分布称为预期分布。计算 PSI 的第一步,也是决定计算结果可靠性的关键步骤,就是分箱。
4.1 三种主流分箱方法
分箱的目的是将连续变量或离散变量划分为有限的区间(Bins),从而计算每个区间内的样本占比。不同的分箱方法适用于不同的数据特征和监控目标。下表对比了三种最常用的方法:
| 分箱方法 | 核心逻辑 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| 等距分箱 | 将变量取值范围划分为宽度相等的区间 | 变量分布均匀,无极端离群值 | 计算简单,区间边界直观 | 数据稀疏时,部分区间可能样本量为0 |
| 等频分箱 | 每个区间包含大致相等的样本数量 | 分布偏态严重,或需要保证每个区间都有足够样本 | 避免空箱,对离群值不敏感 | 区间宽度差异大,边界值可能不直观 |
| 分位数分箱 | 基于数据的分位数(如十分位数、四分位数)划分区间 | 需要关注分布尾部的变化,或与模型评分卡对齐 | 对分布形状自适应,常用于评分卡监控 | 分位数点计算需注意样本量 |
实操建议:在高频行情坏账率监控中,推荐优先使用等频分箱或分位数分箱。因为坏账率分布通常呈现右偏(大部分客户坏账率低,少数客户坏账率高),等距分箱容易导致前几个区间样本过多、后几个区间样本过少,使 PSI 计算失真。
4.2 分箱边界确定与分布计算
无论采用哪种分箱方法,计算 PSI 的流程都遵循以下步骤。我们以等频分箱为例,展示从原始数据到分布占比的完整计算过程。
- 确定基准分箱边界:基于预期分布(基准数据集)计算分位数,得到每个区间的上下边界。例如,将数据等频分为 10 箱,则边界为第 10%、20%……90% 分位数。
- 映射实际分布:将实际分布(当前监控数据集)的每个样本,按照基准分箱边界落入对应的区间。
- 计算区间占比:分别统计预期分布和实际分布在每个区间内的样本数量,除以各自的总样本数,得到
P_i(预期占比)和A_i(实际占比)。
关键原则:分箱边界必须固定在基准数据集上,不能随实际分布的变化而重新计算。否则,PSI 将失去衡量分布偏移的意义。
下面是一个计算示例,假设我们将某评分变量等频分为 5 箱:
| 分箱编号 | 区间边界(基于预期) | 预期占比 P_i | 实际占比 A_i |
|---|---|---|---|
| 1 | [0, 200) | 20.0% | 15.0% |
| 2 | [200, 400) | 20.0% | 22.0% |
| 3 | [400, 600) | 20.0% | 25.0% |
| 4 | [600, 800) | 20.0% | 18.0% |
| 5 | [800, 1000] | 20.0% | 20.0% |
4.3 实际分布与预期分布的计算逻辑
在得到每个区间的占比后,PSI 的计算公式为:
PSI = Σ ( (A_i - P_i) * ln(A_i / P_i) )
其中:
A_i:第 i 个区间的实际分布占比P_i:第 i 个区间的预期分布占比ln:自然对数
避坑提示:当某个区间的
A_i 或 P_i 为 0 时,ln(A_i / P_i) 将无法计算。解决方案:1)使用等频分箱避免空箱;2)对占比进行平滑处理,如加一个极小值(如 0.0001);3)合并样本量过少的相邻区间。
4.4 分箱与分布计算流程总览
下面的 SVG 图展示了从原始数据到 PSI 输出的完整计算链路,重点突出了分箱与分布计算环节:
4.5 Python 代码实现:分箱与分布计算
以下代码演示了如何使用 Python 实现等频分箱,并计算预期分布与实际分布的占比:
import numpy as np
import pandas as pd
def calculate_psi(expected, actual, bins=10):
"""
计算 PSI,使用等频分箱
:param expected: array-like, 预期分布数据(基准)
:param actual: array-like, 实际分布数据(当前)
:param bins: int, 分箱数量
:return: float, PSI 值
"""
# 1. 基于预期分布确定分位数边界(等频分箱)
percentiles = np.linspace(0, 100, bins + 1)
bin_edges = np.percentile(expected, percentiles)
# 处理边界重复(如数据中有大量相同值)
bin_edges = np.unique(bin_edges)
if len(bin_edges) < 2:
return 0.0 # 数据无变化,PSI 为 0
# 2. 计算预期分布占比 P_i
expected_counts, _ = np.histogram(expected, bins=bin_edges)
expected_pct = expected_counts / len(expected)
# 3. 计算实际分布占比 A_i(使用相同的分箱边界)
actual_counts, _ = np.histogram(actual, bins=bin_edges)
actual_pct = actual_counts / len(actual)
# 4. 平滑处理:避免 ln(0)
expected_pct = np.clip(expected_pct, 0.0001, 1.0)
actual_pct = np.clip(actual_pct, 0.0001, 1.0)
# 5. 计算 PSI
psi = np.sum((actual_pct - expected_pct) * np.log(actual_pct / expected_pct))
return psi
# 示例用法
np.random.seed(42)
expected_data = np.random.normal(0.5, 0.1, 10000) # 基准分布
actual_data = np.random.normal(0.55, 0.12, 10000) # 当前分布(发生偏移)
psi_value = calculate_psi(expected_data, actual_data, bins=10)
print(f"PSI 值: {psi_value:.4f}")
代码要点:
np.percentile用于计算等频分箱的边界,确保每个区间样本量大致相等。np.histogram使用相同的bin_edges分别统计预期和实际分布。np.clip对占比进行下限截断,避免对数计算出现负无穷。
4.6 分箱方法选择决策树
在实际项目中,如何快速选择分箱方法?下面的 SVG 决策树可以帮助你做出判断:
4.7 常见问题与避坑指南
- 分箱数量如何确定? 一般推荐 10~20 箱。箱数太少会丢失分布细节,箱数太多会导致噪声放大。高频场景下,10 箱是兼顾稳定性和敏感度的常用选择。
- 边界重复怎么办? 当数据中存在大量重复值时,分位数边界可能重合。使用
np.unique去重,并合并样本量过少的区间。 - 预期分布如何选取? 通常选取模型上线后第一个完整周期(如首周)的数据作为预期分布。后续每次监控都与此基准对比。
- 高频行情下的性能优化: 对于每秒千万级的数据流,建议预计算分箱边界并缓存,避免每次计算 PSI 时重复分箱。