[Spark/ML] 特征取值分布与特征分桶
2020/10/17
分桶
- 将连续型特征离散化为离散特征。当数值特征跨越不同的数量级时,模型可能会只对大的特征值敏感,这种情况可以考虑分桶操作。
- 分桶后得到的稀疏向量,内积乘法运算速度更快,计算结果更方便存储;对异常数据有很强的鲁棒性
分桶方法
-
等频分桶
每个桶内的数据量严格相等,可能存在的问题是同一个桶内的数据取值差异较大。
-
等距分桶
根据值域等距截取,相同数值范围内的数据落入同一个桶。适用于数据分布均匀的情况,否则可能会导致各个桶内数据量不均匀。
对于重尾分布的正数,使用对数变换处理,它将分布在高端的长尾压缩成较短的尾部,并将低端扩展成较长的头部。
-
模型分桶。使用模型找到最佳分桶,比如聚类,将特征分成多个类别,或者树模型,这种非线性模型天生具有对连续型特征切分的能力,利用特征分割点进行离散化。
注意事项
- 要让桶内的属性取值变化对样本标签的影响基本在一个不大的范围,即不能出现单个桶内,样本取值变化很大的情况;
- 每个桶内都有足够的样本,如果样本太少,随机性太大,不具有统计意义上的说服力;
- 每个桶内的样本进行分布均匀;
实战
-
绘制特征取值分布图
select day, get_json_object(json_data, '$.name') as name, get_json_object(json_data, '$.sex') as sex, get_json_object(json_data, '$.age') as age, get_json_object(json_data, '$.height') as height, get_json_object(json_data, '$.weight') as weight from database.table where day between '2020-08-01' and '2020-10-12'
注意:绘制特征值的分布图时,要体现出特征的数量(横坐标)。方法为:先通过sql将指定范围内的特征数值取出。将数据放入excel,然后将各个特征的取值按照升序排列后,绘制分布图。
-
等距分桶和等频分桶代码
- 等距分桶:关键在于根据值域取值,确定分位点
- 等频分桶:关键在于根据数据量取值,确定分位点
object BucketTest { val BUCKET_NUMBER = 5 // 可设置 val DATA_TOTAL_NUMBER = 10000 // 可设置 case class PeopleInfo(name: String, age: Double, height: Double, weight: Double) /** * 等距分桶的分段值域区间, 及对应桶编号(0,1,2,3,..., BUCKET_NUMBER-1) * @param sourceData 原始数据 * @param fun case class PeopleInfo 到 字段名称的映射函数 * @return */ def equalDistanceBucket(sourceData: RDD[PeopleInfo], fun: PeopleInfo => Double, fieldName:String): mutable.HashMap[(Double, Double), Int] = { val value_sorted_array = sourceData.map(fun).collect().sorted val max_value = value_sorted_array.last // 针对某些特殊字段, 处理头部的陡变部分, 单独分桶 val min_value = fieldName match { case "height" => 100D case "weight" => 50D } val step = (max_value - min_value) / BUCKET_NUMBER // step表示等距步长 println(s"equalDistanceBucket step = ${step}") val valueWindow = new Array[Double](BUCKET_NUMBER + 1) // 等距分桶的分位信息, 长度为 BUCKET_NUMBER + 1 for (i <- 0 until (BUCKET_NUMBER - 1)) { valueWindow(i) = min_value + step * i } valueWindow(BUCKET_NUMBER - 1) = max_value // 共 N - 1 个桶 val bucketMap = new mutable.HashMap[(Double, Double), Int]() for (i <- 0 until (BUCKET_NUMBER - 1)) { val duration = (valueWindow(i), valueWindow(i + 1)) bucketMap.put(duration, i + 1) } bucketMap.put((0.0D, valueWindow(0)), 0) // 再加第 0 个桶(头部陡变部分单独分桶), 共 N -1 + 1 个桶 bucketMap } /** * 等频分桶的分段值域区间, 以及对应桶编号(0,1,2,3,..., BUCKET_NUMBER-1) * @param sourceData * @param fun case class PictureInfo 到 字段名称的映射函数 * @return [(分段起始值, 分段终止值), 对应分桶编号] */ def equalFrequencyBucket(sourceData: RDD[PeopleInfo], fun: PeopleInfo => Double): mutable.HashMap[(Double, Double), Int] = { val value_sorted_array = sourceData.map(fun).collect().sorted val max_value = value_sorted_array.last val step = Math.floorDiv(DATA_TOTAL_NUMBER, BUCKET_NUMBER).toInt // step表示等频步长 println(s"equalFrequencyBucket step = ${step}") val valueWindow = new Array[Double](BUCKET_NUMBER + 1) // 将分位点的频次位置, 映射到值域 for (i <- 0 until BUCKET_NUMBER) { valueWindow(i) = value_sorted_array(i * step) } valueWindow(BUCKET_NUMBER) = max_value val bucketMap = new mutable.HashMap[(Double, Double), Int]() for (i <- 0 until BUCKET_NUMBER) { val duration = (valueWindow(i), valueWindow(i + 1)) bucketMap.put(duration, i) } bucketMap } // 调用方法(只演示用法,不考虑实际意义) val heightBucket = equalFrequencyBucket(sourceData, fun = (x: PeopleInfo) => x.height, "height") // 对height采用等频分桶 val weightBucket = equalDistanceBucket(sourceData, fun = (x: PeopleInfo) => Math.log(x.weight) / Math.log(2), "weight") // 对weight采用取对数后的等距分桶 }