4. 等距分箱实战:使用 Pandas 对连续特征进行等距分箱、计算每箱占比
等距分箱(Equal-Width Binning)是特征工程与稳定性监控中最基础的分箱策略之一。它将连续特征的取值范围均匀划分为 K 个等宽区间,每个区间内的样本数可能不同,但区间宽度完全一致。本节将使用 Pandas 完成从分箱到占比计算的全流程实战。
4.1 等距分箱的核心逻辑
等距分箱的数学定义非常简单:
- 区间宽度 = (最大值 - 最小值) / 分箱数 K
- 第 i 个箱的边界 = [min + i * width, min + (i+1) * width)
这种分箱方式的优点是计算高效、实现直观;缺点是当数据分布极不均匀时,某些箱可能包含极少样本甚至为空箱。
适用场景: 评分卡分数、标准化后的指标、年龄、收入等分布相对均匀的连续变量。不适用于存在极端长尾或严重偏态的特征。
4.2 使用 Pandas 实现等距分箱
Pandas 提供了 cut() 函数,专门用于等距分箱。以下为完整实战代码:
import pandas as pd
import numpy as np
# 生成示例数据:模拟 1000 个客户的信用评分(300-850)
np.random.seed(42)
data = pd.DataFrame({
'customer_id': range(1, 1001),
'credit_score': np.random.normal(loc=650, scale=80, size=1000).clip(300, 850)
})
# 参数设置
K = 5 # 分箱数
min_val = data['credit_score'].min()
max_val = data['credit_score'].max()
bin_width = (max_val - min_val) / K
# 生成等距分箱边界
bins = [min_val + i * bin_width for i in range(K + 1)]
bins = [round(b, 1) for b in bins] # 保留一位小数
# 使用 cut() 进行分箱
data['score_bin'] = pd.cut(
data['credit_score'],
bins=bins,
include_lowest=True,
right=True,
labels=[f'Bin_{i+1}' for i in range(K)]
)
# 查看分箱结果
print("分箱边界:", bins)
print(data[['credit_score', 'score_bin']].head(10))
4.3 计算每箱样本数与占比
分箱完成后,使用 value_counts() 统计每箱样本数,并计算占比:
# 统计每箱样本数
bin_counts = data['score_bin'].value_counts().sort_index()
# 计算每箱占比
bin_ratios = bin_counts / len(data)
# 合并为 DataFrame
bin_stats = pd.DataFrame({
'样本数': bin_counts,
'占比(%)': (bin_ratios * 100).round(2)
})
bin_stats.index.name = '分箱'
print("\n等距分箱统计结果:")
print(bin_stats)
# 输出示例:
# 样本数 占比(%)
# 分箱
# Bin_1 112 11.20
# Bin_2 238 23.80
# Bin_3 310 31.00
# Bin_4 224 22.40
# Bin_5 116 11.60
注意: 使用
sort_index() 确保分箱按自然顺序排列,避免因标签顺序错乱导致后续 PSI 计算错误。
4.4 等距分箱结果可视化
下图展示了等距分箱的边界划分与样本分布关系:
4.5 等距分箱参数对比
| 参数 | 说明 | 推荐值 |
|---|---|---|
bins |
分箱边界列表或分箱数量 | 5~20(根据样本量调整) |
include_lowest |
是否包含最小值所在区间 | True |
right |
区间是否右闭(左开右闭) | True |
labels |
自定义分箱标签 | 建议使用 Bin_1 ~ Bin_K |
retbins |
是否返回分箱边界 | 调试时设为 True |
4.6 实战避坑指南
- 空箱问题: 当数据分布极不均匀时,某些等距箱可能无样本。建议检查
value_counts()结果,若出现空箱,考虑增加分箱数或改用分位数分箱。 - 边界精度: 浮点数边界可能导致样本落在区间外。使用
np.round()或Decimal控制精度,确保边界覆盖所有数据。 - 标签顺序:
labels列表必须与分箱区间一一对应,且按升序排列。使用sort_index()避免标签错乱。 - 缺失值处理:
cut()默认会为 NaN 生成 NaN 分箱。建议先使用dropna()或单独处理缺失值。
重要提醒: 等距分箱的占比结果将直接用于 PSI 计算。务必确保训练集(预期分布)与测试集(实际分布)使用完全相同的分箱边界,否则 PSI 值将失去可比性。
4.7 完整函数封装
将上述流程封装为可复用的函数,便于后续 PSI 计算调用:
def equal_width_binning(data, feature, n_bins=5):
"""
对连续特征进行等距分箱,返回分箱统计结果和分箱边界
参数:
data: DataFrame,包含目标特征
feature: str,特征列名
n_bins: int,分箱数量
返回:
bin_stats: DataFrame,每箱样本数、占比
bins: list,分箱边界
"""
# 计算等距边界
min_val = data[feature].min()
max_val = data[feature].max()
bin_width = (max_val - min_val) / n_bins
bins = [min_val + i * bin_width for i in range(n_bins + 1)]
# 分箱
labels = [f'Bin_{i+1}' for i in range(n_bins)]
data['_bin'] = pd.cut(data[feature], bins=bins,
include_lowest=True, right=True, labels=labels)
# 统计
counts = data['_bin'].value_counts().sort_index()
ratios = counts / len(data)
bin_stats = pd.DataFrame({
'样本数': counts,
'占比(%)': (ratios * 100).round(2)
})
# 清理临时列
data.drop(columns=['_bin'], inplace=True)
return bin_stats, bins
# 使用示例
stats, bin_edges = equal_width_binning(data, 'credit_score', n_bins=5)
print(stats)
print("分箱边界:", [round(b, 1) for b in bin_edges])