本文是对《数据挖掘概念与技术》第三章的补充,详细展开分箱技术的细节
1、Chimerge 分箱
Chimerge分箱虽然在书中只是寥寥几行,但却瞬间吸引了我的兴趣, 因为它的方式比较特别, 属于自下而上的分箱方式 首先将变量值排序, 初始化时每个值作为一组, 对相邻组做卡方检验,具有最小卡方值的组合并在一起(卡方值小,说明两组值的差别与目标变量不独立,可以参考小说和男女的关系),循环合并,直到满足预先设定的终止条件(满足分组数或卡方值大于某个阈值)。
Chimerge原理虽然很简单, 但实现起来还是有点麻烦的, 这里笔者整理了一份Chimerge分箱的代码
def cal_Chi2(df):
"""从列联表计算出卡方值"""
res = []
# 计算values的和
num_sum = sum(df.values.flatten())
for i in range(df.shape[0]):
for j in range(df.shape[1]):
# 计算位置i,j上的期望值
e = sum(df.iloc[i,:])*sum(df.iloc[:,j])/num_sum
tt = (df.iloc[i,j]-e)**2/e
res.append(tt)
return sum(res)
def line_merge(df,i,j):
"""将i,j行合并"""
df.iloc[i, 1] = df.iloc[i, 1] + df.iloc[j, 1]
df.iloc[i, 2] = df.iloc[i, 2] + df.iloc[j, 2]
df.iloc[i, 0] = df.iloc[j, 0]
df = pd.concat([df.iloc[:j,:],df.iloc[j+1:,:]])
return df
# 定义一个卡方分箱(可设置参数置信度水平与箱的个数)停止条件为大于置信水平且小于bin的数目
def ChiMerge(df, variable, flag, confidenceVal=3.841, bin=10):
'''
df:传入一个数据框仅包含一个需要卡方分箱的变量与正负样本标识(正样本为1,负样本为0)
variable:需要卡方分箱的变量名称(字符串)
flag:正负样本标识的名称(字符串)
confidenceVal:置信度水平(默认是不进行抽样95%)
bin:最多箱的数目
'''
#进行数据格式化录入
regroup = df.groupby([variable])[flag].agg(["size","sum"])
regroup.columns = ['total_num','positive_class']
regroup['negative_class'] = regroup['total_num'] - regroup['positive_class'] # 统计需分箱变量每个值负样本数
regroup = regroup.drop('total_num', axis=1).reset_index()
col_names = regroup.columns
print('已完成数据读入,正在计算数据初处理')
#处理连续没有正样本或负样本的区间,并进行区间的合并(以免卡方值计算报错)
i = 0
while (i <= regroup.shape[0] - 2):
# 如果正样本(1)列或负样本(2)列的数量求和等于0 (求和等于0,说明i和i+1行的值都等于0)
if sum(regroup.iloc[[i,i+1],[1,2]].sum()==0) >0 :
# 合并两个区间
regroup = line_merge(regroup,i,i+1)
i = i - 1
i = i + 1
# 对相邻两个区间进行卡方值计算
chi_ls = [] # 创建一个数组保存相邻两个区间的卡方值
for i in np.arange(regroup.shape[0] - 1):
chi = cal_Chi2(regroup.iloc[[i,i+1],[1,2]])
chi_ls.append(chi)
print('已完成数据初处理,正在进行卡方分箱核心操作')
#把卡方值最小的两个区间进行合并(卡方分箱核心)
while True:
if (len(chi_ls) <= (bin - 1) and min(chi_ls) >= confidenceVal):
break
min_ind = chi_ls.index(min(chi_ls)) # 找出卡方值最小的位置索引
# 合并两个区间
regroup = line_merge(regroup,min_ind,min_ind+1)
if (min_ind == regroup.shape[0] - 1): # 最小值是最后两个区间的时候
# 计算合并后当前区间与前一个区间的卡方值并替换
chi_ls[min_ind - 1] = cal_Chi2(regroup.iloc[[min_ind,min_ind-1],[1,2]])
# 删除替换前的卡方值
del chi_ls[min_ind]
else:
# 计算合并后当前区间与前一个区间的卡方值并替换
chi_ls[min_ind - 1] = cal_Chi2(regroup.iloc[[min_ind,min_ind-1],[1,2]])
# 计算合并后当前区间与后一个区间的卡方值并替换
chi_ls[min_ind] = cal_Chi2(regroup.iloc[[min_ind,min_ind+1],[1,2]])
# 删除替换前的卡方值
del chi_ls[min_ind+ 1]
print('已完成卡方分箱核心操作,正在保存结果')
# 把结果保存成一个数据框
list_temp = []
for i in np.arange(regroup.shape[0]):
if i == 0:
x = '-inf'+'~'+ str(regroup.iloc[i,0])
elif i == regroup.shape[0] - 1:
x = str(regroup.iloc[i-1,0])+'+'
else:
x =str(regroup.iloc[i-1,0])+ '~'+str(regroup.iloc[i,0])
list_temp.append(x)
regroup[variable] = list_temp # 结果表第二列:区间
return regroup
import pandas as pd
import numpy as np
pd.set_option('display.max_columns',None)
import matplotlib.pyplot as plt
%matplotlib inline
df = pd.read_csv('BankChurners.csv')
df['target'] = df['Attrition_Flag'].map({"Existing Customer":0,"Attrited Customer":1})
df.drop('Attrition_Flag',axis=1,inplace=True)
df.rename(columns={"Customer_Age":"age"},inplace=True)
#调用函数参数示例
bins = ChiMerge(df, 'Credit_Limit','target', confidenceVal=3.841, bin=6)
bins
以上代码是对数值型变量的操作。 对于序数型变量, 需要先将根据变量顺序将变量值转换为0,1,2,3...的数值, 再根据以上代码操作。至于标称型属性, 没有明确的排序依据, 但是Chimerge方法有需要先排序, 所以有一个办法可以作为参考, 即每个离散值作为一组,根据每组中正样本的比例排序, 有了排序后, 将属性映射为0,1,2,3..., 后续的处理方式可以和数值型变量一致。
关于变量类型的介绍,可以参考另一篇文章 数据挖掘概念与技术-第2章 - 简书,关于卡方检验的详细探讨可以参考另一篇文章 卡方检验、相关系数、协方差和数据标准化 - 简书
2、决策树分箱
不同于Chimerge的自下而上, 决策树是自顶向下划分的, 但两者都是监督式分箱方法, 即都需要使用到标签变量。由于分箱时使用了类信息, 因此区间的边界更有可能定义在有帮助于提高分类准确率的地方。
from sklearn.tree import DecisionTreeClassifier
def decision_tree_bins(df:pd.DataFrame,x_name:str,y_name:str,max_leaf_num:int=6):
"""利用决策树获得最优分箱的边界值"""
boundary = []
x = df[x_name].values
y = df[y_name].values
clf = DecisionTreeClassifier(criterion='entropy', # 信息熵最小化准则划分
max_leaf_nodes=max_leaf_num, # 最大叶子节点数
min_samples_leaf = 0.05) # 叶子节点样本数量最小占比
clf.fit(x.reshape(-1,1),y) # 训练决策树
n_nodes = clf.tree_.node_count
children_left = clf.tree_.children_left
children_right = clf.tree_.children_right
threshold = clf.tree_.threshold
for i in range(n_nodes):
if children_left[i] != children_right[i] : # 获的决策时节点上的划分边界
boundary.append(threshold[i])
boundary.sort()
min_x = x.min()
max_x = x.max() + 0.1 # 加0.1是为了考虑后续groupby操作时, 能包含特征最大值得样本
boundary = boundary +[max_x]
# 根据得到的边界值, 得到分箱结果
df[x_name] = pd.cut(df[x_name],bins=boundary)
# 查看分箱结果
df = df.groupby(x_name,as_index=False)[y_name].agg(['size','sum'])
df = df.reset_index()
df.columns = [x_name,'num_all','positive_class']
df['negative_class'] = df['num_all'] - df['positive_class']
return df[[x_name,'positive_class','negative_class']]
import pandas as pd
import numpy as np
pd.set_option('display.max_columns',None)
import matplotlib.pyplot as plt
%matplotlib inline
df = pd.read_csv('BankChurners.csv')
df['target'] = df['Attrition_Flag'].map({"Existing Customer":0,"Attrited Customer":1})
df.drop('Attrition_Flag',axis=1,inplace=True)
decision_tree_bins(df.copy(),'Months_on_book','target',max_leaf_num=6)
3、等宽分箱
将属性的值域从最小值到最大值分成具有相同宽度的n个区间,等宽分箱比较简单, 一个函数np.linspace就可以很方便的得到分箱的边界, 再使用pd.cut() 就可以对数值进行方便的分箱转换了
import pandas as pd
import numpy as np
pd.set_option('display.max_columns',None)
import matplotlib.pyplot as plt
%matplotlib inline
df = pd.read_csv('BankChurners.csv')
df['target'] = df['Attrition_Flag'].map({"Existing Customer":0,"Attrited Customer":1})
df.drop('Attrition_Flag',axis=1,inplace=True)
np.linspace(df['Credit_Limit'].min(),df['Credit_Limit'].max(),10)
pd.cut(df['Credit_Limit'],bins=np.linspace(df['Credit_Limit'].min(),df['Credit_Limit'].max(),10))
4、等频分箱
等频法是将相同数量的记录放在每个区间,保证每个区间的数量基本一致, 等频分箱比较简单, 一个pd.qcut就解决问题了
import pandas as pd
import numpy as np
pd.set_option('display.max_columns',None)
import matplotlib.pyplot as plt
%matplotlib inline
df = pd.read_csv('BankChurners.csv')
df['target'] = df['Attrition_Flag'].map({"Existing Customer":0,"Attrited Customer":1})
df.drop('Attrition_Flag',axis=1,inplace=True)
df['qq'] = pd.qcut(df['Credit_Limit'],q=np.linspace(0,1,11))
df.groupby('qq')['qq'].size().plot(kind='bar',figsize=(18,10))
本文涉及到python代码、数据集以及思维导图可以在我们的公众号'数据臭皮匠'后台回复'第三章2'获取。有任何疑问可以在评论区提出,我们会及时答复。
关注公众号:数据臭皮匠;获得更多精彩内容
作者:范小匠
审核:灰灰匠
编辑:森匠