数据处理实战: Chimerge和决策树分箱

本文是对《数据挖掘概念与技术》第三章的补充,详细展开分箱技术的细节

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'获取。有任何疑问可以在评论区提出,我们会及时答复。

关注公众号:数据臭皮匠;获得更多精彩内容

作者:范小匠

审核:灰灰匠

编辑:森匠

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349