LightGBM分箱算法

目录

  • 等距分箱与等频分箱
  • LightGBM分箱算法
  • 实现代码
    • GreedyFindBin
    • FindBinWithZeroAsOneBin
    • GetBins
    • GetCodes

等距分箱与等频分箱

在深度学习中,通常需要对连续特征进行离散化处理,这样可以使用嵌入向量表示特征。离散化处理的方法,常见的有等距分箱和等频分箱。

等距分箱的缺点是,数据容易集中在某个区间内,导致编号基本相同,丢失大量信息。并且等距分箱通常需要一定的专家知识。

等频分箱的优点是,当数据集中在某个区间内时也不会编号完全相同,克服了等距分箱的这个缺点。而等频分箱的缺点是,当大量数据取值相同时,比如都为0,那么等频分箱就会将这些取值相同的数据标记不同的编号,导致数据异常。

LightGBM分箱算法

LightGBM使用的分箱方法克服了以上所有缺点,在此推荐一下。主要特点如下:
1、由于数据集中经常会出现零值,所以零值单独使用一个箱子。
2、相同特征值的数据一定在一个箱子中。
3、采用动态平衡机制,各个箱子中的数据量基本一致。
所以,lightgmb的分箱方法可以看作是一个等频分箱的方法,但是采用了动态平衡机制,各个箱子的数据量基本相同但又不完全相同,克服了传统等频分箱的缺点。

我参考了Lightgbm 直方图优化算法深入理解这篇博文,将其中分箱代码使用python重新实现,供大家使用。

实现代码

GreedyFindBin

#########################得到数值型特征取值的各个bin的切分点################################
def GreedyFindBin(distinct_values, counts,num_distinct_values, max_bin, total_cnt, min_data_in_bin=3):
#INPUT:
#   distinct_values 保存特征取值的数组,特征取值单调递增
#   counts 特征的取值对应的样本数目
#   num_distinct_values 特征取值的数量
#   max_bin 分桶的最大数量
#   total_cnt 样本数量
#   min_data_in_bin 桶包含的最小样本数

# bin_upper_bound就是记录桶分界的数组
    bin_upper_bound=list();
    assert(max_bin>0)
    
    # 特征取值数比max_bin数量少,直接取distinct_values的中点放置
    if num_distinct_values <= max_bin:
        cur_cnt_inbin = 0
        for i in range(num_distinct_values-1):
            cur_cnt_inbin += counts[i]
            #若一个特征的取值比min_data_in_bin小,则累积下一个取值,直到比min_data_in_bin大,进入循环。
            if cur_cnt_inbin >= min_data_in_bin:
                #取当前值和下一个值的均值作为该桶的分界点bin_upper_bound
                bin_upper_bound.append((distinct_values[i] + distinct_values[i + 1]) / 2.0)
                cur_cnt_inbin = 0
        # 对于最后一个桶的上界则为无穷大
        cur_cnt_inbin += counts[num_distinct_values - 1];
        bin_upper_bound.append(float('Inf'))
        # 特征取值数比max_bin来得大,说明几个特征值要共用一个bin
    else:
        if min_data_in_bin>0:
            max_bin=min(max_bin,total_cnt//min_data_in_bin)
            max_bin=max(max_bin,1)
        #mean size for one bin
        mean_bin_size=total_cnt/max_bin
        rest_bin_cnt = max_bin
        rest_sample_cnt = total_cnt
        #定义is_big_count_value数组:初始设定特征每一个不同的值的数量都小(false)
        is_big_count_value=[False]*num_distinct_values
        #如果一个特征值的数目比mean_bin_size大,那么这些特征需要单独一个bin
        for i in range(num_distinct_values):
        #如果一个特征值的数目比mean_bin_size大,则设定这个特征值对应的is_big_count_value为真。。
            if counts[i] >= mean_bin_size:
                is_big_count_value[i] = True
                rest_bin_cnt-=1
                rest_sample_cnt -= counts[i]
        #剩下的特征取值的样本数平均每个剩下的bin:mean size for one bin
        mean_bin_size = rest_sample_cnt/rest_bin_cnt
        upper_bounds=[float('Inf')]*max_bin
        lower_bounds=[float('Inf')]*max_bin
        
        bin_cnt = 0
        lower_bounds[bin_cnt] = distinct_values[0]        
        cur_cnt_inbin = 0
        #重新遍历所有的特征值(包括数目大和数目小的)
        for i in range(num_distinct_values-1):
            #如果当前的特征值数目是小的
            if not is_big_count_value[i]:
                rest_sample_cnt -= counts[i]        
            cur_cnt_inbin += counts[i]
            
            # 若cur_cnt_inbin太少,则累积下一个取值,直到满足条件,进入循环。
            # need a new bin 当前的特征如果是需要单独成一个bin,或者当前几个特征计数超过了mean_bin_size,或者下一个是需要独立成桶的
            if is_big_count_value[i] or cur_cnt_inbin >= mean_bin_size or \
            is_big_count_value[i + 1] and cur_cnt_inbin >= max(1.0, mean_bin_size * 0.5):
                upper_bounds[bin_cnt] = distinct_values[i] # 第i个bin的最大就是 distinct_values[i]了
                bin_cnt+=1
                lower_bounds[bin_cnt] = distinct_values[i + 1] # 下一个bin的最小就是distinct_values[i + 1],注意先++bin了
                if bin_cnt >= max_bin - 1:
                    break
                cur_cnt_inbin = 0
                if not is_big_count_value[i]:
                    rest_bin_cnt-=1
                    mean_bin_size = rest_sample_cnt / rest_bin_cnt
	bin_cnt+=1
        # update bin upper bound 与特征取值数比max_bin数量少的操作类似,取当前值和下一个值的均值作为该桶的分界点
        for i in range(bin_cnt-1):
            bin_upper_bound.append((upper_bounds[i] + lower_bounds[i + 1]) / 2.0)
        bin_upper_bound.append(float('Inf'))
    return bin_upper_bound

FindBinWithZeroAsOneBin

def FindBinWithZeroAsOneBin(distinct_values, counts,num_distinct_values, max_bin, total_cnt, min_data_in_bin=3):
#INPUT:
#   distinct_values 保存特征取值的数组,特征取值单调递增
#   counts 特征的取值对应的样本数目
#   num_distinct_values 特征取值的数量
#   max_bin 分桶的最大数量
#   total_cnt 样本数量
#   min_data_in_bin 桶包含的最小样本数

# bin_upper_bound就是记录桶分界的数组
    bin_upper_bound=list()
    assert(max_bin>0)
    # left_cnt_data记录小于0的值
    left_cnt_data = 0
    cnt_zero = 0
    # right_cnt_data记录大于0的值
    right_cnt_data = 0
    kZeroThreshold = 1e-35
    for i in range(num_distinct_values):
        if distinct_values[i] <= -kZeroThreshold:
            left_cnt_data += counts[i]
        elif distinct_values[i] > kZeroThreshold:
            right_cnt_data += counts[i]
        else:
            cnt_zero += counts[i]
    # 如果特征值里存在0和正数,则left_cnt不为-1,left_cnt-1是最后一个负数的位置
    # left_cnt实际上就是负值的个数
    left_cnt = -1
    for i in range(num_distinct_values):
        if distinct_values[i] > -kZeroThreshold:
            left_cnt = i
            break
    # 如果特征值全是负值,left_cnt = num_distinct_values
    if left_cnt < 0:
      left_cnt = num_distinct_values
    if left_cnt > 0:
      # 负数除以(正数+负数)的比例,即负数的桶数。-1的1就是0的桶。
      left_max_bin = int( left_cnt_data/ (total_cnt - cnt_zero) * (max_bin - 1) )
      left_max_bin = max(1, left_max_bin)
      bin_upper_bound = GreedyFindBin(distinct_values, counts, left_cnt, left_max_bin, left_cnt_data, min_data_in_bin)
      bin_upper_bound[-1] = -kZeroThreshold
#如果特征值存在正数,则right_start不为-1,则right_start是第一个正数开始的位置
    right_start = -1
    for i in range(left_cnt, num_distinct_values):
        if distinct_values[i] > kZeroThreshold:
            right_start = i
            break
    # 如果特征值里存在正数
    if right_start >= 0:
        right_max_bin = max_bin - 1 - len(bin_upper_bound)
        assert(right_max_bin>0)
        right_bounds = GreedyFindBin(distinct_values[right_start:], counts[right_start:],
        num_distinct_values - right_start, right_max_bin, right_cnt_data, min_data_in_bin)
      # 正数桶的分界点第一个自然是kZeroThreshold,拼接到了-kZeroThreshold后面。
        bin_upper_bound.append(kZeroThreshold)
      # 插入正数桶的分界点,形成最终的分界点数组。
        bin_upper_bound+=right_bounds
    else:
        bin_upper_bound.append(float('Inf'))
    # bin_upper_bound即数值型特征取值(负数,0,正数)的各个bin的切分点
    return bin_upper_bound

     


GetBins

def GetBins(df,col_names, max_bin, min_data_in_bin=3):
    bins={
     }
    def _count(arr):
        distinct_values=[arr[0]]
        counts=[]
        counts_dict={
     arr[0]:1}
        for i in range(1,len(arr)):
            if arr[i]==arr[i-1]:
                counts_dict[arr[i]]+=1
            else:
                distinct_values.append(arr[i])
                counts_dict[arr[i]]=1
        for x in distinct_values:
            counts.append(counts_dict[x])
        return distinct_values, counts
        
    for col in col_names:
        tmp=df[col].to_list()
        tmp.sort()
        distinct_values, counts=_count(tmp)
        num_distinct_values=len(distinct_values)
        total_cnt=sum(counts)
        bins[col]=FindBinWithZeroAsOneBin(distinct_values, counts, num_distinct_values, max_bin, total_cnt, min_data_in_bin=min_data_in_bin)

    return bins

GetCodes

# 连续特征编码
def GetCodes(df,col,bins):
    # bins 保存了col的bin_upper_bound
    def _find(x,arr):
        if x<=arr[0]:
            return 0
        left=0
        right=len(arr)-1
        while True:
            mid=(left+right)//2
            if x<=arr[mid] and x<=arr[mid-1]:
                right=mid-1
            elif x<=arr[mid] and x>arr[mid-1]:
                return mid
            elif x>arr[mid] and x<=arr[mid+1]:
                return mid+1
            else:
                left=mid+1
        return 
        
    tmp=[]
    for x in df[col]:
        tmp.append(_find(x,bins[col]))

    return tmp

有问题欢迎留言给我,我会及时答复~~

你可能感兴趣的:(深度学习,机器学习,深度学习,数据挖掘)