1 BestKS分箱
从网上翻了很多BestKS内容,结果发现不同网站不同博主的BestKS代码几乎都是相同的。这里我的代码也参考了资料1(链接在文末)
BestKS分箱的基本原理如图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代码:
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))
参考资料:
本文介绍了一种名为BestKS的自动分箱方法,并通过实际案例展示了如何使用该方法进行变量分箱。同时,还提供了IV(信息值)的计算方法,用于评估分箱后的变量预测能力。

被折叠的 条评论
为什么被折叠?



