《Python金融大数据风控建模实战》 第6章 变量分箱方法
本章引言
变量分箱是一种特征工程方法,意在增强变量的可解释性与预测能力。变量分箱方法主要用于连续变量,对于变量取值较稀疏的离散变量也应该进行分箱处理。
变量分箱对模型的好处:
-
降低异常值的影响,增强模型的稳定性
数据中存在异常值会使模型产生一定的偏差,从而影响预测效果。通过分箱模型可以降低异常值的噪声特性,使模型更稳健。树模型对异常值不敏感,但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]表示总的坏样本的个数,