《Python金融大数据风控建模实战》 第6章 变量分箱方法

  • 本章引言
  • Python代码实现及注释


本章引言

变量分箱是一种特征工程方法,意在增强变量的可解释性与预测能力。变量分箱方法主要用于连续变量,对于变量取值较稀疏的离散变量也应该进行分箱处理。
变量分箱对模型的好处:

  • 降低异常值的影响,增强模型的稳定性
    数据中存在异常值会使模型产生一定的偏差,从而影响预测效果。通过分箱模型可以降低异常值的噪声特性,使模型更稳健。树模型对异常值不敏感,但Logistic回归模型和神经网络对异常值敏感。
  • 缺失值作为特殊变量参与分箱,减少缺失值填补的不确定性。
    缺失值造成的原因不可追溯,插补方法也不尽相同,但如果能将缺失值作为一种特征,则会免去主观填充带来的不确定性问题,以增加模型的稳定性。而分箱方法可以将缺失值作为特殊值参与分箱处理。通常的做法是,离散特征将缺失值转为字符串作为特殊字符即可,而连续特征将缺失值作为特殊值即可,这样缺失值将作为一个特征参与分箱。
  • 增加变量的可解释性
    分箱的方法往往要配合变量编码使用,这就大大提高了变量的可解释性。通常采用的编码方式为WOE编码。本章将介绍的分箱方法有Chi-megerd方法、Best-KS方法、IV最优分箱方法和基于树的最优分箱方法。
  • 增加变量的非线性
    由于分箱后会采用编码操作,常用的编码方式有WOE编码、哑变量编码和One-hot编码。对于WOE编码,编码的计算结果与目标变量相关,会产生非线性的结果,而采用哑变量编码或One-hot编码,会使编码后的变量比原始变量获得更多的权重,并且这些权重是不同的,因此增加了模型的非线性。
  • 增加模型的预测效果
    从统计学角度考虑,机器学习模型在训练时会将数据划分为训练集和测试集,通常假设训练集和测试集是服从同分布的,分箱操作使连续变量离散化,使得训练集和测试集更容易满足这种假设。因此,分箱会增加模型预测效果的稳定性,即会减少模型在训练集上的表现与测试集上的偏差。

使用分箱的局限如下:

  • 同一箱内的样本具有同质性
    分箱的基本假设是分在一个箱内样本具有相同的风险等级。对于树模型就减少了模型选择最优切分点的可选择范围,会对模型的预测能力产生影响,损失了模型的分辨能力。
  • 需要专家经验支持
    一个变量怎样分箱对结果的影响是不同的,需要专家经验进行分箱指导,这往往非常耗时。本章介绍的均是自动分箱方法,它的好处是可以减少人工干预,但对专家的经验知识却没有过多体现。如果有成体系的变量分箱经验,可以在自动分箱时设置切分点,使其在候选集中即可在结合经验的基础上完成自动分箱。

变量分箱需要注意的问题:

  • 分箱结果不宜过多
    因为分箱后需要用编码的方式进行数值转化,转化的方式为WOE编码或One-hot编码。当采用WOE编码时,如果分箱过多会造成好样本或坏样本在每个箱内的分布不均,造成某个箱内几乎没有分布,使得样本较少的箱内其代表性不足。当采用One-hot编码时,由于分箱过多导致变量过于稀疏,编码后的变量维度快速增加,使变量更加稀疏,会降低模型的预测效果,后续章节会讨论稀疏特征下的变量组合,以增加模型的预测效果。
  • 分箱结果不易过少
    由于每个箱内的变量默认是同质的,即风险等级相同,如果分箱过少,则会造成模型的辨识度过低。
  • 分箱后单调性的要求
    分箱单调性是指分箱后的WOE值随着分箱索引的增加而呈现增加或减少的趋势。分箱单调是为了让Logistic回归模型可以得到更好的预测结果(线性特征更容易学习),但是往往有的变量自身就是U形结构,不太好通过分箱的方式让数据达到单调的效果(当然也可以通过分箱合并的方式达到近似单调的效果),这时候只是Logistic回归模型可能效果不佳,但是更复杂的算法是可以学习到这种规则的。

变量分箱主要是对连续变量进行离散化,然后通过编码转化为数值特征。此外,如果离散变量过于稀疏,可以先用坏样本比率转为数值,将其作为连续变量执行分箱操作。

Python代码实现及注释

# 第6章 变量分箱方法

'''
程序运行逻辑:数据读取->划分训练集与测试集->在训练集上得到分箱规则(连续变量与离散变量分开计算)->对训练集原始数据进行分箱映射
->测试集数据分箱映射
用到的函数:
    data_read:数据读取函数
    cont_var_bin:连续变量分箱
    cont_var_bin_map:连续变量分箱映射函数,将cont_var_bin函数分箱规则应用到原始连续数据上
    disc_var_bin:离散变量分箱
    disc_var_bin_map:离散变量分箱映射函数,将disc_var_bin函数分箱规则应用到原始离散数据上
1:Chi-merge(卡方分箱), 2:IV(最优IV值分箱), 3:信息熵(基于树的分箱)
'''

'''
os是Python环境下对文件,文件夹执行操作的一个模块
这里是采用的是scikit-learn的model_selection模块中的train_test_split()函数实现数据切分
'''
import os
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
import warnings
warnings.filterwarnings("ignore") ##忽略警告

def data_read(data_path,file_name):
    '''
    csv文件是一种用,和换行符区分数据记录和字段的一种文件结构,可以用excel表格编辑,也可以用记事本编辑,是一种类excel的数据存
    储文件,也可以看成是一种数据库。pandas提供了pd.read_csv()方法可以读取其中的数据并且转换成DataFrame数据帧。python的强大
    之处就在于他可以把不同的数据库类型,比如txt/csv/.xls/.sql转换成统一的DataFrame格式然后进行统一的处理。真是做到了标准化。
    pd.read_csv()函数参数:
        os.path.join()函数:连接两个或更多的路径名组件
        sep:如果不指定参数,则会尝试使用逗号分隔。
        delimiter :定界符,备选分隔符(如果指定该参数,则sep参数失效)
        delim_whitespace : 指定空格是否作为分隔符使用,等效于设定sep=’\s+’。如果这个参数设定为True那么delimiter 参数失效。
        header :指定行数用来作为列名,数据开始行数。如果文件中没有列名,则默认为0【第一行数据】,否则设置为None。
    '''
    df = pd.read_csv( os.path.join(data_path, file_name), delim_whitespace = True, header = None )

    # 变量重命名
    columns = ['status_account','duration','credit_history','purpose', 'amount',
               'svaing_account', 'present_emp', 'income_rate', 'personal_status',
               'other_debtors', 'residence_info', 'property', 'age',
               'inst_plans', 'housing', 'num_credits',
               'job', 'dependents', 'telephone', 'foreign_worker', 'target']

    '''
    修改列名的两种方式为:
        直接使用df.columns的方式重新命名,不过这种方式需要列出所有列名。
        使用rename方法,注意如果需要原地修改需要带上inplace=True的参数,否则原dataframe列名不会发生改变。
    '''
    df.columns = columns

    # 将标签变量由状态1,2转为0,1;0表示好用户,1表示坏用户
    df.target = df.target - 1

    '''
    数据分为data_train和 data_test两部分,训练集用于得到编码函数,验证集用已知的编码规则对验证集编码。
    这里是采用的是scikit-learn的model_selection模块中的train_test_split()函数实现数据切分,函数原型为:
        sklearn.model_selection.train_test_split(*arrays, **options)
    主要参数说明:
        arrays:为需要切分的原始数据,可以是列表、Numpy arrays、稀疏矩阵、pandas的数据框。
        test_size:划分的测试数据的占比,为0-1的数,默认为0.25,即训练数据为原始数据的75%,测试数据为原始数据的25%。
        train_size:与test_size设置一个参数即可,并满足加和为1的关系。
        random_state:随机数设置,可以保证每次切分得到的数据是相同的,这样在比较不用算法的性能时更加严谨,保证了数据集的一致性。
                     如果不设置,每次将随机选择随机数,产生不同的切分结果。  
        shuffle:是否在切分前打乱数据原有的顺序,默认为进行随机洗牌。
        stratify:设置是否采用分层抽样,默认为none,不分层。分层抽样可以保证正负样本的比例与原始的数据集一致。如果设置为none,
                  则切分时采用随机采样方式。如果需要进行分层采样,则需要指定按哪个变量分层,一般按照标签进行采样。  
                  如在本程序中,使用target标签进行采样。
    '''
    data_train, data_test = train_test_split(df, test_size=0.2, random_state=0,stratify=df.target)

    return data_train, data_test

def cal_advantage(temp, piont, method,flag='sel'):
    '''
    计算当前切分点下的指标值
    参数:
        temp: 上一步的分箱结果,pandas dataframe
        piont: 切分点,以此来划分分箱
        method: 分箱方法选择,1:chi-merge , 2:IV值, 3:信息熵
    '''

#    temp = binDS
    if flag == 'sel':
        # 用于最优切分点选择,这里只是二叉树,即二分
        bin_num = 2

        '''
        numpy.empty(shape, dtype=float, order=‘C’)
        根据给定的维度和数值类型返回一个新的数组,其元素不进行初始化。
        参数:shape:整数或者整数组成的元组	
                     功能:空数组的维度,例如:(2, 3)或者2
              dtype:数值类型,可选参数	
                     功能:指定输出数组的数值类型,例如numpy.int8。默认为numpy.float64。
              order:{‘C’, ‘F’},可选参数
              	     功能:是否在内存中以C或fortran(行或列)顺序存储多维数据
        下面这行代码返回行为bin_num,列为3的矩阵
        '''
        good_bad_matrix = np.empty((bin_num, 3))

        for ii in range(bin_num):
            if ii==0:

                '''
                temp: 上一步的分箱结果,pandas dataframe
                ii=0时,df_temp_1是temp中'bin_raw'<= point的结果
                ii=1时,df_temp_1是temp中'bin_raw'>point的结果
                '''
                df_temp_1 = temp[temp['bin_raw'] <= piont]
            else:
                df_temp_1 = temp[temp['bin_raw'] > piont]

            '''
            计算每个箱内的好坏样本书
            good_bad_matrix[0][0] = df_temp_1['good'].sum()
            good_bad_matrix[0][1] = df_temp_1['bad'].sum()
            good_bad_matrix[0][2] = df_temp_1['total'].sum()
            good_bad_matrix[1][0] = df_temp_1['good'].sum()
            good_bad_matrix[1][1] = df_temp_1['bad'].sum()
            good_bad_matrix[1][2] = df_temp_1['total'].sum()
            '''
            good_bad_matrix[ii][0] = df_temp_1['good'].sum()
            good_bad_matrix[ii][1] = df_temp_1['bad'].sum()
            good_bad_matrix[ii][2] = df_temp_1['total'].sum()

    elif flag == 'gain':
       '''
       用于计算本次分箱后的指标结果,即分箱数,每增加一个,就要算一下当前分箱下的指标结果
       bin_num的取值为temp['bin'].max()
       '''
       bin_num = temp['bin'].max()
       good_bad_matrix = np.empty((bin_num, 3))
       for ii in range(bin_num):

           '''
           df_temp_1 = temp[temp['bin'] == 1]
           df_temp_1 = temp[temp['bin'] == 2]
           ......
           df_temp_1 = temp[temp['bin'] == (ii +1)]
           '''
           df_temp_1 = temp[temp['bin'] == (ii + 1)]
           good_bad_matrix[ii][0] = df_temp_1['good'].sum()
           good_bad_matrix[ii][1] = df_temp_1['bad'].sum()
           good_bad_matrix[ii][2] = df_temp_1['total'].sum()
       
    # 计算总样本中的好坏样本
    total_matrix = np.empty(3)
    total_matrix[0] = temp.good.sum()
    total_matrix[1] = temp.bad.sum()
    total_matrix[2] = temp.total.sum()
    
    # method ==1,表示Chi-merger分箱
    if method == 1:
        X2 = 0

        # i=0,1
        for i in range(bin_num):
            # j=0,1
            for j in range(2):
                '''
                expect = (total_matrix[0]/ total_matrix[2])*good_bad_matrix[0][2]
                expect = (total_matrix[1]/ total_matrix[2])*good_bad_matrix[0][2]
                expect = (total_matrix[0]/ total_matrix[2])*good_bad_matrix[1][2]
                expect = (total_matrix[1]/ toral_matrix[2])*good_bad_matrix[1][2]
                '''
                expect = (total_matrix[j] / total_matrix[2])*good_bad_matrix[i][2]
                X2 = X2 + (good_bad_matrix[i][j] - expect )**2/expect
        M_value = X2

    # IV分箱
    elif method == 2:
        '''
        total_matrix[0]表示总的好样本的个数,total_matrix[1]表示总的坏样本的个数
        '''
        if pd.isnull(total_matrix[0]) or  pd.isnull(total_matrix[1]) or total_matrix[0] == 0 or total_matrix[1] == 0:
            M_value = np.NaN
        else:
            IV = 0
            for i in range(bin_num):
                ##坏好比
                weight = good_bad_matrix[i][1] / total_matrix[1] - good_bad_matrix[i][0] / total_matrix[0]
                IV = IV + weight * np.log( (good_bad_matrix[i][1] * total_matrix[0]) / (good_bad_matrix[i][0] * total_matrix[1]))
            M_value = IV

    # 信息熵分箱
    elif method == 3:
        # 总的信息熵
        entropy_total = 0
        for j in range(2):

            '''
            total_matrix[0]表示总的好样本的个数,total_matrix[1]表示总的坏样本的个数,total_matrix[2]表示总的样本个数
            '''
            weight = (total_matrix[j]/ total_matrix[2])
            entropy_total = entropy_total - weight * (np.log(weight))
                    
        # 计算条件熵
        entropy_cond = 0
        for i in range(bin_num):
            entropy_temp = 0
            for j in range(2):
                entropy_temp = entropy_temp - ((good_bad_matrix[i][j] / good_bad_matrix[i][2])
                                         * np.log(good_bad_matrix[i][j] / good_bad_matrix[i][2]) )
            entropy_cond = entropy_cond + good_bad_matrix[i][2]/total_matrix[2] * entropy_temp 
        
        # 计算归一化信息增益
        M_value = 1 - (entropy_cond / entropy_total)

    # Best-Ks分箱
    else:
        pass
    return M_value

def best_split(df_temp0, method, bin_num):
    '''
    在每个候选集中寻找切分点,完成一次分裂。
    select_split_point函数的中间过程函数
    参数:
        df_temp0: 上一次分箱后的结果,pandas dataframe
        method: 分箱方法选择,1:chi-merge , 2:IV值, 3:信息熵
        bin_num: 分箱编号,在不同编号的分箱结果中继续二分
    返回值:
        返回在本次分箱标号内的最优切分结果, pandas dataframe
    '''

#    df_temp0 = df_temp
#    bin_num = 1

    '''
    DataFrame.sort_values(by, axis=0, ascending=True, inplace=False, kind='quicksort', na_position='last')  
    参数:
        by:str or list of str;
        axis:{0 or ‘index’, 1 or ‘columns’}, default 0,默认按照索引排序,即纵向排序,如果为1,则是横向排序    
        ascending:布尔型,True则升序,可以是[True,False],即第一字段升序,第二个降序  
        inplace:布尔型,是否用排序后的数据框替换现有的数据框  
        kind:排序方法,{‘quicksort’, ‘mergesort’, ‘heapsort’}, default ‘quicksort’。似乎不用太关心  
        na_position : {‘first’, ‘last’}, default ‘last’,默认缺失值排在最后面  
    '''
    df_temp0 = df_temp0.sort_values(by=['bin', 'bad_rate'])
    piont_len = len(df_temp0[df_temp0['bin'] == bin_num])  # 候选集的长度
    bestValue = 0
    bestI = 1

    # 以候选集的每个切分点做分隔,计算指标值
    for i in range(1, piont_len):

        # 计算指标值
        value = cal_advantage(df_temp0,i,method,flag='sel')
        if bestValue < value:
            bestValue = value
            bestI = i

    # create new var split
    '''
    1.np.where(condition, x, y)
        当where内有三个参数时,第一个参数表示条件,当条件成立时where方法返回x,当条件不成立时where返回y
    2.np.where(condition)
        当where内只有一个参数时,那个参数表示条件,当条件成立时,where返回的是每个符合condition条件元素的坐标, 返回的是以元组的形式
    3.多条件时condition, & 表示与, | 表示或。如a = np.where((0 < a) & (a < 5), x, y),当0 < a与a < 5满足时,返回x的值,当0 < a与a < 5
    不满足时,返回y的值。注意x, y必须和a保持相同尺寸。
    '''
    df_temp0['split'] = np.where(df_temp0['bin_raw'] <= bestI, 1, 0)

    '''
    DataFrame.drop(labels=None,axis=0, index=None, columns=None, inplace=False)
    参数说明:
        labels:就是要删除的行列的名字,用列表给定
        axis:默认为0,指删除行,因此删除columns时要指定axis=1;
        index:直接指定要删除的行
        columns:直接指定要删除的列
        inplace=False:默认该删除操作不改变原数据,而是返回一个执行删除操作后的新dataframe;
        inplace=True:则会直接在原数据上进行删除操作,删除后无法返回。
    '''
    df_temp0 = df_temp0.drop('bin_raw', axis=1)

    newbinDS = df_temp0.sort_values(by=['split', 'bad_rate'])

    # rebuild var i
    '''
    df_temp0['split'] = np.where(df_temp0['bin_raw'] <= bestI, 1, 0)
    newbinDS_0 为>bestI
    newbinDS_0 为<=bestI
    '''
    newbinDS_0 = newbinDS[newbinDS['split'] == 0]
    newbinDS_1 = newbinDS[newbinDS['split'] == 1]

    '''
    copy()与deepcopy()之间的区分必须要涉及到python对于数据的存储方式
    我们寻常意义的复制就是深复制,即将被复制对象完全再复制一遍作为独立的新个体单独存在。所以改变原有被复制对象不会对已经复制出
    来的新对象产生影响。 
    而浅复制并不会产生一个独立的对象单独存在,他只是将原有的数据块打上一个新标签,所以当其中一个标签被改变的时候,数据块就会发
    生变化,另一个标签也会随之改变。这就和我们寻常意义上的复制有所不同了。
    '''
    newbinDS_0 = newbinDS_0.copy()
    newbinDS_1 = newbinDS_1.copy()

    newbinDS_0['bin_raw'] = range(1, len(newbinDS_0) + 1)
    newbinDS_1['bin_raw'] = range(1, len(newbinDS_1) + 1)

    '''
    pd.concat(objs, axis=0, join='outer', join_axes=None, ignore_index=False,keys=None, levels=None, names=None,
              verify_integrity=False)
    参数:
        objs:用来保存需要用来进行连接的Series/DataFrame,可以是列表或者dict类型 
        axis:表示希望进行连接的轴向,默认为0,也就是纵向拼接 
        join:有多个选择,inner,outer,这里默认值是outer,下面会根据实例来比较下 
        join_axes:默认为空,可以设置值指定为其他轴上使用的索引 
        ignore_index:连接后原来两个DF的index值会被保存,如果该索引没有实际的意义可以设置为True来进行重分配index号
    '''
    newbinDS = pd.concat([newbinDS_0, newbinDS_1], axis=0)

    return newbinDS  

def select_split_point(temp_bin, method):
    '''
    二叉树分割方式,从候选者中挑选每次的最优切分点,与切分后的指标计算,cont_var_bin函数的中间过程函数
    参数:temp_bin:分箱后的结果 pandas dataframe
         method:分箱方法选择,1:chi-merge , 2:IV值, 3:信息熵
    返回值:新的分箱结果  pandas dataframe
    '''

#    temp_bin = df_temp_all
    '''
    DataFrame.sort_values(by, axis=0, ascending=True, inplace=False, kind='quicksort', na_position='last')  
    参数:
        by:str or list of str;
        axis:{0 or ‘index’, 1 or ‘columns’}, default 0,默认按照索引排序,即纵向排序,如果为1,则是横向排序    
        ascending:布尔型,True则升序,可以是[True,False],即第一字段升序,第二个降序  
        inplace:布尔型,是否用排序后的数据框替换现有的数据框  
        kind:排序方法,{‘quicksort’, ‘mergesort’, ‘heapsort’}, default ‘quicksort’。似乎不用太关心  
        na_position : {‘first’, ‘last’}, default ‘last’,默认缺失值排在最后面  
    '''
    temp_bin = temp_bin.sort_values(by=['bin', 'bad_rate'])

    # 得到最大的分箱值
    max_num = max(temp_bin['bin'])

#    temp_binC = dict()
#    m = dict()
#    # 不同箱内的数据取出来
#    for i in range(1, max_num + 1):
#        temp_binC[i] = temp_bin[temp_bin['bin'] == i]
#        m[i] = len(temp_binC[i])

    '''
    dict() 函数用于创建一个字典。返回一个字典。
    '''
    temp_main = dict()

    bin_i_value = []
    for i in range(1, max_num + 1):

        df_temp = temp_bin[temp_bin['bin'] == i]
        if df_temp.shape[0]>1 :

            # bin=i的做分裂
            temp_split= best_split(df_temp, method, i)

            # 完成一次分箱,更新bin的枝
            temp_split['bin'] = np.where(temp_split['split'] == 1,
                                               max_num + 1,
                                               temp_split['bin'])

            # 取出bin!=i合并为新组
            temp_main[i] = temp_bin[temp_bin['bin'] != i]
            temp_main[i] = pd.concat([temp_main[i], temp_split ], axis=0, sort=False)

            # 计算新分组的指标值
            value = cal_advantage(temp_main[i],0, method,flag='gain')
            newdata = [i, value]
            bin_i_value.append(newdata)

    # find maxinum of value bintoSplit
    bin_i_value.sort(key=lambda x: x[1], reverse=True)

    # binNum = temp_all_Vals['BinToSplit']
    binNum = bin_i_value[0][0]
    newBins = temp_main[binNum].drop('split', axis=1)
    return newBins.sort_values(by=['bin', 'bad_rate']), round( bin_i_value[0][1] ,4)


def init_equal_bin(x,bin_rate):

    '''
    初始化等距分组,cont_var_bin函数的中间过程函数
    参数:
        x:要分组的变量值,pandas series
        bin_rate:比例值1/bin_rate
    返回值:
        返回初始化分箱结果,pandas dataframe
    '''

    # 异常值剔除,只考虑95%的最大值与最小值,边界与-inf或inf分为一组
    '''
    分位数是统计中使用的度量,表示小于这个值的观察值占总数q的百分比。 函数numpy.percentile()接受以下参数。
    numpy.percentile(a, q, axis)
    参数:
        a:输入数组
        q:要计算的百分位数,在 0 ~ 100 之间
        axis:沿着它计算百分位数的轴   ,二维取值0,1
    如果x>np.percentile(x,95)的x个数,并且x的个数>=30,则var_up取其中x>np,percentile(x,95)最小值,否则直接取最大值
    如过x<np.percentile(x,5)的x个数>0,则var_low取其中x<np.percentile(x,5)最大值,苟泽直接取最小值
    '''
    if len(x[x > np.percentile(x, 95)]) > 0 and len(np.unique(x)) >=30:
        var_up= min( x[x > np.percentile(x, 95)] )
    else:
        var_up = max(x)

    if len(x[x < np.percentile(x, 5)]) > 0:
        var_low= max( x[x < np.percentile(x, 5)] )
    else:
        var_low = min(x)

    # 初始化分组
    bin_num = int(1/ bin_rate)
    dist_bin = (var_up - var_low) / bin_num  # 分箱间隔
    bin_up = []
    bin_low = []

    '''
    第一组和最后一组分开处理
    '''
    for i in range(1, bin_num + 1):
        if i == 1:
            bin_up.append( var_low + i * dist_bin)

            '''
            np.Inf:正无穷大的浮点表示,常用于数值比较当中的初始值
            '''
            bin_low.append(-np.inf)
        elif i == bin_num:
            bin_up.append( np.inf)
            bin_low.append( var_low + (i - 1) * dist_bin )
        else:
            bin_up.append( var_low + i * dist_bin )
            bin_low.append( var_low + (i - 1) * dist_bin )
    result = pd.DataFrame({'bin_up':bin_up,'bin_low':bin_low})
    result.index.name = 'bin_num'
    return result

def limit_min_sample(temp_cont,  bin_min_num_0):
    '''
    分箱约束条件:每个箱内的样本数不能小于bin_min_num_0,cont_var_bin函数的中间过程函数
    参数:
        temp_cont: 初始化分箱后的结果 pandas dataframe
        bin_min_num_0:每组内的最小样本限制
    返回值:
        合并后的分箱结果,pandas dataframe
    '''
    for i in temp_cont.index:

        '''
        行数据=temp_cont.loc[i, :]
        '''
        rowdata = temp_cont.loc[i, :]
        if i == temp_cont.index.max():

            # 如果是最后一个箱就,取倒数第二个值
            ix = temp_cont[temp_cont.index < i].index.max()
        else:

            # 否则就取大于i的最小的分箱值
            ix = temp_cont[temp_cont.index > i].index.min()

        # 如果0, 1, total项中样本的数量小于20则进行合并
        if rowdata['total'] <= bin_min_num_0:

            # 与相邻的bin合并
            temp_cont.loc[ix, 'bad'] = temp_cont.loc[ix, 'bad'] + rowdata['bad']
            temp_cont.loc[ix, 'good'] = temp_cont.loc[ix, 'good'] + rowdata['good']
            temp_cont.loc[ix, 'total'] = temp_cont.loc[ix, 'total'] + rowdata['total']
            if i < temp_cont.index.max():
                temp_cont.loc[ix, 'bin_low'] = rowdata['bin_low']
            else:
                temp_cont.loc[ix, 'bin_up'] = rowdata['bin_up']
            temp_cont = temp_cont.drop(i, axis=0)  
    return temp_cont.sort_values(by='bad_rate')

def cont_var_bin_map(x, bin_init):
    '''
    按照初始化分箱结果,对原始值进行分箱映射,用于训练集与测试集的分箱映射
    '''

    temp = x.copy()
    for i in bin_init.index:
        bin_up = bin_init['bin_up'][i]
        bin_low = bin_init['bin_low'][i]

        # 寻找出 >lower and <= upper的位置
        if pd.isnull(bin_up) or pd.isnull(bin_up):
            temp[pd.isnull(temp)] = i
        else:
            index = (x > bin_low) & (x <= bin_up)
            temp[index] = i
    temp.name = temp.name + "_BIN"
    return temp

def merge_bin(sub, i):

    '''
    将相同箱内的样本书合并,区间合并
    参数:
        sub:分箱结果子集,pandas dataframe ,如bin=1的结果
        i: 分箱标号
    返回值:
        返回合并结果
    '''

    l = len(sub)
    total = sub['total'].sum()

    '''
    loc——通过行标签索引行数据 
    iloc——通过行号索引行数据 
    ix——通过行标签或者行号索引行数据(基于loc和iloc 的混合) 
    '''
    first = sub.iloc[0, :]
    last = sub.iloc[l - 1, :]

    lower = first['bin_low']
    upper = last['bin_up']
    df = pd.DataFrame()
    df = df.append([i, lower, upper, total], ignore_index=True).T
    df.columns = ['bin', 'bin_low', 'bin_up', 'total']
    return df


def cont_var_bin(x, y, method, mmin=5, mmax=10, bin_rate=0.01, stop_limit=0.1, bin_min_num=20):
    '''
    连续变量分箱函数原型如下:
        cont_var_bin(x,y,method,mmin=5,mmax=10,bin_rate=0.01,stop_limit=0.1,bin_min_num=20)
    参数:
        x:输入分箱数据,pandas series
          (待分箱变量)
        y:标签变量
          (目标向量)
        method:分箱方法选择,1:chi-merge , 2:IV值, 3:基尼系数分箱
              (指定分箱方法,1表示采用最优Chi-merge分箱;2表示采用最用IV分箱;3表示采用信息增益分箱。另外,指标可以自行扩展,
               那么反应变量区分能力的指标可以用于分箱,如基尼指数等,这里没有对Best-KS方法做具体实现,因为Best-KS方法只能处理
               连续变量分箱)
        mmin:最小分箱数,当分箱初始化后如果初始化箱数小于等于mmin,则mmin=2,即最少分2箱,
            如果分两箱也无法满足箱内最小样本数限制而分1箱,则变量删除
            (最小分箱数。分箱初始化合并后要满足最小样本限制,如果不满足则需要进行分箱初始化合并。如果初始化合并后箱数小于等于
            mmin,则mmin=2,即最少分2箱,如果分2箱也无法满足箱内最小样本数限制而分为一箱,则删除该变量)
        mmax:最大分箱数,当分箱初始化后如果初始化箱数小于等于mmax,则mmax等于初始化箱数-1
             (最大分箱数。当分箱初始化合并后,如果从初始化箱数小于等于mmax,则mmax等于初始化合并箱数-1.如果数据中有缺失值,缺失值
             单独作为一箱,最大分箱数为mmax+1,即mmax限制的是非缺失值情况下的最大分箱数)
        bin_rate:等距初始化分箱参数,分箱数为1/bin_rate,分箱间隔在数据中的最小值与最大值将等间隔取值
                (等距初始化分箱参数,分箱数为1/bin_rate,分箱间隔在数据中的最小值与最大值的范围内等间隔取值。注意变量异常值的限制
                ,否则等间隔分箱后将有大部分箱内没有样本,而在某个箱内样本较集中,会降低分箱的辨识能力)
        stop_limit:分箱earlystopping机制,如果已经没有明显增益即停止分箱
                  (分箱前后的最小增益限值,即early stopping策略的限制。当本次分箱前后得到的增益小于限值则分箱终止)
        bin_min_num:每组最小样本数
                  (最小样本数,分箱初始化后每个箱内的最小样本数不能少于该值,否则进行分箱合并)
    返回值
        分箱结果:pandas dataframe
    '''

    # 缺失值单独取出来
    df_na = pd.DataFrame({'x': x[pd.isnull(x)], 'y': y[pd.isnull(x)]})
    y = y[~pd.isnull(x)]
    x = x[~pd.isnull(x)]

    # 初始化分箱,等距的方式,后面加上约束条件,没有箱内样本数没有限制
    bin_init = init_equal_bin(x, bin_rate)

    # 分箱映射
    bin_map = cont_var_bin_map(x, bin_init)
    
    df_temp = pd.concat([x, y, bin_map], axis=1)

    # 计算每个bin中好坏样本的频数
    '''
    pd.crosstab():用于计算分组的频率,算是一种特殊的pivot_table(),是顶级类函数
    pandas.crosstab(index, columns, values=None, rownames=None, colnames=None, aggfunc=None, margins=False, 
                    margins_name='All', dropna=True, normalize=False)
    参数:
        index:行分组键
        columns:列分组键
        margins:False 默认值,增加一行/列 ‘总计’  
        margins_name:All 默认值
        normalize:False 默认值
                   'index' or 1,normalize 每行
                   'columns' or 0,normalize 每列
                   'All' or 'True',normalize 全部
                   如果 margins = True,则 margins 也会被 normalize
                   值 = 每个数据 / 数据总和 浮点数格式
        values:可选项
                根据 index 和 columns 的分组后,计算 values 项的值,计算规则由 aggfunc 决定
                (values 和 aggfunc 成对出现)
        aggfunc:可选项
                np.sum,np.mean,len,... ...
                (values 和 aggfunc 成对出现)
        rownames:None 默认值
                 pd.crosstab()操作数组时,设定 row's name,而不使用默认名称
        colnames:None 默认值
                 pd.crosstab()操作数组时,设定 column's name,而不使用默认名称
        dropna:True 默认值
               如果某列的数据全是 NaN,则被删除
    '''
    df_temp_1 = pd.crosstab(index=df_temp[bin_map.name], columns=y)

    '''
    zip(*iterables):创建一个聚合了来自每个可迭代对象中的元素的迭代器。
    '''
    df_temp_1.rename(columns= dict(zip([0,1], ['good', 'bad'])) , inplace=True)

    # 计算每个bin中一共有多少样本
    '''
    groupby()是pandas库中DataFrame结构的函数
    '''
    df_temp_2 = pd.DataFrame(df_temp.groupby(bin_map.name).count().iloc[:, 0])
    df_temp_2.columns = ['total']

    '''
    pd.merge(left, right, how='inner', on=None, left_on=None, right_on=None,
         left_index=False, right_index=False, sort=True,
         suffixes=('_x', '_y'), copy=True, indicator=False,
         validate=None)
    参数:
        left: 拼接的左侧DataFrame对象
        right: 拼接的右侧DataFrame对象
        on: 要加入的列或索引级别名称。 必须在左侧和右侧DataFrame对象中找到。 如果未传递且left_index和right_index为False,
            则DataFrame中的列的交集将被推断为连接键。
        left_on:左侧DataFrame中的列或索引级别用作键。 可以是列名,索引级名称,也可以是长度等于DataFrame长度的数组。
        right_on: 左侧DataFrame中的列或索引级别用作键。 可以是列名,索引级名称,也可以是长度等于DataFrame长度的数组。
        left_index: 如果为True,则使用左侧DataFrame中的索引(行标签)作为其连接键。 对于具有MultiIndex(分层)的DataFrame,
                    级别数必须与右侧DataFrame中的连接键数相匹配。
        right_index: 与left_index功能相似。
        how: One of ‘left’, ‘right’, ‘outer’, ‘inner’. 默认inner。inner是取交集,outer取并集。比如left:[‘A’,‘B’,‘C’];
             right[’'A,‘C’,‘D’];inner取交集的话,left中出现的A会和right中出现的买一个A进行匹配拼接,如果没有是B,在right中没有匹配到,则会丢失。'outer’取并集,出现的A会进行一一匹配,没有同时出现的会将缺失的部分添加缺失值。
        sort: 按字典顺序通过连接键对结果DataFrame进行排序。 默认为True,设置为False将在很多情况下显着提高性能。
        suffixes: 用于重叠列的字符串后缀元组。 默认为(‘x’,’ y’)。
        copy: 始终从传递的DataFrame对象复制数据(默认为True),即使不需要重建索引也是如此。
        indicator:将一列添加到名为_merge的输出DataFrame,其中包含有关每行源的信息。 _merge是分类类型,并且对于其合并键仅出现
                  在“左”DataFrame中的观察值,取得值为left_only,对于其合并键仅出现在“右”DataFrame中的观察值为right_only,
                  并且如果在两者中都找到观察点的合并键,则为left_only
    '''
    df_temp_all= pd.merge(pd.concat([df_temp_1, df_temp_2], axis=1), bin_init,
                         left_index=True, right_index=True,
                         how='left')
    
    # 做分箱上下限的整理,让候选点连续
    for j in range(df_temp_all.shape[0]-1):
        if df_temp_all.bin_low.loc[df_temp_all.index[j+1]] !=  df_temp_all.bin_up.loc[df_temp_all.index[j]]:
            df_temp_all.bin_low.loc[df_temp_all.index[j+1]] = df_temp_all.bin_up.loc[df_temp_all.index[j]]

    # 离散变量中这个值为bad_rate,连续变量时为索引,索引值是分箱初始化时,箱内有变量的箱的索引
    df_temp_all['bad_rate'] = df_temp_all.index

    # 最小样本数限制,进行分箱合并
    df_temp_all = limit_min_sample(df_temp_all, bin_min_num)

    # 将合并后的最大箱数与设定的箱数进行比较,这个应该是分箱数的最大值
    if mmax >= df_temp_all.shape[0]:
        mmax = df_temp_all.shape[0]-1
    if mmin >= df_temp_all.shape[0]:
        gain_value_save0=0
        gain_rate_save0=0

        '''
        np.linspace主要用来创建等差数列。
        numpy.linspace(start, stop, num=50, endpoint=True, retstep=False, dtype=None, axis=0)
        参数:
            start:返回样本数据开始点
            stop:返回样本数据结束点
            num:生成的样本数据量,默认为50
            endpoint:True则包含stop;False则不包含stop
            retstep:If True, return (samples, step), where step is the spacing between samples.
                     (即如果为True则结果会给出数据间隔)
            dtype:输出数组类型
            axis:0(默认)或-1
        '''
        df_temp_all['bin'] = np.linspace(1,df_temp_all.shape[0],df_temp_all.shape[0],dtype=int)

        data = df_temp_all[['bin_low','bin_up','total','bin']]
        data.index = data['bin']
    else:
        df_temp_all['bin'] = 1
        df_temp_all['bin_raw'] = range(1, len(df_temp_all) + 1)
        df_temp_all['var'] = df_temp_all.index  # 初始化箱的编号
        gain_1 = 1e-10
        gain_rate_save0 = []
        gain_value_save0 = []

        # 分箱约束:最大分箱数限制
        for i in range(1,mmax):

    #       i = 1
            df_temp_all, gain_2 = select_split_point(df_temp_all, method=method)
            gain_rate = gain_2 / gain_1 - 1  #  ratio gain
            gain_value_save0.append(np.round(gain_2,4))
            if i == 1:
                gain_rate_save0.append(0.5)
            else:
                gain_rate_save0.append(np.round(gain_rate,4))
            gain_1 = gain_2
            if df_temp_all.bin.max() >= mmin and df_temp_all.bin.max() <= mmax:
                if gain_rate <= stop_limit or pd.isnull(gain_rate):
                    break
                
    
        df_temp_all = df_temp_all.rename(columns={'var': 'oldbin'})
        temp_Map1 = df_temp_all.drop(['good', 'bad', 'bad_rate', 'bin_raw'], axis=1)
        temp_Map1 = temp_Map1.sort_values(by=['bin', 'oldbin'])

        # get new lower, upper, bin, total for sub
        data = pd.DataFrame()
        for i in temp_Map1['bin'].unique():

            # 得到这个箱内的上下界
            sub_Map = temp_Map1[temp_Map1['bin'] == i]
            rowdata = merge_bin(sub_Map, i)
            data = data.append(rowdata, ignore_index=True)
    
        # resort data
        data = data.sort_values(by='bin_low')
        data = data.drop('bin', axis=1)
        mmax = df_temp_all.bin.max()
        data['bin'] = range(1, mmax + 1)
        data.index = data['bin']

    # 将缺失值的箱加过来
    if len(df_na) > 0:
        row_num = data.shape[0] + 1
        data.loc[row_num, 'bin_low'] = np.nan
        data.loc[row_num, 'bin_up'] = np.nan
        data.loc[row_num, 'total'] = df_na.shape[0]
        data.loc[row_num, 'bin'] = data.bin.max() + 1
    return data , gain_value_save0 ,gain_rate_save0


def cal_bin_value(x, y, bin_min_num_0=10):
    '''
    按变量类别进行分箱初始化,不满足最小样本数的箱进行合并
    参数:
        x: 待分箱的离散变量 pandas Series
        y: 标签变量
        target: 正样本标识
        bin_min_num_0:箱内的最小样本数限制
    返回值:
        计算结果
    '''

    # 按类别x计算yz中0,1两种状态的样本数
    df_temp = pd.crosstab(index=x, columns=y, margins=False)
    df_temp.rename(columns= dict(zip([0,1], ['good', 'bad'])) , inplace=True)

    '''
    assign的用途是增加新的一列
    '''
    df_temp = df_temp.assign(total=lambda x:x['good']+ x['bad'],bin=1,var_name=df_temp.index)\
              .assign(bad_rate=lambda x:x['bad']/ x['total'])

    # 按照baterate排序
    df_temp = df_temp.sort_values(by='bad_rate')
    df_temp = df_temp.reset_index(drop=True)

    # 样本数不满足最小值进行合并
    for i in df_temp.index:
        rowdata = df_temp.loc[i, :]
        if i == df_temp.index.max():

            # 如果是最后一个箱就,取倒数第二个值
            ix = df_temp[df_temp.index < i].index.max()
        else:

            # 否则就取大于i的最小的分箱值
            ix = df_temp[df_temp.index > i].index.min()

        # 如果0, 1, total项中样本的数量小于20则进行合并
        if any(rowdata[:3] <= bin_min_num_0):

            # 与相邻的bin合并
            df_temp.loc[ix, 'bad'] = df_temp.loc[ix, 'bad'] + rowdata['bad']
            df_temp.loc[ix, 'good'] = df_temp.loc[ix, 'good'] + rowdata['good']
            df_temp.loc[ix, 'total'] = df_temp.loc[ix, 'total'] + rowdata['total']
            df_temp.loc[ix, 'bad_rate'] = df_temp.loc[ix,'bad'] / df_temp.loc[ix, 'total']

            # 将区间也进行合并
            df_temp.loc[ix, 'var_name'] = str(rowdata['var_name']) +'%'+ str(df_temp.loc[ix, 'var_name'])
         
            df_temp = df_temp.drop(i, axis=0)  # 删除原来的bin

    # 如果离散变量小于等于5,每个变量为一个箱
    df_temp['bin_raw'] = range(1, df_temp.shape[0] + 1)
    df_temp = df_temp.reset_index(drop=True)
    return df_temp


def disc_var_bin(x, y, method=1, mmin=3, mmax=8, stop_limit=0.1, bin_min_num = 20  ):
    '''
    离散变量分箱方法,如果变量过于稀疏最好先编码在按连续变量分箱
    参数:
        x:输入分箱数据,pandas series
        y:标签变量
        method:分箱方法选择,1:chi-merge , 2:IV值, 3:信息熵
        mmin:最小分箱数,当分箱初始化后如果初始化箱数小于等mmin,则mmin=2,即最少分2箱,
             如果分两厢也无法满足箱内最小样本数限制而分1箱,则变量删除
        mmax:最大分箱数,当分箱初始化后如果初始化箱数小于等于mmax,则mmax等于初始化箱数-1
        stop_limit:分箱earlystopping机制,如果已经没有明显增益即停止分箱
        bin_min_num:每组最小样本数
    返回值:分箱结果:pandas dataframe
    '''

#    x = data_train.purpose
#    y = data_train.target
    del_key = []

    # 缺失值单独取出来
    df_na = pd.DataFrame({'x': x[pd.isnull(x)], 'y': y[pd.isnull(x)]})
    y = y[~pd.isnull(x)]
    x = x[~pd.isnull(x)]

    # 数据类型转化
    '''
    np.issubdtype
    可以判断某一个dtype是否是某一超类的子类,也可以用dtype的mro方法查看其所有的父类
    '''
    if np.issubdtype(x.dtype, np.int_):

        '''
        ndim返回的是数组的维度,返回的只有一个数,该数即表示数组的维度。
        shape:表示各位维度大小的元组。返回的是一个元组。
        dtype:一个用于说明数组数据类型的对象。返回的是该数组的数据类型。
        astype:转换数组的数据类型
        '''
        x = x.astype('float').astype('str')

    if np.issubdtype(x.dtype, np.float_):
        x = x.astype('str')
  
    # 按照类别分箱,得到每个箱下的统计值
    temp_cont = cal_bin_value(x, y,bin_min_num)
    
    # 如果去掉缺失值后离散变量的可能取值小于等于5不分箱
    if len(x.unique()) > 5:

        #将合并后的最大箱数与设定的箱数进行比较,这个应该是分箱数的最大值
        if mmax >= temp_cont.shape[0]:
            mmax = temp_cont.shape[0]-1
        if mmin >= temp_cont.shape[0]:
            mmin = 2
            mmax = temp_cont.shape[0]-1
        if mmax ==1:
            print('变量 {0}合并后分箱数为1,该变量删除'.format(x.name))
            del_key.append(x.name)
        
        gain_1 = 1e-10
        gain_value_save0 = []
        gain_rate_save0 = []
        for i in range(1,mmax):
            temp_cont, gain_2 = select_split_point(temp_cont, method=method)
            gain_rate = gain_2 / gain_1 - 1  #  ratio gain
            gain_value_save0.append(np.round(gain_2,4))
            if i == 1:
                gain_rate_save0.append(0.5)
            else:
                gain_rate_save0.append(np.round(gain_rate,4))
            gain_1 = gain_2
            if temp_cont.bin.max() >= mmin and temp_cont.bin.max() <= mmax:
                if gain_rate <= stop_limit:
                    break
    
        temp_cont = temp_cont.rename(columns={'var': x.name})
        temp_cont = temp_cont.drop(['good', 'bad', 'bin_raw', 'bad_rate'], axis=1)
    else:
        temp_cont.bin = temp_cont.bin_raw
        temp_cont = temp_cont[['total', 'bin', 'var_name']]
        gain_value_save0=[]
        gain_rate_save0=[]
        del_key=[]

    # 将缺失值的箱加过来
    if len(df_na) > 0:
        index_1 = temp_cont.shape[0] + 1
        temp_cont.loc[index_1, 'total'] = df_na.shape[0]
        temp_cont.loc[index_1, 'bin'] = temp_cont.bin.max() + 1
        temp_cont.loc[index_1, 'var_name'] = 'NA'
    temp_cont = temp_cont.reset_index(drop=True)  
    if temp_cont.shape[0]==1:
        del_key.append(x.name)
    return temp_cont.sort_values(by='bin') , gain_value_save0 , gain_rate_save0,del_key


def disc_var_bin_map(x, bin_map):
    '''
    用离散变量分箱后的结果,对原始值进行分箱映射
    参数:
        x: 待分箱映射的离散变量,pandas Series
        bin_map:分箱映射字典, pandas dataframe
    返回值:
        返回映射结果
    '''

    # 数据类型转化
    xx = x[~pd.isnull(x)]
    if np.issubdtype(xx.dtype, np.int_):
        x[~pd.isnull(x)] = xx.astype('float').astype('str')
    if np.issubdtype(xx.dtype, np.float_):
        x[~pd.isnull(x)] = xx.astype('str') 
    d = dict()
    for i in bin_map.index:
        for j in  bin_map.loc[i,'var_name'].split('%'):
            if j != 'NA':
                d[j] = bin_map.loc[i,'bin']

    new_x = x.map(d)

    # 有缺失值要做映射
    if sum(pd.isnull(new_x)) > 0:
        index_1 = bin_map.index[bin_map.var_name == 'NA']
        if len(index_1) > 0:

            '''
            tolist()作用:将矩阵(matrix)和数组(array)转化为列表。
            '''
            new_x[pd.isnull(new_x)] = bin_map.loc[index_1,'bin'].tolist()
    new_x.name = x.name + '_BIN'

    return new_x

if __name__ == '__main__':
    
    path = 'D:/code/chapter6/'
    data_path = os.path.join(path,'data')
    file_name = 'german.csv'

    # 读取数据
    data_train, data_test = data_read(data_path,file_name)

    # 连续变量分箱
    data_train.amount[1:30] = np.nan
    data_test1,gain_value_save1 ,gain_rate_save1  = cont_var_bin(data_train.amount, data_train.target, 
                             method=1, mmin=4 ,mmax=10,bin_rate=0.01,stop_limit=0.1 ,bin_min_num=20 )
    
    data_test2,gain_value_save2 ,gain_rate_save2  = cont_var_bin(data_train.amount, data_train.target,
                             method=2, mmin=4 ,mmax=10,bin_rate=0.01,stop_limit=0.1 ,bin_min_num=20 )

    data_test3,gain_value_save3 ,gain_rate_save3 = cont_var_bin(data_train.amount, data_train.target, 
                             method=3, mmin=4 ,mmax=10,bin_rate=0.01,stop_limit=0.1 ,bin_min_num=20 )
    
   
    # 区分离散变量和连续变量批量进行分箱,把每个变量分箱的结果保存在字典中
    dict_cont_bin = {}
    cont_name = ['duration', 'amount', 'income_rate',  'residence_info',  
               'age',  'num_credits','dependents']
    for i in cont_name:
        dict_cont_bin[i],gain_value_save , gain_rate_save = cont_var_bin(data_train[i], data_train.target, method=1, mmin=4, mmax=10,
                                     bin_rate=0.01, stop_limit=0.1, bin_min_num=20)

    # 离散变量分箱
    data_train.purpose[1:30] = np.nan
    data_disc_test1,gain_value_save1 ,gain_rate_save1,del_key  = disc_var_bin(data_train.purpose, data_train.target, 
                             method=1, mmin=4 ,mmax=10,stop_limit=0.1 ,bin_min_num=10 )
    
    data_disc_test2,gain_value_save2 ,gain_rate_save2 ,del_key = disc_var_bin(data_train.purpose, data_train.target,
                             method=2, mmin=4 ,mmax=10,stop_limit=0.1 ,bin_min_num=10 )

    data_disc_test3,gain_value_save3 ,gain_rate_save3,del_key = disc_var_bin(data_train.purpose, data_train.target, 
                             method=3, mmin=4 ,mmax=10,stop_limit=0.1 ,bin_min_num=10 )
    
    dict_disc_bin = {}
    del_key = []
    disc_name = [x for x in data_train.columns if x not in cont_name]
    disc_name.remove('target')
    for i in disc_name:
        dict_disc_bin[i],gain_value_save , gain_rate_save,del_key_1  = disc_var_bin(data_train[i], data_train.target, method=1, mmin=3,
                                     mmax=8, stop_limit=0.1, bin_min_num=5)
        if len(del_key_1)>0 :
            del_key.extend(del_key_1)

    # 删除分箱数只有1个的变量
    if len(del_key) > 0:
        for j in del_key:
            del dict_disc_bin[j]

    # 训练数据分箱
    # 连续变量分箱映射
#    ss = data_train[list( dict_cont_bin.keys())]
    df_cont_bin_train = pd.DataFrame()
    for i in dict_cont_bin.keys():
        df_cont_bin_train = pd.concat([ df_cont_bin_train , cont_var_bin_map(data_train[i], dict_cont_bin[i]) ], axis = 1)

    # 离散变量分箱映射
#    ss = data_train[list( dict_disc_bin.keys())]
    df_disc_bin_train = pd.DataFrame()
    for i in dict_disc_bin.keys():
        df_disc_bin_train = pd.concat([ df_disc_bin_train , disc_var_bin_map(data_train[i], dict_disc_bin[i]) ], axis = 1)

    # 测试数据分箱
    # 连续变量分箱映射
    ss = data_test[list( dict_cont_bin.keys())]
    df_cont_bin_test = pd.DataFrame()
    for i in dict_cont_bin.keys():
        df_cont_bin_test = pd.concat([ df_cont_bin_test , cont_var_bin_map(data_test[i], dict_cont_bin[i]) ], axis = 1)

    # 离散变量分箱映射
#    ss = data_test[list( dict_disc_bin.keys())]
    df_disc_bin_test = pd.DataFrame()
    for i in dict_disc_bin.keys():
        df_disc_bin_test = pd.concat([ df_disc_bin_test , disc_var_bin_map(data_test[i], dict_disc_bin[i]) ], axis = 1)