评分卡

【算法】一篇入门之-卡方分箱

作者 : 老饼 发表日期 : 2023-01-18 12:45:22 更新日期 : 2024-12-20 03:07:24
本站原创文章,转载请说明来自《老饼讲解-机器学习》www.bbbdata.com



卡方分箱是一种基于卡方独立检验的、利用卡方值对变量进行分箱的方法

本文讲解卡方分箱的算法流程,并通过一个具体例子展示卡方分箱是如何一步一步分箱的

通过本文,可以快速了解卡方分箱是什么、卡方分箱原理、流程,以及卡方分箱的具体代码实现





   01. 什么是卡方分箱   

 




本节展示什么是卡方分箱,以及它的用途





     什么是卡方分箱     


卡方分箱是基于卡方独立检验、利用卡方值对连续变量进行变量分箱的一种方法
由于卡方分箱是基于卡方独立检验的,如果不熟悉的可以参考文章《卡方检验-独立检验
 卡方分箱的方法如下:
卡方分箱先初始化m个分箱,然后不断合并分箱,直到分箱数只剩n个
每次合并分箱时,它用卡方值判断哪两个相邻分箱的类别分布最没区分度,就合并哪两个分箱
 什么是卡方分箱
 如图,卡方分箱先初始化多个分箱,然后根据卡方值来不断进行合并,直到只剩目标分箱个数
备注:卡方分箱原论文:https://sci2s.ugr.es/keel/pdf/algorithm/congreso/1992-Kerber-ChimErge-AAAI92.pdf






    卡方分箱-算法流程     


卡方分箱的流程如下:
 卡方分箱的算法流程
 一、初始化分箱                                                                  
  将变量先初始化分为m个箱                                        
 初始化分箱可以等距、等频或其它规则进行初始化   
二、计算相邻箱的卡方值                                                   
          相邻箱的卡方值:即两个分箱对y的响应是否独立的评估   
三、合并分箱                                                                    
  找出卡方值最小值的一组相邻箱,将其合并         
 即合并最不独立的两个邻箱                       

四、判断分箱终止条件                                                      
    判断是否满足停止条件,如是,则停止,否则重复2-4 
 停止条件一般可设为:箱数小于目标箱数m              
 五、输出                                                                           
输出最终的分箱结果,以及相邻箱的p值                
上述流程涉及卡方值和p值的计算,下面是它们的计算方法
(这里仅简要列出流程,其原理请参考《卡方独立检验》)
 
 设当前的两个分箱为与 ,现用代表分箱里类别的样本个数
 然后按如下流程计算卡方值与P值:
 
 一、计算期望值                                                                                 
 
  
            其中,为第i行求和,为第j列求和, 为总样本数
二、根据公式计算卡方值                                                                
卡方值的计算公式如下:
 

三、卡方值转换为P值                                                                     
1. 确定自由度DF                                                               
 

     r 为A的行数, c 为A的列数 
2. 根据卡方概率分布积分函数计算p值                             
 
 
 是自由度为DF的卡方概率累计分布函数,它的计算通常由程序给出







     03. 卡方分箱-实例详细解说   






本节以具体数据为例,展示卡方分箱是如何一步一步进行分箱的






    卡方分箱过程-实例解说    


iris数据的sepal_length变量与类别的数据如下: 
 iris数据 
我们希望使用卡方分箱,将sepal_length分成5个箱
卡方分箱的过程如下:
 一、初始化分箱                                                                                       
先初始化分箱,我们这里将它初始化成10个分箱                        
 初始化分箱可以等距、等频分箱或其它规则初始化分箱                
 初始化分箱 
        以第一行为例,它代表 x <=4.8时,0、1、2类别各有16、0、0个
 这里统计类别个数,主要是为了方便后面计算卡方值        
 
二、计算卡方值(计算相邻两个分箱的卡方值)                                         
     分别计算相邻两个分箱(0与1、1与2.....8与9)的卡方值,结果如下:
 相邻两个分箱的卡方值             
 三、合并分箱                                                                                           
将卡方值最小的两个分箱合并                                                    
 
 根据2中结果,卡方值最小的是1号箱与2号箱
将1号箱与2号箱合并,并重置箱号,得到新的分箱表:

 按卡方值合并后的分箱 
 四、 最终分箱结果                                                                                   
重复以上步骤,直到达到分箱终止条件                                    
 重复二、三步,直到箱数等于目标箱数5,则停止
 最终结果如下:

 卡方分箱最终结果
 我们可以看到,最后分箱结果中,0 号箱基本是0类 
   1、2号箱偏向1类 ,3、4号箱偏向2类,说明得到了较好的分箱效果

 五、计算p值                                                                                            
  我们还可以计算相邻分箱的p值
p值越小,代表区分越明显,p值越大,代表分箱越没区分度

 卡方分箱的p值 
 根据上面的P值列表,可以考虑将 3-4箱合并,因为P值较大






    卡方值、P值计算实例     


上面例子涉及到卡方值和p值的计算,这里详细讲解卡方值和p值是怎么算出来的
以上述例子中,4号箱与5号箱数据:
 4号箱与5号箱数据 
   以其为例,计算卡方值的过程如下:
一、行列分别求和                                                                                                    
 4号箱与5号箱数据求和后的数据
二、计算期望值                                                                                                       
 
4号箱与5号箱的期望值
三、根据公式计算卡方值                                                                                          
                                  

四、卡方值转换p值                                                                                                  
   先计算自由度DF,如下:                                                                              
 
把卡方值代入下式,即可得到p值                                     
            
 其中,是自由度为2的卡方累计分布函数
 卡方累计分布函数需要查表或用程序计算,在python中可以如下计算:     
1-scipy.stats.chi2.cdf(df=2, x=8.7108)
 
卡方值转换p值的计算代码  
即可得到:p = 0.0128373                                           








    04. 卡方分箱-代码实现    





本节展示卡方分箱的具体代码实现





    卡方分箱-代码实现    


下面用python实现卡方分箱,它只适用于连续变量
 具体代码实现如下:
# -*- coding: utf-8 -*-
"""
卡方分箱的代码实现和Demo
本代码由《老饼讲解-机器学习》www.bbbdata.com编写
"""
import numpy as np
from sklearn.datasets import load_iris
import pandas as pd
import scipy

# 将类别转为类别矩阵(one-hot格式)
def class2Cmat(y):
    c_name = list(np.unique(y))        # 类别名称
    c_num  = len(c_name)               # 类别个数
    cMat   = np.zeros([len(y),c_num])  # 初始化类别矩阵
    for i in range(c_num):             
        cMat[y==c_name[i],i] = 1       # 将样本对应的类别标为1
    c_name = [str(i) for i in c_name]  # 类别名称统一转为字符串类型
    return cMat,c_name                 # 返回one-hot类别矩阵和类别名称

# 将切割点转换成分箱说明
def getBinDesc(bin_cut):
    # 分箱说明
    bin_first = ['<='+str(bin_cut[0])]                        # 第一个分箱
    bin_last  = ['>'+str(bin_cut[-1])]                        # 最后一个分箱
    bin_desc  = ['('+str(bin_cut[i])+','+str(bin_cut[i+1])+']' for i in range(len(bin_cut)-1)]
    bin_desc  = bin_first+bin_desc+bin_last                   # 分箱说明
    return  bin_desc

# 计算分箱详情
def statBinNum(x,y,bin_cut):
    if(len(bin_cut)==0):                                       # 如果没有切割点
        return None                                            # 返回空
    bin_desc       = getBinDesc(bin_cut)                       # 获取分箱说明
    c_mat,c_name   = class2Cmat(y)                             # 将类别转为one-hot类别矩阵
    df             = pd.DataFrame(c_mat,columns=c_name,dtype=int)   # 将类别矩阵转为dataFrame
    
    df['cn']       = 1                                         # 预设一列1,方向后面统计
    df['grp']      = 0                                         # 初始化分组序号
    df['grp_desc'] = ''                                        # 初始化分箱说明
    
    # 计算各个样本的分组序号与分箱说明
    df.loc[x<=bin_cut[0],'grp']=0                              # 第0组样本的序号 
    df.loc[x<=bin_cut[0],'grp_desc']  =bin_desc[0]             # 第0组样本的分箱说明 
    for i in range(len(bin_cut)-1):                            # 逐区间计算分箱序号与分箱说明
        df.loc[(x>bin_cut[i])&(x<=bin_cut[i+1]),'grp']      =i+1   
        df.loc[(x>bin_cut[i])&(x<=bin_cut[i+1]),'grp_desc'] =bin_desc[i+1]
         
    df.loc[x>bin_cut[-1],'grp']=len(bin_cut)                   # 最后一组样本的序号 
    df.loc[x>bin_cut[-1],'grp_desc']=bin_desc[-1]              # 最后一组样本的分箱说明 
    
    # 按组号聚合,统计出每组的总样本个数和各类别的样本个数
    col_dict     = {'grp':'max','grp_desc':'max','cn':'sum'}  
    col_dict.update({col:'sum' for col in c_name})
    df           = df.groupby('grp').agg(col_dict).reset_index(drop=True)
    return df

# 初始化分箱
'''
按等频分箱,等频分箱并不代表每个箱里的样本个数都一样,
因为如果每10个样本作为一个箱,刚好9-11样本的x取值一样,
那必须把9-11划到同一个箱。
'''
def initBin(x,y,bin_num=10):
    xx = x.copy()
    xx.sort()
    idx     =  [int(np.floor((len(x)/bin_num)*(i+1))-1) for i in range(bin_num-1)]
    bin_cut = list(xx[idx])
    return bin_cut 


#计算卡方值
def cal_chi2(pair):
    # chi2_value,p,free_n,ex = scipy.stats.chi2_contingency(pair)
    pair[pair==0]  = 1
    class_rate     = pair.sum(axis=1)/pair.sum().sum()                     # 两类样本的占比
    col_sum        = pair.sum(axis=0)                                      # 各组别的样本个数
    ex             = np.dot(np.array([class_rate]).T,np.array([col_sum]))  # 计算期望值
    chi2           = (((pair - ex)**2/ex)).sum()                           # 计算卡方值
    return chi2


# 计算P值
def cal_p(df):
    chi2_list = [cal_chi2(np.array(df.iloc[i:i+2,:])) for i in range(df.shape[0]-1)]  # 计算卡方值
    grp_num   = df.shape[1]                                                         # 计算组别个数
    free_n    = grp_num - 1                                                            # 计算自由度          
    p_list    = [1-scipy.stats.chi2.cdf(df=free_n, x=i) for i in chi2_list]            # 计算p值
    return p_list


# 卡方分箱主函数,bin_desc为分组说明
def Chi2Merge(x,y,bin_num = 5,init_bin_num=10):
    # ------------初始化--------------------------------
    
    bin_cut = initBin(x,y,bin_num=init_bin_num)               # 初始化分箱
    df = statBinNum(x,y,bin_cut)
    df.drop(columns = ['cn'])
    
    bin_cut.append(max(x))
    df['grp_desc'] = bin_cut
    
    c_name = list(np.unique(y))
    c_name = [str(i) for i in c_name]
    
    
    # ------------根据卡方值合并分箱,直到达到目标分箱数---------------------
    while(df.shape[0]>bin_num):
        # 计算卡方值
        chi2_list = [cal_chi2(np.array(df[c_name][i:i+2])) for i in range(df.shape[0]-1)] 
        
        #将卡方值最小的两组合并
        min_idx = np.argmin(chi2_list)
        
        df.loc[min_idx+1,c_name] += df.loc[min_idx,c_name]
        df.drop(min_idx, inplace = True)
        df = df.reset_index(drop=True)
        
    # ----------输出结果-----------------------------
    bin_cut = list(df['grp_desc'][:-1])   # 获取切割点
    bin_desc = getBinDesc(bin_cut)        # 获取分箱说明

    return bin_cut,bin_desc

# --------卡方分箱的使用Demo-------------------
# ------加载数据-------
iris = load_iris()    
x    = iris.data[:,0]
y    = iris.target

# ----进行卡方分箱------
bin_cut,bin_desc  = Chi2Merge(x,y,bin_num=5)   # 调用函数进行卡方分箱
bin_stat          = statBinNum(x,y,bin_cut)    # 统计分箱结果详情
p_list = cal_p(bin_stat.iloc[:,3:])            # 计算相邻分箱间的p值
p_list = [round(p,4) for p in p_list]

# ------打印结果------
print('切割点:'     , bin_cut)
print('分箱说明:'   , bin_desc)
print('分箱置信度p:', p_list)
print('---------分箱统计----------')
print(bin_stat)
运行结果如下:
 卡方分箱代码运行结果 







好了,以上就是卡方分箱的原理、流程与及实现代码了~








 End 




联系老饼