1. Bucketizer 做什么?
Bucketizer 会把输入列(连续值)按你配置的 splitsArray 切分成区间,并输出区间编号:
- 输出的桶索引范围:
[0, numSplitsInThisColumn - 2](也就是区间数量-1) - 文档描述为
[0, numSplitsInThisColumn - 1],但通常 splits 的长度为 N,会产生 N-1 个区间,因此索引通常落在[0, N-2](你可以以实际输出为准)
例如 splits 为 [-0.5, 0.0, 0.5]:
- 区间1:[-0.5, 0.0)
- 区间2:[0.0, 0.5]
最终输出 bucketIndex 为 0 或 1(两段)
2. 输入列与输出列
输入列(Input Columns)
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
inputCols |
Number | null | 需要分桶的连续特征列(可多列) |
输出列(Output Columns)
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
outputCols |
Double | null | 分桶后的离散特征(桶索引)列(可多列) |
输出是 Double 类型的桶索引(有些场景你也会想转成 Int/Long 或进一步 one-hot)。
3. 核心参数(Parameters)详解
| Key | 默认值 | 必填 | 说明 |
|---|---|---|---|
inputCols |
null | ✅ | 输入列名数组 |
outputCols |
null | ✅ | 输出列名数组 |
splitsArray |
null | ✅ | 每个输入列的切分点数组(二维数组) |
handleInvalid |
ERROR_INVALID |
否 | 遇到非法值(NaN、越界等)如何处理 |
3.1 splitsArray:分桶切分点怎么写?
splitsArray 是 Double[][],它的长度必须等于 inputCols.length,每个子数组对应一个输入列的切分点。
要求/建议:
- 每个切分点数组要单调递增
- 通常建议首尾用
-INF/+INF包住,避免边界外数据出错
比如:[-INF, 10.0, +INF]
3.2 handleInvalid:非法值如何处理?
常见策略(示例中使用了 SKIP_INVALID):
ERROR_INVALID:遇到非法值直接报错(默认)SKIP_INVALID:跳过包含非法值的记录(整行被丢弃)- 有的体系还会支持
KEEP_INVALID(保留并映射到特殊桶),具体以 Flink ML 实现为准
工程建议:
- 离线训练:可以用
ERROR_INVALID,尽早暴露数据质量问题 - 在线/生产:更常用
SKIP_INVALID或"先做清洗再分桶",避免任务被脏数据打挂
4. Java 示例逐段解读
你的示例做了 4 列特征同时分桶:
4.1 输入数据
java
DataStream<Row> inputStream = env.fromElements(Row.of(-0.5, 0.0, 1.0, 0.0));
Table inputTable = tEnv.fromDataStream(inputStream).as("f1", "f2", "f3", "f4");
只有一行数据,四个连续特征:f1=-0.5, f2=0.0, f3=1.0, f4=0.0
4.2 配置 splitsArray
java
Double[][] splitsArray =
new Double[][] {
new Double[] {-0.5, 0.0, 0.5}, // f1:两段
new Double[] {-1.0, 0.0, 2.0}, // f2:两段
new Double[] {Double.NEGATIVE_INFINITY, 10.0, Double.POSITIVE_INFINITY}, // f3:两段
new Double[] {Double.NEGATIVE_INFINITY, 1.5, Double.POSITIVE_INFINITY} // f4:两段
};
可以把它理解为:
f1在 [-0.5,0) 或 [0,0.5] -> bucket 0/1f2在 [-1,0) 或 [0,2] -> bucket 0/1f3在 (-inf,10) 或 [10,inf) -> bucket 0/1f4在 (-inf,1.5) 或 [1.5,inf) -> bucket 0/1
4.3 创建 Bucketizer 并 transform
java
Bucketizer bucketizer =
new Bucketizer()
.setInputCols("f1", "f2", "f3", "f4")
.setOutputCols("o1", "o2", "o3", "o4")
.setSplitsArray(splitsArray)
.setHandleInvalid(HasHandleInvalid.SKIP_INVALID);
Table outputTable = bucketizer.transform(inputTable)[0];
输出表会保留原列 + 新增 o1~o4 四个桶索引列。
4.4 打印输入与输出
示例把每列输入与输出读出来并打印:
java
inputValues[i] = (double) row.getField(bucketizer.getInputCols()[i]);
outputValues[i] = (double) row.getField(bucketizer.getOutputCols()[i]);
最终打印类似:
- Input Values: [-0.5, 0.0, 1.0, 0.0]
- Output Values: [?, ?, ?, ?](取决于边界规则,通常 0.0 会落在右边区间)
5. 实战建议:分桶到底怎么设计?
1)优先用 -INF / +INF 包住区间
避免线上出现"越界数据导致报错"。
2)分桶边界最好来自数据分布
常见做法:
- 等频分桶(按分位数)
- 等宽分桶(按范围均分)
- 业务阈值分桶(如价格档位、风控评分段)
3)Bucketizer 通常不是最后一步
分桶后的输出是"桶索引",很多模型更喜欢 one-hot 或 embedding:
- LR / 线性模型:桶索引 → one-hot(或直接当作类别特征处理)
- 树模型:可以直接用数值,但分桶可能帮助稳定