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 等距分箱结果可视化

下图展示了等距分箱的边界划分与样本分布关系:

等距分箱示意图(K=5) Bin_1 Bin_2 Bin_3 Bin_4 Bin_5 300 410 520 630 740 850 width=110 width=110 width=110 width=110 width=110 11.2% 23.8% 31.0% 22.4% 11.6% 区间宽度相等,但各箱样本占比差异明显(中间箱占比最高)

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])