金融申请评分卡的数据预处理和特征衍生
1、模型处理的一般流程
以上为模型的一般处理办法;在本次数据字段有:
字段 | 名称 |
---|---|
member_id | ID |
loan_amnt | 申请额度 |
term | 产品期限 |
int_rate | 利率 |
emp_length | 工作期限 |
home_ownership | 是否有自有住宅 |
annual_inc | 年收入 |
verification_status | 收入核验状态 |
desc | 描述 |
purpose | 贷款目的 |
title | 贷款目的描述 |
zip_code | 联系地址邮政编码 |
addr_state | 联系地址所属州 |
delinq_2yrs | 申贷日期前2年逾期次数 |
inq_last_6mths | 申请日前6个月咨询次数 |
mths_since_last_delinq | 上次逾期距今月份数 |
mths_since_last_record | 上次登记公众记录距今的月份数 |
open_acc | 征信局中记录的信用产品数 |
pub_rec | 公众不良记录数 |
total_acc | 正在使用的信用产品数 |
pub_rec_bankruptcies | 公众破产记录数 |
earliest_cr_line | 第一次借贷时间 |
loan_status | 贷款状态—目标变量 |
2、数据的预处理
2.1、基本处理办法
- 利率方面的处理办法:带%的百分比,需要转化为浮点数
- 工作年限“<1 year”转化为0,“>10 year”的转化为1
- 日期方面:直接转化为标准日期
- 文本信息:字段中的desc就是客户申请期间的申请原因等信息,这里处理采用最简单的办法,如果里面有信息,则为1,无信息则为0,其他例如采用NLP的办法,做其他处理,暂时不做,因为涉及分词等等,处理其他麻烦,不是写这次博客的主要目的。
2.2、缺失值的处理办法
缺失值的种类情况:
- 完全随机缺失
- 随机缺失
- 完全非随机缺失
处理的办法一般为以下几种:
- 补缺
- 作为一种状态,例如,空的为0,非空为1,处理起来简单,如果缺失值不多,效果不错
- 删除本行的记录,这种处理办法最简单,尤其在数据量较大的情况下,删除部分数据,对整体基本无影响。
2.3、数据特征构-特征衍生
因为在原有的特征上面,也就是直接特征方面的信息含量不足以很好的建立申请评分卡模型,所以一般都会去构建新的特征,进行特征的衍生。那么经常接触到的特征衍生办法如下:
- 计数:过去1年内申请贷款的总次数
- 求和:过去1年内的在线上的消费金额
- 比例:贷款申请额度和年收入的占比
- 时间差:第一次开户距离今天的时间长度
- 波动率:过去3年内每份工作的时间的标准差
以上构建的办法均基于经验的构建,不包含了因子分析等办法
2.4、特征的分箱
特征分箱的目的:
- 将连续变量离散化
- 将多状态的离散变量合并成少状态
分箱的通俗解释:
- 稳定性:避免了特征中的无意义的波动对评分带来的不好的影响
- 加强了模型的健壮性:避免了模型受到极端值的影响
举个例子:例如未进行分箱之前,样本数据里面没有一个高二年级的学生,那么假定做好分箱之后,高一到高三均属于高中,因此出现一个高二年级的学生后,就会被划入高中这个“箱”,模型的稳定性就得到了加强;在健壮性方面,例如我的收入是1000,在申请贷款的时候给予的评分很低,假定就20分,经过我的不断努力,跳槽7-8次之后,薪水涨到1500左右,这个时候,还是属于低收入的困难人群,那么给予的评分还是20分左右,这样模型的健壮性就得到了体现,模型不需要根据一些小的变化就进行调整。
分箱简单的解释是:分箱就是为了做到同组之间的差异尽可能的小,不同组之间的差异尽可能的大。
分箱的好处:
- 可以把缺失值作为一个独立的箱带入到模型中去
- 将所有的变量变换到相似的尺度上(例如:一个变量是年龄,一个变量是月收入,不做分箱,2者之间的变化差距太大)
分箱的缺点:
- 计算量比较大,处理数据过程较为繁琐。
- 编码之后容易导致信息的丢失。
2.5、特征的分箱方法
分箱的办法主要接触到很多,等距、等频、卡方分箱、决策树分箱法,这里只具体展示卡方分箱法,决策树分箱的代码如下,其他的分箱仅说明原理:
coding=utf-8
import operator
from math import log
import time
class InformationGainSplitDiscretization(object):
def __init__(self):
self.minInfoGain_epos = 1e-8 #停止条件之一:最小信息增益,当某数据集的最优分裂对应的信息增益(即最大信息增益)小于这个值,则此数据集停止进一步的分裂。
self.splitPiontsList = [] #分裂点列表,最终要依分裂点的值升序排列。以便后续的离散化函数(输入:待离散的数据集)使用。 #self.totalGain = ()
self.tree_deep = 3
def splitDataSet(self,dataSet, splitpoint_idx):
leftSubDataSet = []
rightSubDataSet = []
for leftSubSet in dataSet[:(splitpoint_idx+1)]:
leftSubDataSet.append(leftSubSet)
for rightSubSet in dataSet[(splitpoint_idx+1):]:
rightSubDataSet.append(rightSubSet)
leftSubDataSet.sort(key=lambda x : x[0], reverse=False)
rightSubDataSet.sort(key=lambda x : x[0], reverse=False)
return (leftSubDataSet,rightSubDataSet)
def calcInfoGain(self,dataSet):
lable1_sum = 0
total_sum = 0
infoGain = 0
if dataSet == []:
pass
else :
for i in range(len(dataSet)):
lable1_sum += dataSet[i][1]
total_sum += dataSet[i][1] + dataSet[i][2]
p1 = (lable1_sum*1.0) / (total_sum*1.0)
p0 = 1 - p1
if p1 == 0 or p0 == 0:
infoGain = 0
else:
infoGain = - p0 * log(p0) - p1 * log(p1)
return infoGain,total_sum
def getMaxInfoGain(self,dataSet):
gainList = []
totalGain = self.calcInfoGain(dataSet)
maxGain = 0
maxGainIdx = 0
for i in range(len(dataSet)):
leftSubDataSet_info = self.calcInfoGain(self.splitDataSet(dataSet, i)[0])
rightSubDataSet_info = self.calcInfoGain(self.splitDataSet(dataSet, i)[1])
gainList.append(totalGain[0]
- ((leftSubDataSet_info[1]*1.0)/(totalGain[1]*1.0)) * leftSubDataSet_info[0]
- ((rightSubDataSet_info[1]*1.0)/(totalGain[1]*1.0)) * rightSubDataSet_info[0])
maxGain = max(gainList)
maxGainIdx = gainList.index(max(gainList))
splitPoint = dataSet[maxGainIdx][0]
return splitPoint,maxGain,maxGainIdx
def getSplitPointList(self,dataSet,maxdeeps,begindeep):
if begindeep >= maxdeeps:
pass
else:
maxInfoGainList = self.getMaxInfoGain(dataSet)
if maxInfoGainList[1] <= self.minInfoGain_epos:
pass
else:
self.splitPiontsList.append(maxInfoGainList[0])
begindeep += 1
subDataSet = self.splitDataSet(dataSet, maxInfoGainList[2])
self.getSplitPointList(subDataSet[0],maxdeeps,begindeep)
self.getSplitPointList(subDataSet[1],maxdeeps,begindeep)
def fit(self, x, y,deep = 3, epos = 1e-8):
self.minInfoGain_epos = epos
self.tree_deep = deep
bin_dict = {}
bin_list = []
for i in range(len(x)):
pos = x[i]
target = y[i]
bin_dict.setdefault(pos,[0,0])
if target == 1:
bin_dict[pos][0] += 1
else:
bin_dict[pos][1] += 1
for key ,val in bin_dict.items():
t = [key]
t.extend(val)
bin_list.append(t)
bin_list.sort( key=lambda x : x[0], reverse=False)
self.getSplitPointList(bin_list,self.tree_deep,0)
self.splitPiontsList = [elem for elem in self.splitPiontsList if elem != []]
self.splitPiontsList.sort()
def transform(self,x):
res = []
for e in x :
index = self.get_Discretization_index(self.splitPiontsList, e)
res.append(index)
return res
def get_Discretization_index(self, Discretization_vals, val):
index = len(Discretization_vals) + 1
for i in range(len(Discretization_vals)):
bin_val = Discretization_vals[i]
if val <= bin_val:
index = i + 1
break
return index
无监督分箱方法(一般不推荐,好不好用,得看人品,一般比卡方和决策树的效果要差点)
等距划分:
从最小值到最大值之间,均分为 N 等份, 这样, 如果 A,B 为最小最大值, 则每个区间的长度为 W=(B−A)/N , 则区间边界值为A+W,A+2W,….A+(N−1)W 。这里只考虑边界,每个等份里面的实例数量可能不等。等频分箱:
区间的边界值要经过选择,使得每个区间包含大致相等的实例数量。比如说 N=10 ,每个区间应该包含大约10%的实例。比较:
比如,等宽区间划分,划分为5区间,最高工资为50000,则所有工资低于10000的人都被划分到同一区间。等频区间可能正好相反,所有工资高于50000的人都会被划分到50000这一区间中。这两种算法都忽略了实例所属的类型,落在正确区间里的偶然性很大。
对特征进行分箱后,需要对分箱后的每组(箱)进行woe编码,然后才能放进模型训练。
有监督分箱方法
Best-KS(非常类似决策树的分箱,决策树分箱的标准是基尼指数,这里就只考虑KS值):
让分箱后组别的分布的差异最大化。
步骤:对于连续变量- 排序
- 计算每一点的KS值
- 选取最大的KS值对应的特征值,用该特征值将特征分为大于该值和小于该值两端
- 对于每一部分,循环b、c步骤,直到满足终止条件
终止条件,继续回滚到上一步:
- 下一步分箱,最小的箱的占比低于设定的阈值(0.05)
- 下一步分箱后,有一箱的对应的y的类别全部为0或者1
- 下一步分箱后,bad rate不单调
步骤:对于离散很高的分类变量
- 编码(类别变量个数很多,先编码,再分箱。)
- 依据连续变量的方式进行分箱
分箱以后变量必须单调,具体的例子如下图:
假定变量被分成了6个箱,假定X轴为年龄,Y轴为坏样本率,这样就可以解释了,年龄越大,坏客户的比例约多。如果分箱之后不单调,那么模型在这个变量上的可解释性就成问题了。所以在分箱期间要注意变量的单调性。
卡方分箱:
这里copy一段官方解释(比较长):自底向上的(即基于合并的)数据离散化方法。它依赖于卡方检验:具有最小卡方值的相邻区间合并在一起,直到满足确定的停止准则。通俗的讲,即让组内成员相似性强,让组间的差异大。基本思想:对于精确的离散化,相对类频率在一个区间内应当完全一致。因此,如果两个相邻的区间具有非常类似的类分布,则这两个区间可以合并;否则,它们应当保持分开。而低卡方值表明它们具有相似的类分布。
忘记上面,直接实践一下,步骤如下:
- 预先我们设定一个卡方的阈值
- 根据离散化的属性对实例进行排序,每个实例属于一个区间
- 开始合并,具体分2步:
- 计算每一对相邻区间的卡方数值
- 卡方值最小的一对区间直接合并
X2=∑i=12∑j=12(Aij−Eij)2Eij X 2 = ∑ i = 1 2 ∑ j = 1 2 ( A i j − E i j ) 2 E i j
Aij A i j :第i区间第j类的实例的数量
Eij:Aij E i j : A i j 的期望频率,为 Ni×CjN N i × C j N ,N是总样本, Ni N i 是第i组的样本数, Cj C j 是第j类样本在全体中的比例
接下来就百度一下卡方检验阈值,直接看里面的数值,找到显著水平和自由度,自由度为2,90%置信度的情况下,卡方为4.6;如果忘记了卡方检验的意义,直接百度卡方检验。
目前一般分箱5个或者6个,置信度在0.95左右,区间为10-15之间。主要是因为分箱太多,操作起来太麻烦,对模型的提高也不大,分箱5个一般就不错。
卡方分箱的终止条件很简单,基本就是2条:
- 默认分到多少箱,如果已经分到了这个数值了,那就第2步
- 检查一下单调性,满足就完成分箱了,如果不满足,相邻的箱就合并,直到单调了为止,因为最后合并到2个箱的时候,是一定单调的。
- 补充:分箱之前要切分,通常50-100个切分点,看数据量的大小,最最最重要的,千万不要用等距划分,因为比如收入、年龄这些字段成偏态分步,数据没有平均分布,要用等频划分。
- 类别变量,类别较少,就不用在分箱了,如果有那个类别是全部为坏样本,需要和最小的不是坏样本的合并一下,因为不合并等会WOE不能计算了。
- 最后补充:在评分卡模型中,能不用热编码就不要用热编码,因为热编码膨胀了数据量,在选择变量是不是进入模型当中去,也是存在问题了,例如逐步回归就不好搞,业务方面的解释性也差,没直接的业务逻辑关系。总之,能不用就不用,要是没变量了,还是可以考虑用一下。
3、WOE编码
WOE编码官方解释:一种有监督的编码方式,将预测类别的集中度的属性作为编码的数值;优势是:将特征的值规范到相近的尺度上。缺点是:需要分箱后每箱都同时有好坏样本(例如,预测违约和不违约可是使用WOE编码,如果去预测中度违约、重度违约、轻度违约等等情况,这个时候WOE编码就不行了)。通常意义上,WOE的绝对值在0.1-3之间。
编码的意义在于符号与好样本的比例有关;当好样本为分子,坏样本为分母的时候,可以要求回归模型的系数为负。
具体的WOE编码这里就不找材料了,CSDN博客上,有很多写的很好的,这里引用一篇博客在这里,请猛击。
这里简单引用一下其他人成熟的比较正式说法,WOE公式如下:
例如,以年龄作为一个变量,由于年龄是连续型自变量,需要对其进行离散化处理,假设离散化分为5组(如何分箱,上面已经介绍,后面将继续介绍),#bad和#good表示在这五组中违约用户和正常用户的数量分布,最后一列是woe值的计算,通过后面变化之后的公式可以看出,woe反映的是在自变量每个分组下违约用户对正常用户占比和总体中违约用户对正常用户占比之间的差异;从而可以直观的认为woe蕴含了自变量取值对于目标变量(违约概率)的影响。再加上woe计算形式与logistic回归中目标变量的logistic转换(logist_p=ln(p/1-p))如此相似,因而可以将自变量woe值替代原先的自变量值;,具体的计算情况如下:
Age | bad | good | WOE |
---|---|---|---|
0-10 | 50 | 200 | =ln((50/100)/(200/1000))=ln((50/200)/(100/1000)) |
10-18 | 20 | 200 | =ln((20/100)/(200/1000))=ln((20/200)/(100/1000)) |
18-35 | 5 | 200 | =ln((5/100)/(200/1000))=ln((5/200)/(100/1000)) |
35-50 | 15 | 200 | =ln((15/100)/(200/1000))=ln((15/200)/(100/1000)) |
50以上 | 10 | 200 | =ln((10/100)/(200/1000))=ln((10/200)/(100/1000)) |
汇总 | 100 | 1000 |
4、IV值
IV值的官方解释为:IV(Information Value), 衡量特征包含预测变量浓度的一种指标。
计算公式如下:
Age | bad | good | iv |
---|---|---|---|
0-10 | 50 | 200 | =(50/100-200/1000)*ln((50/100)/(200/1000))= IV1 I V 1 |
10-18 | 20 | 200 | =(20/100-200/1000)*ln((20/100)/(200/1000))= IV2 I V 2 |
18-35 | 5 | 200 | =(5/100-200/1000)*ln((5/100)/(200/1000))= IV3 I V 3 |
35-50 | 15 | 200 | =(25/100-200/1000)*ln((15/100)/(200/1000))= IV4 I V 4 |
50以上 | 10 | 200 | =(10/100-200/1000)*ln((10/100)/(200/1000))= IV5 I V 5 |
汇总 | 100 | 1000 | IV汇总 I V 汇 总 = IV1 I V 1 + IV2 I V 2 + IV3 I V 3 + IV4 I V 4 + IV5 I V 5 |
IV汇总 I V 汇 总 就得到了这个变量的总体IV值。