评分卡

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

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



KS分箱是一种用于对连续变量进行分箱的算法,它基于最大KS(Best-KS)切割原理,属于有监督分箱

本文讲解最大KS分箱的原理、算法流程,并通过一个案例展示KS分箱的具体过程,以及KS分箱的代码实现

通过本文,可以了解KS分箱是什么,它的分箱原理与分箱过程,以及如何用代码实现KS分箱来对连续变量进行分箱





    01. 什么是KS分箱    





本节介绍什么是最大KS分箱,快速了解KS分箱是什么





    什么是KS分箱      


KS分箱又称为最大KS分箱,它是基于KS值来对变量分箱的一种方法,属于有监督分箱
 由于KS分箱基于KS,如果不了解KS是什么,可以参考文章《KS与KS曲线
最大KS分箱是先将整体数据作为一个分箱
然后利用KS作为切割依据,不断将分箱一分为二,直到数据分为n个分箱
 如下,KS分箱就是根据最大KS值所在处,将分箱不断一分为二,直到达到目标分箱个数
 什么是最大KS分箱 
备注:由于KS分箱是基于KS值来进行分箱的一种方法,所以它只适用于二分类







    02. KS分箱-算法流程    





本节讲解最大KS分箱的算法流程和具体细节





   KS分箱-算法流程    


KS分箱的算法流程如下:
 KS分箱的算法流程
 
1. 初始化分箱列表                                                                                     
  初始的分箱列表,就只有一个箱[min,max]                                          
2. 用KS进行对待分箱进行二分箱                                                               
            对“待分-箱”进行最大KS二分箱,分后得到两个子箱,检验本次分箱是否有效
          如果有效,将该箱分裂成两个子箱,如果无效,该箱不分裂,并标记“不需再分”

3. 判断新的分箱是否需要再分                                                                   
     在2中如果分裂有效,就得到两个新的子箱,判断两个子箱是否需要再分箱
      如果需要继续分箱,标记为“待分”,如果不需要分箱,标记为“不需再分”

4. 检查是否达到终止条件                                                                          
 检查是否满足分箱终止条件,否则重复2                           
 分箱终止条件:所有“箱”都是“不需再分-箱”,或者达到目标箱数 






     KS分箱中的条件判断    


在主流程中,“分箱有效判断”与“判断子箱是否需要再分”的细节如下:
 分箱有效的判断依据
使用KS进行分箱后,需要判断分箱是否有效
 分箱是否有效的判断依据如下:
 
1. 子箱样本足够                                                                                 
 如果分出的子箱样本量过少,则本次分箱无效                            
 2. WOE方向与整体保持一致                                                              
                 分箱后两个子箱WOE的方向必须与整体保持一致,整体方向由第一次分箱决定
            例如首次分出两箱的WOE是左大右小,则之后都要保持左大右小,否则分箱无效

WOE方向指的是判断,其中:
    
          
                   备注:由于只是比较左右WOE的大小,所以可以只比较内部的好坏比就可以
                     即判断 就可以得到
    判断子箱是否需要再分的依据    
在KS分箱流程中,将分箱一分为二后,得到两个子箱
 子箱是否需要再分的判断依据如下:
1. 箱内全是同一类:如果全是同一类,则不需要再分               
2. 箱内样本量过少:如果箱内样本量过少,则不需要再分           








    03. KS分箱-详细实例解说   





本节通过一个具体的实例,一步一步解说KS分箱是如何执行的





     KS实例解说-问题与数据    


以breast_cancer里的数据第一个变量为例,讲解如何进行KS分箱
 数据如下:
 breast_cancer的第一个变量
 其中,x为breast_cancer的第一个变量
 
下面使用最大KS分箱法对x进行分箱,
目标是:最大分箱不超过6个,且每个分箱样本不低于20个





     KS实例解说-分箱过程    


用最大KS分箱法对x进行分箱的具体过程如下:
一、初始分箱                                                                                                                 
 初始分箱只有一个,就是[min(x),max(x)] = [6.981, 28.11]                 
由于分箱的样式统一为(a,b],我们不妨将6.981向下截取一点                 

因此,最终得到初始分箱为:init_bin = (6.881, 28.11]                                     
     二、对初始分箱进行KS二分箱                                                                                             
 初始分箱进行KS二分箱
 如图所示,先求切割点,再判断切割是否有效,如果有效则将箱一分为二         
1. 先求切割点                                                                                                   
 取出初始分箱的数据(即6.881<x<=28.11 的样本)                        
求取初始分箱数据的最大KS点对应的x, 这里求得x为14.99               
         以14.99为切割点,将箱体一分为二,得到两个子箱:(6.881,14.99 ],(14.99,28.11]

 2. 判断切割是否有效                                                                                          
 (1)  判断子箱样本是否足够                                                                         
 左箱有395个样本,右箱有174个样本,结论:足够                       
 (2)  判断woe方向是否正确                                                                         
 左箱坏样本个数/左箱好样本个数= 344/51 = 6.74                       
右箱坏样本个数/右箱好样本个数= 13/161 = 0.08                       

6.74> 0.08 ,左箱woe>右箱woe ,故woe方向为下降                              
 由于本次是第一次分箱,不需要判断woe方向是否满足                    
但需要记录下本次的woe方向,之后的分箱要与此保持一致               

结论:样本量足够,且woe方向满足,本次分箱有效                                  
3. 将箱一分为二,得到(6.881,14.99 ],(14.99,28.11]                                           
三、判断新分箱是否需要再分                                                                                         
 判断新分箱是否需要再分             
1. 判断左箱(6.881,14.99 ]是否需要再分                                                             
 (1)判断样本是否足够:左箱有395个样本,样本足够                     
(2)判断是否全为一类:好坏样本分别为344和51,未全为好或坏
                  
结论:左箱需要继续分箱                                                                           
2. 判断右箱(14.99,28.11]是否需要再分                                                              
 (1)判断样本是否足够 :左箱有174个样本,样本足够                    
(2)判断是否全为一类:好坏样本分别为13和161,未全为好或坏          

结论:右箱需要继续分箱                                                                          
四、判断是否终止分箱                                                                                                   
判断是否要终止分箱:                                                                                     
 1. 是否有待分箱:当前还有两个分箱需要分                                
2. 箱数是否足够:当前只有两够分箱,未达到最大箱数                     

 结论:继续分箱                                                                                                
五、继续重复分箱                                                                                                          
继续对(6.881,14.99 ]进行分箱......                                                                       
如此循环.......                                                                                                     
整个分箱过程如下:
 
ks分箱过程
 
最后得到分箱结果如下: 
 KS分箱结果







     04. KS分箱-代码实现     




本节展示如何用代码实现KS分箱,用于对变量进行自动分箱




    最大KS分箱代码    


目前python没有很统一使用的KS分箱的包,更多的是自己实现
下面用python实现KS分箱,它只适用于连续变量与二分类标签
 具体代码实现如下:
# -*- coding: utf-8 -*-
"""
最大KS分箱的代码实现和Demo
本代码由《老饼讲解-机器学习》www.bbbdata.com编写
"""
from sklearn.metrics import roc_curve
import numpy as np
from sklearn.datasets import load_breast_cancer
import pandas as pd

# 将类别转为类别矩阵(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

#---------------------以上部分只用于统计分箱结果详情,与KS分箱算法无关-----------------------

# 获取ks切割点
def getKsCutPoint(x,y):
    fpr, tpr, thresholds= roc_curve(y, x)          # 计算fpr,tpr
    ks_idx = np.argmax(abs(fpr-tpr))               # 计算最大ks所在位置
    #由于roc_curve给出的切割点是>=作为右箱,而我们需要的是>作为右箱,所以切割点应向下再取一位,也即索引向上取一位
    return thresholds[ks_idx+1]                    # 返回切割点


# 检查切割点是否有效
def checkCutValid(x,y,cutPoint,woe_asc,min_sample):    
    left_y           = y[x<=cutPoint]                                       # 左箱的y        
    right_y          = y[x>cutPoint]                                        # 右箱的y  
    check_sample_num = min(len(left_y),len(right_y))>=min_sample            # 检查左右箱样本是否足够
    left_rate        = sum(left_y)/max((len(left_y)-sum(left_y)),1)         # 左箱好坏比例
    right_rate       = sum(right_y)/max((len(right_y)-sum(right_y)),1)      # 右箱好坏比例
    cur_woe_asc      = left_rate<right_rate                                 # 通过好坏比例的比较,确定woe是否上升
    
    check_woe_asc    = True if woe_asc ==None else  cur_woe_asc == woe_asc  # 检查woe方向是否与预期一致
    woe_asc          = cur_woe_asc if woe_asc ==None else woe_asc           # 首次woe方向为空,需要返回woe方向   
    cut_valid        = check_sample_num & check_woe_asc                     # 样本足够且woe方向正确,则本次切割有效
    return cut_valid,woe_asc

# 获取箱体切割点
def cutBin(bin_x,bin_y,woe_asc,min_sample):
    cutPoint = getKsCutPoint(bin_x,bin_y)                                          # 获取最大KS切割点
    is_cut_valid,woe_asc = checkCutValid(bin_x,bin_y,cutPoint,woe_asc,min_sample)  # 检查切割点是否有效
    if( not is_cut_valid):                                                         # 如果切割点无效
        cutPoint = None                                                            # 返回None
    return  cutPoint,woe_asc                                                       # 返回切割点

# 检查箱体是否不需再分
def checkBinFinish(y,min_sample):
    check_sample_num =  len(y)<min_sample                          # 检查样本是否足够
    check_class_pure = (sum(y) == len(y))| (sum(y) == 0)           # 检查样本是否全为一类
    bin_finish       =  check_sample_num | check_class_pure        # 如果样本足够或者全为一类,则不需再分
    return bin_finish                                      

# KS分箱主流程
def ksMerge(x,y,min_sample,max_bin):
    # -----初始化分箱列表等变量----------------
    un_cut_bins = [[min(x)-0.1,max(x)]]     # 初始化待分箱列表
    finish_bins = []                        # 初始化已完成分箱列表
    woe_asc     = None                      # 初始化woe方向
    # -----如果待分箱体不为空,则对待分箱进行分箱----------------
    for i in range(10000):                                         # 为避免有bug使用while不安全,改为for           
        cur_bin = un_cut_bins.pop(0)                               # 从待分箱列表获取一个分箱
        bin_x   = x[(x>cur_bin[0])&(x<=cur_bin[1])]                # 当前分箱的x
        bin_y   = y[(x>cur_bin[0])&(x<=cur_bin[1])]                # 当前分箱的y
        cutPoint,woe_asc = cutBin(bin_x,bin_y,woe_asc,min_sample)  # 获取分箱的最大KS切割点
        if (cutPoint==None):                                       # 如果切割点无效
            finish_bins.append(cur_bin)                            # 将当前箱移到已完成列表
        else:                                                      # 如果切割点有效
            # ------检查左箱是否需要再分,需要再分就添加到待分箱列表,否则添加到已完成列表-----
            left_bin    = [cur_bin[0],cutPoint]                    # 生成左分箱
            left_y      = bin_y[bin_x <=cutPoint]                  # 获取左箱y数据
            left_finish = checkBinFinish(left_y,min_sample)        # 检查左箱是否不需再分
            if (left_finish):                                      # 如果左箱不需再分
               finish_bins.append(left_bin)                        # 将左箱添加到已完成列表
            else:                                                  # 否则
               un_cut_bins.append(left_bin)                        # 将左箱移到待分箱列表
               
             # ------检查右箱是否需要再分,需要再分就添加到待分箱列表,否则添加到已完成列表-----   
            right_bin    = [cutPoint,cur_bin[1]]                   # 生成右分箱
            right_y      = bin_y[bin_x >cutPoint]                  # 获取右箱y数据
            right_finish = checkBinFinish(right_y,min_sample)      # 检查右箱是否不需再分
            if (right_finish):                                     # 如果右箱不需再分
               finish_bins.append(right_bin)                       # 将右箱添加到已完成列表
            else:                                                  # 否则
               un_cut_bins.append(right_bin)                       # 将右箱移到待分箱列表
        # 检查是否满足退出分箱条件:待分箱列表为空或者分箱数据足够
        if((len(un_cut_bins)==0)|((len(un_cut_bins)+len(finish_bins))>=max_bin)):
            break
        
    # ------获取分箱切割点-------
    bins = un_cut_bins + finish_bins                                # 将完成或待分的分箱一起作为最后的分箱结果
    bin_cut = [cur_bin[1] for cur_bin in bins]                      # 获取分箱右边的值
    list.sort(bin_cut)                                              # 排序
    bin_cut.pop(-1)                                                 # 去掉最后一个,就是分箱切割点
    
    # ------------分箱说明--------------
    bin_desc       ='['+str(min(x))+','+str(max(x))+']'             # 如果没有切割点,就只有一个分箱[min_x,max_x]
    if (len(bin_cut)>0) :                                           # 如果有切割点
        bin_desc = getBinDesc(bin_cut)                              # 获取分箱说明
        
    # ------------返回结果--------------    
    return bin_cut,bin_desc     

#----------使用Demo----------------------------------                              
# 数据加载
breast = load_breast_cancer()
x    = breast.data[:,0]
y    = breast.target

# KS分箱
bin_cut,bin_desc  = ksMerge(x,y,min_sample=19,max_bin=6)            # 进行KS分箱,
bin_stat          = statBinNum(x,y,bin_cut)                         # 计算各个分箱的样本个数

# 打印结果
print('切割点:'  ,bin_cut)
print('分箱说明:',bin_desc)
print('---------分箱统计----------')
print(bin_stat)
代码运行结果如下:
 
KS分箱代码运行结果 
可以看到,最大KS分箱通过5个切割点,把变量分为了6个箱






好了,以上就是KS分箱的原理和代码实现了~








 End 




联系老饼