第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

注意:左闭右开,最后一个区间是闭区间。

来看一张图,更直观。

等距分箱示意图(5等分 [0,100]) 原始分数分布:[0, 100] 连续值,宽度 = 100/5 = 20 箱1 [0, 20) 箱2 [20, 40) 箱3 [40, 60) 箱4 [60, 80) 箱5 [80, 100] 每个箱的宽度固定为20,距离相等

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 引擎需要数值型,所以整数编号更安全。

小技巧: 如果你的数据包含异常值,比如评分突然蹦出个 999 或 -100,等距分箱会把区间拉得很宽。我曾在生产上遇到过用户年龄字段混入了 0 和 999,结果分箱后大量样本挤在头尾两个箱里。
建议先做异常值截断: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 跑几轮,看特征重要性变化。
曾经踩过的一个坑: 我用等距分箱处理“用户注册天数”特征,最大值是 3650,分 10 箱,每个箱宽 365 天。结果最后一箱(3285~3650)只有几十个样本(因为注册时间超过 9 年的用户很少)。LightGBM 把这个特征的重要性降得很低——因为它分到的箱基本是噪声。后来我改成 5 箱,效果反而更好。
说白了,等距分箱在尾巴处会制造“稀疏箱”,你得定期检查各箱样本数。

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 的直方图算法本来就基于分箱,你提前分好,它能减少一次建直方图的开销——虽然有重复工作,但灵活性在你手里。

本章总结一句话: 等距分箱是“图省事”时的首选,但别忘了检查分布、处理异常值和稀疏箱。代码就两行,但背后的数据理解才是关键。