Python实现BestKS分箱和IV的计算

本文介绍了一种名为BestKS的自动分箱方法,并通过实际案例展示了如何使用该方法进行变量分箱。同时,还提供了IV(信息值)的计算方法,用于评估分箱后的变量预测能力。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1 BestKS分箱

从网上翻了很多BestKS内容,结果发现不同网站不同博主的BestKS代码几乎都是相同的。这里我的代码也参考了资料1(链接在文末)

BestKS分箱的基本原理如图1

图1

PS:最近在网上找到的代码都跟参考资料1中的重复。先简单说一下这两个版本的代码(希望代码原作者看到不要生气):

  • 简单版:简单版的问题主要在于ks_zone()函数。从这个函数中可以看到,每次是选择分箱占比最大的区间进行下一步的切割。另外,求最后一个分箱的占比的时候用了50000减去前面所有分箱的总量,这个50000也有问题。如果待分箱数据的(data_)的样本总量远小于50000,那新的割点只会在最高的区间产生。如果要用这个版本的代码的话,把50000改成data_.shape[0]。
  • 复杂版:复杂版先是尽可能的分割,然后按照指定的分箱数对所有割点进行组合,选择复合条件的割点组合作为最后的分箱割点。(复杂版的代码其实已经很完备了,包括了前期数据校验,割点划分及最后的分箱结果统计等)。

而下面的这段代码是基于复杂版的代码进行改写的,对于下面这段需要说明以下几点:

  • 这段代码只能处理数值型变量。对于离散型变量需要自己先将其转化成数值型。
  • 如果原始数据中有缺失值,需要先把缺失值填充了。
  • 是否满足终止条件的判断函数verify_cut(),可以根据自己的需要自行修改。
import pandas as pd
import numpy as np

def univeral_df(data,feature,target,total_name,good_name,bad_name):
    """
    统计feature不同值对应的target数量
    feature: 将要进行分割的变量
    target: 目标变量,其值只能取0和1 1为坏样本,0为好样本
    """
    data=data[[feature,target]]
    result=data.groupby(feature)[target].agg(['count','sum'])
    result=result.sort_index()
    result.columns=[total_name,bad_name]
    result=result.fillna(0)
    result[good_name]=result[total_name]-result[bad_name]
    result=result.reset_index()
    return result

def get_max_ks(data_df,start,end,rate,total_name,good_name,bad_name):
    """
    寻找能满足的分箱占比的最大的KS对应的值
    """
    total_all=data_df[total_name].sum()
    limit=rate*total_all
    data_cut=data_df.loc[start:end,:]#data_cut统计的范围包含end
    data_cut['bad_rate_cum']=(data_cut[bad_name]/data_cut[bad_name].sum()).cumsum()
    data_cut['good_rate_cum']=(data_cut[good_name]/data_cut[good_name].sum()).cumsum()
    data_cut['total_cum']=data_cut[total_name].cumsum()
    data_cut['total_other_cum']=total_all-data_cut[total_name]
    data_cut['KS']=(data_cut['bad_rate_cum']-data_cut['good_rate_cum']).abs()
    try:
        cut=data_cut[(data_cut['total_cum']>=limit)&(data_cut['total_other_cum']>=limit)]['KS'].idxmax()
        return cut
    except:
        return np.nan

def verify_cut(data_df,cut_list,total_name,good_name,bad_name):
    """
    判断是否能继续分箱下去,返回True(继续进行切割)或False(不继续切割)。具体判断条件如下:
    1 是否存在某箱对应的类别全为0或1
    2 现有的分箱能否保证bad rate递增或递减
    3 woe的单调性和bad rate的单调性相反(这个条件感觉太严格了)
    """
    bad_all=data_df[bad_name].sum()
    good_all=data_df[good_name].sum()
    cut_bad=np.array([data_df.loc[x[0]:x[1],bad_name].sum() for x in cut_list])
    cut_good=np.array([data_df.loc[x[0]:x[1],good_name].sum() for x in cut_list])
    cond1=(0 not in cut_bad)and(0 not in cut_good)
    cut_bad_rate=cut_bad/bad_all
    cut_good_rate=cut_good/good_all
    cut_woe=np.log(cut_bad_rate/cut_good_rate)
    cond2=sorted(cut_woe,reverse=False)==list(cut_woe) and sorted(cut_bad_rate,reverse=True)==list(cut_bad_rate)
    cond3=sorted(cut_woe,reverse=True)==list(cut_woe) and sorted(cut_bad_rate,reverse=False)==list(cut_bad_rate)
    cond4=sorted(cut_bad_rate,reverse=False)==list(cut_bad_rate) or sorted(cut_bad_rate,reverse=True)==list(cut_bad_rate)
    return cond1 and cond4
    
def cut_fun(data_df,start,end,rate,total_name,good_name,bad_name):
    """
    对从start到end这一段数据进行下一步切分,并返回新的割点对
    """
    cut=get_max_ks(data_df,start,end,rate,total_name,good_name,bad_name)
    if cut:
        return [(start,cut),(cut+1,end)]
    else:
        return [(start,end)]

def cut_main_fun(data_df,feature,rate,total_name,good_name,bad_name,bins=None,null_value=False,missing_value=[]):
    """
    bins: 分箱数。默认为None,即不限定分箱数目。若为int,则为指定的分箱数
    null_value: 布尔型。字段中填充的缺失值是否需要单独划分为一箱。若为True,则单独划分为一箱。
    missing_value:若null_value为True,则missing_value中的数据即为缺失值,每个缺失值会单独成一箱
    """
    if null_value and missing_value:
        data_df=data_df[~data_df[feature].isin(missing_value)]
    start=data_df.index.min()
    end=data_df.index.max()
    cut_list=[(start,end)] #真正有效的割点集合
    tt=cut_list.copy()
    for cut_seg in tt:
        cut_bin=cut_fun(data_df,cut_seg[0],cut_seg[1],rate,total_name,good_name,bad_name)
        if len(cut_bin)>1:
            temp=cut_list.copy()
            index_seg=temp.index(cut_seg)
            temp[index_seg]=cut_bin[0]
            temp.insert(index_seg+1,cut_bin[1])
            if verify_cut(data_df,temp,total_name,good_name,bad_name):
                cut_list=temp
                tt.extend(cut_bin)
        if bins and len(cut_list)>bins:#判断是否达到限定的分箱数
            break
    #将割点对转化为对应的数值
    #cut_list中保留的割点对中的数据为data_df中的inde,这里想要获得真正的feature的割点数据则需要依据data_df的index找到对应的feature字段的值
    cut_list=sorted([-np.inf]+[data_df.loc[item[0],feature] for item in cut_list]+[np.inf]+missing_value)
    return cut_list

if __name__ == '__main__':
    #为了验证验证代码随意造的数据
    data=pd.DataFrame(np.random.randint(10,101,size=(10000,2)),columns=list('AB'))
    data['target']=pd.Series(np.random.randint(0,2,size=10000))
    data.loc[data['B']<=20,'B']=np.nan

    #对A进行分箱
    result=univeral_df(data,'A','target','total','good','bad')
    cut_bin=cut_main_fun(data_df=result,feature='A',rate=0.05,total_name='total',
              good_name='good',bad_name='bad')
    data['A_bin']=pd.cut(data['A'],cut_bin,right=False)
    print("对特征A进行分箱其结果如下:")
    print(data.groupby(['A_bin','target'],observed=True)['target'].count().unstack())

    #对B进行分箱
    data['B']=data['B'].fillna(-99)
    result=univeral_df(data,'B','target','total','good','bad')
    cut_bin=cut_main_fun(data_df=result,feature='B',rate=0.05,total_name='total',
              good_name='good',bad_name='bad',bins=5,null_value=True,missing_value=[-99])
    data['B_bin']=pd.cut(data['B'],cut_bin,right=False)
    print("对特征B进行分箱其结果如下:")
    print(data.groupby(['B_bin','target'],observed=True)['target'].count().unstack())

2 IV

1) IV的计算

IV(information value),即信息量,该指标可以只是变量的预测能力。IV值越高,变量的预测能力越强。但是该指标只能用于二分类问题。IV的计算公式如下:

IV=\sum _{i} (\frac{b_{i}}{b_{total}} - \frac{g_{i}}{g_{total}}) *WOE_{i}, 其中i为每个分箱。b_{i},g_{i}分别为每个分箱i中的坏样本及好样本的数量。b_{total},g_{total}分别为所有分箱中坏样本及好样本的总量。WOE_{i}的计算公式如下:WOE_{i}=ln(\frac{b_{i}}{b_{total}}-\frac{g_{i}}{g_{total}})

IV代码:

def computer_iv(data,feature,target,bins,right=True):
    """
    bins:为int型变量时,使用等频分箱对其进行分割;为集合类型时,为分箱割点集合
    right:当bins为list变量时,该变量才有用
    """
    if data[feature].isnull().sum()>0:
        raise Exception("{}中有缺失值,请先填充".format(feature))
    if isinstance(bins,int):
        bins=min(data[feature].nunique(),bins)
        data['bin']=pd.qcut(data[feature],bins)
    if isinstance(bins,list):
        data['bin']=pd.cut(data[feature],bins,right=right)    
    result=data.groupby('bin',observed=True)[target].agg(['count','sum'])
    result.sort_index()
    result.columns=['total','bad']
    result['good']=result['total']-result['bad']
    result=result.fillna(0)
    result['good_rate']=np.maximum(result['good'],0.5)/result['good'].sum()
    result['bad_rate']=np.maximum(result['bad'],0.5)/result['bad'].sum()
    result['woe']=np.log(result['good_rate']/result['bad_rate'])
    result['iv']=(result['good_rate']-result['bad_rate'])*result['woe']
    return result['iv'].sum()  

if __name__ == '__main__':
    #为了验证验证代码随意造的数据
    data=pd.DataFrame(np.random.randint(10,101,size=(10000,2)),columns=list('AB'))
    data['target']=pd.Series(np.random.randint(0,2,size=10000))
    data.loc[data['B']<=20,'B']=np.nan

    #计算特征A的IV
    result=univeral_df(data,'A','target','total','good','bad')
    print(computer_iv(data,'A','target',5,right=False))

    #计算特征B的IV,用了BestKS分箱
    data['B']=data['B'].fillna(-99)
    result=univeral_df(data,'B','target','total','good','bad')
    cut_bin=cut_main_fun(data_df=result,feature='B',rate=0.05,total_name='total',
              good_name='good',bad_name='bad',bins=5,null_value=True,missing_value=[-99])
    data['B_bin']=pd.cut(data['B'],cut_bin,right=False)
    print("对特征B进行分箱其结果如下:")
    print(data.groupby(['B_bin','target'],observed=True)['target'].count().unstack())
    print(computer_iv(data,'B','target',cut_bin,right=False))

参考资料:

1. https://www.cnblogs.com/wqbin/p/10549683.html

2. https://zhuanlan.zhihu.com/p/104757365

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值