第4章 等距分箱原理与实践:固定宽度分箱的方法与代码实现
等距分箱,也叫固定宽度分箱,说白了就是把数据按等长的区间切一刀。你设定好区间数,系统会自动算出每个区间的宽度,然后数据往里一扔,哪个区间就归到哪个箱。
我在项目中最早接触这个场景,是处理用户年龄特征。当时领导说“把年龄分5个段就行了”,我第一反应就是 pd.cut 一行搞定。可后来发现,年龄分布不均匀,有的箱里塞满了人,有的箱空得可怜。这就引出了等距分箱的优缺点——它简单,但不一定适配数据分布。
4.1 等距分箱的核心原理
假设你有一组数值特征 x,最小值是 x_min,最大值是 x_max。你想分 k 个箱,那么每个箱的宽度就是:
width = (x_max - x_min) / k
第 i 个箱的区间为:
[x_min + i*width, x_min + (i+1)*width) ,i = 0, 1, ..., k-1
注意:左闭右开,最后一个区间是闭区间。
来看一张图,更直观。
4.2 什么时候用等距分箱?
我个人习惯:当数据分布比较均匀,或者你不想对分布做任何假设时,等距分箱是首选。比如处理分数、距离、时间戳等纯量,区间本身有可比性。
但你要是遇到长尾分布、严重偏态的数据——比如收入、点击次数——等距分箱很可能让前几个箱挤爆,后面扫几个空箱。这时候等频分箱或自定义分箱更合适。
| 对比维度 | 等距分箱 | 等频分箱 |
|---|---|---|
| 区间宽度 | 固定 | 不固定,样本数大致相同 |
| 对分布敏感 | 是,偏态时箱内样本数差异大 | 否,自动调整区间宽度 |
| 代码复杂度 | 低,pd.cut 一行搞定 |
中等,pd.qcut 需处理重复分位点 |
| 常用场景 | 年龄、温度、分数 | 收入、点击数、价格 |
4.3 代码实现(Python + LightGBM 场景)
我直接上实战代码。假设你有一份用户信用评分数据,评分范围 300~850,你想分 10 个等距箱。
import pandas as pd
import numpy as np
# 模拟数据
np.random.seed(42)
scores = np.random.randint(300, 851, size=10000)
# 等距分箱
bins = 10
df = pd.DataFrame({'score': scores})
df['score_bin'], cut_bins = pd.cut(df['score'], bins=bins,
retbins=True, labels=False)
# 查看区间边界
print("区间边界:", cut_bins)
# 输出:区间边界: [300. 355. 410. 465. 520. 575. 630. 685. 740. 795. 850.]
# 每个区间宽度 = 55
# 统计每个箱的样本数
print(df['score_bin'].value_counts().sort_index())
你看,pd.cut 默认把最大值和最小值等分,返回一个 Categorical 类型。我习惯用 labels=False 得到整数编号,后续喂给 LightGBM 时直接当 int 特征用。如果你想要区间标签,可以传 labels=True,但 LGB 引擎需要数值型,所以整数编号更安全。
建议先做异常值截断:
df['score'] = df['score'].clip(lower=300, upper=850) 再分箱。
4.4 避坑指南:边界处理和缺失值
等距分箱遇到这几个坑,我一个个说。
- 边界外数据:
pd.cut默认右开左闭,最后一个区间是闭区间。如果你想包括两端,可以设置include_lowest=True。我建议你分箱前先打印一下cut_bins,确认边界。 - 缺失值:
pd.cut遇到 NaN 会返回 NaN。LightGBM 原生支持缺失值,你可以保留 NaN 直接传入;或者用fillna填充一个箱外值,比如 -1,再设置extend=True让区间包含它。我个人的经验是:对缺失值单独设一个箱,不要硬填到正常区间里,否则会污染模式。 - 分箱数选择: 不是越多越好。箱太多,每个箱样本太少,模型容易过拟合;箱太少,丢掉信息。我在做 Kaggle 竞赛时常用
np.histogram_bin_edges的 “sturges” 或 “fd” 规则估计箱数,然后微调。但更靠谱的做法是:等距分箱后用 LightGBM 跑几轮,看特征重要性变化。
说白了,等距分箱在尾巴处会制造“稀疏箱”,你得定期检查各箱样本数。
4.5 在 LightGBM 中的最终形态
分箱后,你把 score_bin 作为类别特征(categorical_feature)传给 LightGBM。注意:LGB 的类别特征不接受连续浮点数,所以 labels=False 返回的整数是最合适的。
import lightgbm as lgb
train_data = lgb.Dataset(df[['score_bin', 'other_features']],
label=labels,
categorical_feature=['score_bin'])
params = {
'objective': 'binary',
'metric': 'auc',
'learning_rate': 0.05,
'num_leaves': 31,
'feature_fraction': 0.8,
'verbosity': -1
}
model = lgb.train(params, train_data, num_boost_round=100)
你想想看,等距分箱的最终目的是把数值特征离散化,让树模型更容易找到分裂点。LGB 的直方图算法本来就基于分箱,你提前分好,它能减少一次建直方图的开销——虽然有重复工作,但灵活性在你手里。
本章总结一句话: 等距分箱是“图省事”时的首选,但别忘了检查分布、处理异常值和稀疏箱。代码就两行,但背后的数据理解才是关键。