3、PSI计算前置准备:特征分箱方法
在计算群体稳定性指标(PSI)之前,必须对连续型特征进行分箱(Binning)。分箱的质量直接决定PSI对分布变化的敏感度与可靠性。本节系统介绍三种主流分箱方法、分箱数量的选择原则以及训练集与测试集之间的分箱边界对齐策略。
3.1 三种核心分箱方法
| 分箱方法 | 核心原理 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| 等距分箱 | 将特征值域等分为k个宽度相同的区间 | 特征分布均匀、无极端离群点 | 实现简单、边界直观 | 对长尾分布敏感,空箱风险高 |
| 等频分箱 | 使每个箱内包含大致相同的样本数量 | 分布偏态、样本量充足 | 避免空箱、对分布鲁棒 | 边界值可能重复,分箱宽度差异大 |
| 决策树分箱 | 利用单变量决策树寻找最优分裂点 | 需要与目标变量强关联的分箱 | 信息量最大化、边界有业务解释性 | 易过拟合、计算成本较高 |
实践建议:在PSI计算场景中,推荐优先使用等频分箱作为基线方法。等频分箱能保证每个箱都有足够的样本计算占比,避免因空箱导致PSI分母为零或数值异常。
3.1.1 等距分箱(Equal-Width Binning)
将特征的最小值到最大值区间均匀划分为k段,每段宽度 = (max - min) / k。适用于特征值分布较为均匀且无极端离群值的场景。
import numpy as np
def equal_width_bins(data, n_bins=10):
"""等距分箱,返回分箱边界列表"""
min_val, max_val = np.min(data), np.max(data)
width = (max_val - min_val) / n_bins
bins = [min_val + i * width for i in range(n_bins + 1)]
return bins
# 示例
scores = np.array([320, 450, 580, 610, 720, 830, 950])
bins = equal_width_bins(scores, n_bins=4)
print(bins)
# 输出: [320.0, 477.5, 635.0, 792.5, 950.0]
3.1.2 等频分箱(Equal-Frequency Binning)
基于分位数将数据划分为k个区间,每个区间包含大致相同的样本数。这是PSI计算中最常用的分箱方式。
def equal_freq_bins(data, n_bins=10):
"""等频分箱,基于分位数返回边界"""
percentiles = [100 * i / n_bins for i in range(n_bins + 1)]
bins = np.percentile(data, percentiles)
# 确保边界唯一(去重)
bins = sorted(set(bins))
return bins
# 示例
scores = np.array([320, 450, 580, 610, 720, 830, 950])
bins = equal_freq_bins(scores, n_bins=4)
print(bins)
# 输出: [320.0, 477.5, 635.0, 792.5, 950.0]
注意:当数据中存在大量重复值时,等频分箱可能无法严格保证每个箱样本数完全相等。此时应使用
np.percentile 的 method='nearest' 或手动处理重复边界。
3.1.3 决策树分箱(Decision Tree Binning)
使用单变量决策树(如CART)以目标变量为监督信号,选择信息增益最大的分裂点作为分箱边界。该方法能自动捕捉特征与目标之间的非线性关系。
from sklearn.tree import DecisionTreeClassifier
import numpy as np
def tree_bins(X, y, max_leaf_nodes=10):
"""决策树分箱,返回分裂点边界"""
tree = DecisionTreeClassifier(max_leaf_nodes=max_leaf_nodes)
tree.fit(X.reshape(-1, 1), y)
# 获取树的分裂阈值
threshold = tree.tree_.threshold
threshold = threshold[threshold != -2] # 排除叶节点
bins = np.sort(np.unique(np.concatenate([[np.min(X)], threshold, [np.max(X)]])))
return bins
# 示例
X = np.array([320, 450, 580, 610, 720, 830, 950])
y = np.array([0, 0, 1, 1, 1, 0, 0])
bins = tree_bins(X, y, max_leaf_nodes=4)
print(bins)
# 输出示例: [320. 515. 720. 950.]
适用提示:决策树分箱在信用评分卡(Scorecard)建模中非常流行,因为它能生成与违约率单调相关的分箱。但在纯PSI监控场景中,如果特征与目标关系不稳定,建议谨慎使用。
3.2 分箱数量的选择原则
分箱数量(k值)的选择需要在统计稳定性与变化敏感度之间取得平衡。箱数过少会掩盖局部漂移,箱数过多则导致每个箱的样本占比波动剧烈。
| 箱数(k) | 典型场景 | 优点 | 缺点 |
|---|---|---|---|
| 5 ~ 10 | 样本量较小(< 5000) | 统计稳定,空箱概率低 | 可能掩盖局部漂移 |
| 10 ~ 20 | 中等样本量(5000 ~ 50000) | 平衡稳定性和敏感度 | 需检查每个箱的样本数 |
| 20 ~ 50 | 大样本量(> 50000) | 高敏感度,可捕捉细微漂移 | 空箱风险增加,需等频分箱 |
经验法则:每个箱的样本数不应少于50(或总样本数的1%)。对于PSI计算,推荐从k=10开始,根据业务需求向上或向下调整。如果PSI值对箱数变化敏感,说明数据分布本身不稳定,应进一步排查。
3.3 分箱边界对齐策略
PSI计算的核心要求是:训练集(期望分布)与测试集(实际分布)必须使用完全相同的分箱边界。边界不对齐会导致PSI值失真,无法区分真实漂移与分箱差异。
3.3.1 标准对齐流程
- 在训练集上确定分箱边界:使用训练集数据计算分箱边界(等距/等频/决策树)。
- 将边界应用于测试集:使用训练集边界对测试集数据进行分箱计数。
- 处理测试集越界值:测试集中小于最小边界或大于最大边界的值,归入首箱或尾箱。
- 处理空箱:如果某个箱在测试集中样本数为0,占比记为0(或极小值如1e-6),避免除零错误。
def apply_bins_to_test(train_bins, test_data):
"""将训练集分箱边界应用到测试集"""
# 使用np.digitize将测试数据映射到箱索引
bin_indices = np.digitize(test_data, bins=train_bins, right=False)
# 处理越界:小于最小边界归入箱0,大于最大边界归入最后一个箱
bin_indices = np.clip(bin_indices, 0, len(train_bins) - 2)
# 统计每个箱的样本数
bin_counts = np.bincount(bin_indices, minlength=len(train_bins) - 1)
return bin_counts
# 示例
train_bins = [0, 300, 600, 900, 1200]
test_scores = np.array([150, 400, 750, 1100, 1300])
counts = apply_bins_to_test(train_bins, test_scores)
print(counts) # 输出: [1 1 1 1] (1300归入最后一个箱)
3.3.2 边界对齐的常见陷阱
| 陷阱 | 错误做法 | 正确做法 |
|---|---|---|
| 重新分箱 | 在测试集上重新计算分箱边界 | 始终使用训练集边界 |
| 忽略越界 | 丢弃测试集中超出训练集范围的值 | 归入最近边界箱(首箱或尾箱) |
| 空箱处理 | 直接跳过空箱,导致箱数不一致 | 保留空箱,占比记为0或极小值 |
| 边界精度 | 使用浮点数边界直接比较 | 使用 np.digitize 或 pd.cut 确保一致 |
代码实现提示:推荐使用
pandas.cut(bins=train_bins) 或 numpy.digitize 将训练集边界应用到测试集。避免手动编写循环比较逻辑,以减少边界精度误差。
3.4 本节小结
- 等距分箱适用于均匀分布,但易产生空箱;等频分箱是PSI计算的首选基线方法;决策树分箱适合有监督场景,但需注意过拟合。
- 分箱数量推荐从k=10开始,根据样本量调整,确保每个箱至少有50个样本。
- 边界对齐是PSI计算的前提条件:训练集边界必须严格复用到测试集,并妥善处理越界值与空箱。