美丽的周三早上,被专利问题所困扰,完全写不进去,换个思维决定把之前做过的评分卡模型整体流程逻辑记录在知乎上,希望能给大家提供帮助。

前言:

评分卡模型经常被用于银行,电信等领域,作为识别用户欺诈概率或者流失概率的预判。

首先引入2个概念

woe全称叫Weight of Evidence,常用在风险评估、授信评分卡等领域。

IV全称是Information value,可通过woe权求和得到,衡量自变量对应变量的预测能力

1引入模块

import pandas as pd
import numpy as np
import math
import time
#存储与读取字典
import pickle
#自己写的mdlp
import mdlp
#自己写的相关性计算筛选函数
import cor_list_high_delete

2数据预处理(特征工程第一阶段)

2.1数据读取

样本数据我存储到本地环境下了

读取数据进行简单到统计以及筛选数据

#读入数据
data=pd.read_csv('D:\\train.csv',encoding='utf-8',sep=';')
#71116,349 数据的维度
m,n=data.shape
#读取数据
#目标变量定义为ls_flag
target='ls_flag'
data[target].value_counts()
#去掉一些无用字段这个是根据实际业务和数据分析过的
#缺失值占比交高的字段
useless_field=['a','b','c','d','e']
#获取有用字段的列名names= [x for x in data.columns if x not in useless_field]
#筛选数据
data=data.loc[:,names]

辅助:缺失值统计函数返回的是字典 key=列名 value=缺失值数量 or占比

这个value取决于你输入的axis的值

def MissDataCount(data,axis):
''':param data: 输入数据框:param axis: =1 计数 =0百分比:return: 返回字典'''
count={}
countPercent={}
if axis==1:
for name in data.columns:
missdatacount=sum(data[name].isnull()==True)#缺失值个数的统计
if missdatacount>0:
count[name]=missdatacount#计数
return count#{列名:count}
if axis==0:
for name in data.columns:
missdatacount=sum(data[name].isnull()==True)#缺失值个数的统计
if missdatacount>0:
countPercent[name]=missdatacount/data.shape[0]
return countPercent#{列名:countPercent}

2.2数据划分

由于数据维度很多,包含的数据类型也很多,例如数值型,离散型数据等等

需要对整体数据进行划分,划分numeric类型数据以及非numeric类型数据

好处:

1、很多机器学习算法都不能很好的处理离散型数据,

2、为了后续把数据换woe值这里先区分数据类型

3、缺失值填充,数值型数据和非数值类型数据缺失值填充是不一样的

#区分数据类型
#float 371 int64 5 object 31
#统计各种类型数据
data_type=data.get_dtype_counts()
#筛选numeric类型数据
num_data=data.select_dtypes(exclude=[object])
#筛选非numeric类型数据
factor_data=data.select_dtypes(include=[object])
num_train_data_names=num_data.columns
factor_train_data_names=factor_data.columns

2.3缺失值填充

缺失值填充方法有很多,对于数值型数据可以填充 均值,众数,回归填充,0值填充或者定义一个没有出现过的值填充都是可以的,其实不管是采用上诉哪个方式填充对于结果的影响都差异不大,有兴趣的同学可以都试试。

我这里数值型缺失值填充选取的是0.123456一个没有出现过的值

非数值型填充我这里也是填充了一个没有出现过的值‘wxyhappy’

#缺失值替换
#numeric类型数据填充缺失值
num_data=num_data.fillna(value=0.123456)#数值型缺失值替换为0
#非numeric类型数据填充
factor_data=factor_data.fillna('wxyhappy')
#检验缺失值是否填充成功
num_data.columns[num_data.isnull().sum()>0]

3数据预处理(特征工程第二阶段)

3.1数值型字段离散化

离散化的方法有很多种,有监督的和无监督的,无监督的离散化只依赖待离散化数据列自身的分布,基于熵(或者叫基于信息增益)的离散化分箱方法最好的就是MDLP(Minimal Description Length Principle最短描述长度原则)方法。基本思想是:如果分组后的输入变量对输出变量取值的解释能力显著低于分组之前,那么这样的分组是没有意义的。所以,待分组变量(视为输入变量)应在输出变量的“指导”下进行分组。

辅助:Mdlp方法吴小宇:一种基于信息熵的离散化方法(MDLP)python实现

#数字型字段离散化
lisan_data=num_data
lisan_data_num=lisan_data
lisan_data[target]=data[target]
lisan_time=time.time()
lisan_cut,min_lisan_cut_max=mdlp.mdlp(lisan_data)
print('本次离散化时间为:',round(time.time()-lisan_time,2),'秒','\n',
'离散的数据维度为:',lisan_data.shape[0],'行',lisan_data.shape[1]-1,'列')

3.2数据按切点切分

其实实际应用中这个切点是需要保存下来的,因为你在评估数据集上也是按照训练集的切点来切分的,为了保证数据一致性。

#保存切点(0)~
file=open('d:/ML/data/lisan_cut.txt','w')
for i in lisan_cut:
file.write(str(i))
file.write("\n")
file.close()
#保存切点(1) min 切点 max
file=open('d:/ML/data/min_lisan_cut_max.txt','w')
for i in min_lisan_cut_max:
file.write(str(i))
file.write("\n")
file.close()
#读取切点
file = open('d:/ML/data/lisan_cut.txt','r')
lines = file.readlines()#读取全部内容
lisan_cutx=[]
for line in lines:
line=line.strip().strip('[').strip(']')#去掉换行符号以及列表的[]line=line.split(',')#逗号分隔开 返回的是列表linex=[]
for i in line:
if i=='ALL':
i=i
else:
i=float(i)
linex.append(i)
#line=[float(x) for x in line]#列表中的字符串修改成float
lisan_cutx.append(linex)
#切点全 min lisan_cut max
lisan_cut_all=[]
for i in range(len(lisan_cutx)):
maxdata=[max(lisan_data.ix[:,i])]
mindata=[min(lisan_data.ix[:,i])]
mindata.extend(lisan_cutx[i])
mindata.extend(maxdata)
lisan_cut_all.append(mindata)
#找出切点为'ALL'的字段
ALLlistcolumns=[]
ALLlist=[]
for i in range(len(lisan_cutx)):
if lisan_cutx[i]==['ALL']:
ALLlist.append(i)
ALLlistcolumns.append(lisan_data.columns[i])
##lisan_cut_all去除切点为all的字段
lisan_cut_all_delete=[]
for i in range(len(lisan_cut_all)):
if i not in ALLlist:
lisan_cut_all_delete.append(lisan_cut_all[i])
#num_data 去除切点为ALL的字段
names= [x for x in lisan_data.columns if x not in ALLlistcolumns]
lisan_datax=lisan_data[names]
#根据切点切分每一列数据
cut_num_data=lisan_datax
for i in range(0,cut_num_data.shape[1]-1):
label=range(1,len(lisan_cut_all_delete[i]))
lisan_cut_all_delete[i].sort()
cut_num_data.ix[:,i]=pd.cut(cut_num_data.ix[:,i],bins= lisan_cut_all_delete[i],labels=label,include_lowest=True)
#合并
imergea=pd.concat([cut_num_data[cut_num_data.columns[0:cut_num_data.shape[1]-1]],factor_data],axis=1)
imergea=pd.concat([imergea,num_data[target]],axis=1)

3.3计算woe值与iv值

切分后的numeric都变成类别型数据,结合之前划分的非数值数据,这里把所有列都进行woe值与iv值计算

#计算woe值
def woe_woee_iv(imerge,target):
iv={}
woe={}
woee=[]
for q in imerge.ix[:,0:imerge.shape[1]-1].columns:#遍历所有列名
k=pd.crosstab(imerge.ix[:,imerge.shape[1]-1], imerge[q], margins=False)#margins=True输出总计的值
if(sum(k.values[0]==0)+sum(k.values[1]==0)>0):
#0值替换为1
print('将',q,'与',target,'列联表中的',sum(k.values[0]==0)+sum(k.values[1]==0),'个0元素替换为1')
k.values[k.values==0]=1
#计算频率
z=[sum(k.values[0]),sum(k.values[1])]
woe1=np.zeros([2,len(k.values[0])])
woe2 = np.zeros([2, len(k.values[0])])
for j in range(0,2):
for i in range(0,len(k.values[0])):
woe1[j][i] = k.values[j][i] / z[j]
woe2[j][i]=math.log(k.values[j][i]/z[j])
woe_a=woe1[1]-woe1[0]
woe_b=woe2[1]-woe2[0]
woee.append(list(woe_b))
woe[q]=woe_b#为了计算下面的拐点以及WOE与变量的替换
iv[q]=sum(woe_a*woe_b)
return woe,woee,iv
woe,woee,iv=woe_woee_iv(imergea,target)

3.4woe值拐点计算以及iv筛选变量

根据woe值相邻2点之间做差,判断拐点个数

选取拐点大于1的列删除,为什么删除拐点大于1,其实你自己画图就知道了,目的保留成线性的特征变量

iv值选取较大的特征好处

1、筛选特征贡献明显的进入模型

2、维度适当降低运算速度快

3、维度适当降低不容易过拟合,模型泛化能力强

当然这个iv值选取多少,选取与不选取,都是根据实际数据情况定的,灵活运用。

#计算数值型字段的WOE值的拐点数
guaidian={}
for i in imergea.columns[0:imergea.shape[1]-1]:
w=woe[i]#取字典woe中的values
distance=w[1:]-w[:-1]#相邻的2个点依次做差
guaidian[i]=sum(distance[:-1]*distance[1:]<0)#累计(相邻2个差之间的乘积小于0的个数)
#输出拐点大于n的列
more=[]
n=1
for i in guaidian.keys():
if guaidian[i]>n:
more.append(i)
# 选取IV值大于miniv的列
bigiv=[]
miniv=0.3
for i in iv.keys():
if iv[i]>=miniv:
bigiv.append(i)
namez=np.array(imergea.columns)#全部名称
namemore=np.array(more)#拐点数大于n的名称
namebigiv=np.array(bigiv)
imergex0=np.setdiff1d(namez,namemore)#除去拐点大于n
imergex1=np.setdiff1d(imergex0,namebigiv)
imergex2 = np.setdiff1d(imergex0,imergex1)

3.5woe值

筛选列类别变量种类低于30个到特征,这里是根据事情数据来看的,因为我这里要保证进入模型的数据特征介于50-70之间,这个大神们可以根据实际情况来定义哦。

woe换数据之后,在看下变量之间的相关性(相关性阀值自己定义我习惯定义0.9),之所以在最后看变量之间的相关性,是因为如果两个变量相关性很高,你需要剔除一个,因为之前你已经计算过iv值了这里就可以根据2个变量的iv值来比较,去掉iv值较小的那个变量。

#列类别变量超过30个的剔除
imerge2=imergea[imergex2]
name30=[]
for i in range(0,imerge2.shape[1]-1):
if len(imerge2.ix[:,i].value_counts())>30:
name30.append(imerge2.columns[i])
imergex3 = np.setdiff1d(imergex2,name30)
#WOE转换timewoe=time.time()
imergea=imergea[imergex3]
imergea_1=pd.DataFrame(columns=imergea.columns,index=imergea.index)
for i in imergea.columns:
x=set(imergea[i])
y=list(x)
for j in range(0,len(y)):
imergea_1.ix[imergea[i] == y[j], i] = woe[i][j]
print("done in%0.3fs." % (time.time()-timewoe))
imergea_1.to_csv('d:/ML/data/imergea_1.csv')
#下面的辅助函数为cor_list_high_delete
high_cormatrix=cor_list_high_delete.cor_list(imergea_1,0.9)
del_name=cor_list_high_delete.high_delete(high_cormatrix,imergea_1,iv)
names_1= [x for x in imergea_1.columns if x not in del_name]
imergea_model=imergea_1[names_1]
imergea_model=pd.concat([imergea_model,num_data[target]],axis=1)
#保存进入模型数据
imergea_model.to_csv('d:/ML/data/imergea_model.csv')

辅助:按照iv值大小删除相关性较高的变量

from scipy.stats import pearsonr
def cor_list(x,high_cor):
''':param x: 输入的数据框:param high_cor: 表示输出相关系数大于的界限:return:'''
list=[]
for i in range(0,x.shape[1]):
for j in range(1,x.shape[1]):
a=x.ix[:,i]
b=x.ix[:,j]
pcor=pearsonr(a,b)#计算相关系数
if pcor[0]>high_cor and i!=j:
if i
list.append([x.columns[i],x.columns[j]])
else:
list.append([x.columns[j],x.columns[i]])
#列表去重
list_1=[]
for k in list:
if k not in list_1:
list_1.append(k)
return(list_1)
def high_delete(high_cormatrix,dataframe,iv):
''':param high_cormatrix: 相关性矩阵:param dataframe: 数据:param iv: iv值:return:'''
#留下标签
list=[]
for i in high_cormatrix:
if iv[dataframe.columns[i[0]]]>iv[dataframe.columns[i[1]]]:#比较两个相关变量对应iv值的大小
list.append(i[0])
else:
list.append(i[1])
list_1=[]#去重list
for j in list:
if j not in list_1:
list_1.append(j)
#全部标签
list_all=[]
for k in high_cormatrix:
for l in range(len(k)):
list_all.append(k[l])
list_all_1=[]
for m in list_all:
if m not in list_all_1:
list_all_1.append(m)
#删除标签
list_delete=[]
for n in list_all_1:
if n not in list_1:
list_delete.append(n)
del_name=[]
for number in list_delete:
del_name.append(dataframe.columns[number])
return del_name

其实现在数据已经整理好了,记住如果数据量不是非常大一定要把整理到最后的数据存储起来,这样用起来也方便,我这里存储过多个步骤的数据,这样如果有意外发生,数据还是可以继续往下面使用的,尤其是离散化的切点数据,因为mdlp离散化的速度非常慢,如果切点数据丢失需要重新离散化,这个会消耗大量的时间。

最后一步就是建立训练数据的模型以及模型的调优了,我这里就简单的放个lg模型。

from sklearn.linear_model import LogisticRegression
train_x=imergea_model.ix[:,0:imergea_model.shape[1]-1]
train_y=imergea_model[target]
lg_model=LogisticRegression()
lg_model.fit(train_x,train_y)
lg_model.score(train_x,train_y)#准确率
print(lg_model.coef_)#输出系数

写了一上午终于写完了,代码也比较多,年纪大了感觉还是有点小累,这篇文章希望能给不熟悉评分卡模型或者想学习了解评分卡模型的读者一些帮助,个人觉得主要还是这个数据处理的套路,当然你也可以建立一套属于自己针对这类模型数据特征工程的方法论。