1500字范文,内容丰富有趣,写作好帮手!
1500字范文 > 《Python金融大数据风控建模实战》 第6章 变量分箱方法

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

时间:2024-06-04 01:36:28

相关推荐

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

《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 osimport pandas as pdimport numpy as npfrom sklearn.model_selection import train_test_splitimport warningswarnings.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_testdef cal_advantage(temp, piont, method,flag='sel'):'''计算当前切分点下的指标值参数:temp: 上一步的分箱结果,pandas dataframepiont: 切分点,以此来划分分箱method: 分箱方法选择,1:chi-merge , 2:IV值, 3:信息熵'''# temp = binDSif flag == 'sel':# 用于最优切分点选择,这里只是二叉树,即二分bin_num = 2'''numpy.empty(shape, dtype=float, order=‘C’)根据给定的维度和数值类型返回一个新的数组,其元素不进行初始化。参数:shape:整数或者整数组成的元组功能:空数组的维度,例如:(2, 3)或者2dtype:数值类型,可选参数功能:指定输出数组的数值类型,例如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 dataframeii=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,1for i in range(bin_num):# j=0,1for 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/expectM_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.NaNelse:IV = 0for 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 = 0for 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 = 0for i in range(bin_num):entropy_temp = 0for 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:passreturn M_valuedef best_split(df_temp0, method, bin_num):'''在每个候选集中寻找切分点,完成一次分裂。select_split_point函数的中间过程函数参数:df_temp0: 上一次分箱后的结果,pandas dataframemethod: 分箱方法选择,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 = 0bestI = 1# 以候选集的每个切分点做分隔,计算指标值for i in range(1, piont_len):# 计算指标值value = cal_advantage(df_temp0,i,method,flag='sel')if bestValue < value:bestValue = valuebestI = i# create new var split'''1.np.where(condition, x, y)当where内有三个参数时,第一个参数表示条件,当条件成立时where方法返回x,当条件不成立时where返回y2.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 为>bestInewbinDS_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 dataframemethod:分箱方法选择,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 bintoSplitbin_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 seriesbin_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 resultdef limit_min_sample(temp_cont, bin_min_num_0):'''分箱约束条件:每个箱内的样本数不能小于bin_min_num_0,cont_var_bin函数的中间过程函数参数:temp_cont: 初始化分箱后的结果 pandas dataframebin_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)] = ielse:index = (x > bin_low) & (x <= bin_up)temp[index] = itemp.name = temp.name + "_BIN"return tempdef 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).Tdf.columns = ['bin', 'bin_low', 'bin_up', 'total']return dfdef 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]-1if mmin >= df_temp_all.shape[0]:gain_value_save0=0gain_rate_save0=0'''np.linspace主要用来创建等差数列。numpy.linspace(start, stop, num=50, endpoint=True, retstep=False, dtype=None, axis=0)参数:start:返回样本数据开始点stop:返回样本数据结束点num:生成的样本数据量,默认为50endpoint:True则包含stop;False则不包含stopretstep: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'] = 1df_temp_all['bin_raw'] = range(1, len(df_temp_all) + 1)df_temp_all['var'] = df_temp_all.index # 初始化箱的编号gain_1 = 1e-10gain_rate_save0 = []gain_value_save0 = []# 分箱约束:最大分箱数限制for i in range(1,mmax):# i = 1df_temp_all, gain_2 = select_split_point(df_temp_all, method=method)gain_rate = gain_2 / gain_1 - 1 # ratio gaingain_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_2if df_temp_all.bin.max() >= mmin and df_temp_all.bin.max() <= mmax:if gain_rate <= stop_limit or pd.isnull(gain_rate):breakdf_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 subdata = 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 datadata = 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] + 1data.loc[row_num, 'bin_low'] = np.nandata.loc[row_num, 'bin_up'] = np.nandata.loc[row_num, 'total'] = df_na.shape[0]data.loc[row_num, 'bin'] = data.bin.max() + 1return data , gain_value_save0 ,gain_rate_save0def cal_bin_value(x, y, bin_min_num_0=10):'''按变量类别进行分箱初始化,不满足最小样本数的箱进行合并参数:x: 待分箱的离散变量 pandas Seriesy: 标签变量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_tempdef disc_var_bin(x, y, method=1, mmin=3, mmax=8, stop_limit=0.1, bin_min_num = 20 ):'''离散变量分箱方法,如果变量过于稀疏最好先编码在按连续变量分箱参数:x:输入分箱数据,pandas seriesy:标签变量method:分箱方法选择,1:chi-merge , 2:IV值, 3:信息熵mmin:最小分箱数,当分箱初始化后如果初始化箱数小于等mmin,则mmin=2,即最少分2箱,如果分两厢也无法满足箱内最小样本数限制而分1箱,则变量删除mmax:最大分箱数,当分箱初始化后如果初始化箱数小于等于mmax,则mmax等于初始化箱数-1stop_limit:分箱earlystopping机制,如果已经没有明显增益即停止分箱bin_min_num:每组最小样本数返回值:分箱结果:pandas dataframe'''# x = data_train.purpose# y = data_train.targetdel_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]-1if mmin >= temp_cont.shape[0]:mmin = 2mmax = temp_cont.shape[0]-1if mmax ==1:print('变量 {0}合并后分箱数为1,该变量删除'.format(x.name))del_key.append(x.name)gain_1 = 1e-10gain_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 gaingain_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_2if temp_cont.bin.max() >= mmin and temp_cont.bin.max() <= mmax:if gain_rate <= stop_limit:breaktemp_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_rawtemp_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] + 1temp_cont.loc[index_1, 'total'] = df_na.shape[0]temp_cont.loc[index_1, 'bin'] = temp_cont.bin.max() + 1temp_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_keydef disc_var_bin_map(x, bin_map):'''用离散变量分箱后的结果,对原始值进行分箱映射参数:x: 待分箱映射的离散变量,pandas Seriesbin_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_xif __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.nandata_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.nandata_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)

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。