1. 数据准备
1.1 梳理数据的内在逻辑
1. 关系种类
一对一:一个用户有一个注册手机号
一对多:一个用户多笔借款
多对多:一个用户可以登录多个设备,一个设备可以有多个用户登录
- 举例
下图中,蓝色框为二月当期账单,红色框为订单
2. 梳理类ER图
- 任务:分析厚数据常登陆首单用户的逾期情况
- 可以将表结构展示到特征文档当中,说明取数逻辑
1.2 样本设计和特征框架
1. 定义观察期样本
- 确定观察期(定X时间切面)和表现期(定Y的标签)
- 确认样本数目是否合理
2. 数据EDA
- 看数据总体分布
data.shape
data.isnull()
data.info()
data.describe()
- 看好坏样本分布差异
data[data[label]==0].describe() 好用户
data[data[label]==1].describe() 坏用户
- 看单个数据
data.sample(n=10,random_state=1)
3. 梳理特征框架
- RFM生成新特征
举例 行为评分卡中的用户账单还款特征 - 用户账单关键信息:时间,金额,还款,额度
- 在构建特征前,要完成
- 类ER图
- 样本设计表
- 特征框架表
2. 特征构造
2.1 静态信息特征和时间截面特征
1. 用户静态信息特征
- 用户的基本信息(半年内不会变化)
2. 用户时间截面特征
- 未来信息 当前时间截面之后的数据
- 时间截面数据在取数的时候要小心,避免使用未来信息
- 产生 **未来信息 ** 最直接的原因:缺少快照表
- 金融相关数据原则上都需要快照表记录所有痕迹(额度变化情况,多次申请的通过和拒绝情况…)
- 缺少快照表的可能原因
- 快照表消耗资源比较大,为了性能不做
- 原有数据表设计人员疏忽,没做
- 借用其他业务数据(如电商)做信贷
- 举例
- 首次借贷 二次借贷 爬虫授权 三次借贷
实际存储
join 结果
解决方案:加入快照的存储
2.2 时间序列特征
1. 用户时间序列特征
- 从观察点往前回溯一段时间的数据
2. 时间序列特征衍生
2.1 特征聚合
- 将单个特征的多个时间节点取值进行聚合。特征聚合是传统评分卡建模的主要特征构造方法。
- 举例,计算每个用户的额度使用率,记为特征ft,按照时间轴以月份为切片展开
- 申请前30天内的额度使用率ft1
- 申请前30天至60天内的额度使用率ft2
- 申请前60天至90天内的额度使用率ft3
- 申请前330天至360天内的额度使用率ft12
- 得到一个用户的12个特征
import pandas as pd
import numpy as np
data = pd.read_excel('data/textdata.xlsx')
data.head()
2.2 计算
- 可以根据这个时间序列进行基于经验的人工特征衍生,例如计算最近P个月特征大于0的月份数
#最近p个月,ft>0的月份数
def Num(ft,p): #ft 特征名字 p特征大于0的月份数
df=data.loc[:,ft+'1':ft+str(p)]
auto_value=np.where(df>0,1,0).sum(axis=1)
return ft+'_num'+str(p),auto_value
- 计算最近P个月特征ft等于0的月份数
#最近p个月,ft=0的月份数
def zero_cnt(ft,p):
df=data.loc[:,ft+'1':ft+str(p)]
auto_value=np.where(df==0,1,0).sum(axis=1)
return ft+'_zero_cnt'+str(p),auto_value
- 计算近p个月特征ft大于0的月份数是否大于等于1
#最近p个月,ft>0的月份数是否>=1
def Evr(ft,p):
df=data.loc[:,ft+'1':ft+str(p)]
arr=np.where(df>0,1,0).sum(axis=1)
auto_value = np.where(arr,1,0)
return ft+'_evr'+str(p),auto_value
- 计算最近p个月特征ft的均值
#最近p个月,ft均值
def Avg(ft,p):
df=data.loc[:,ft+'1':ft+str(p)]
auto_value=np.nanmean(df,axis = 1 )
return ft+'_avg'+str(p),auto_value
- 计算最近p个月特征ft的和,最大值,最小值
#最近p个月,ft和
def Tot(ft,p):
df=data.loc[:,ft+'1':ft+str(p)]
auto_value=np.nansum(df,axis = 1)
return ft+'_tot'+str(p),auto_value
#最近(2,p+1)个月,ft和
def Tot2T(ft,p):
df=data.loc[:,ft+'2':ft+str(p+1)]
auto_value=df.sum(1)
return ft+'_tot2t'+str(p),auto_value
#最近p个月,ft最大值
def Max(ft,p):
df=data.loc[:,ft+'1':ft+str(p)]
auto_value=np.nanmax(df,axis = 1)
return ft+'_max'+str(p),auto_value
#最近p个月,ft最小值
def Min(ft,p):
df=data.loc[:,ft+'1':ft+str(p)]
auto_value=np.nanmin(df,axis = 1)
return ft+'_min'+str(p),auto_value
- 其余衍生方法
#最近p个月,最近一次ft>0到现在的月份数
def Msg(ft,p):
df=data.loc[:,ft+'1':ft+str(p)]
df_value=np.where(df>0,1,0)
auto_value=[]
for i in range(len(df_value)):
row_value=df_value[i,:]
if row_value.max()<=0:
indexs='0'
auto_value.append(indexs)
else:
indexs=1
for j in row_value:
if j>0:
break
indexs+=1
auto_value.append(indexs)
return ft+'_msg'+str(p),auto_value
#最近p个月,最近一次ft=0到现在的月份数
def Msz(ft,p):
df=data.loc[:,ft+'1':ft+str(p)]
df_value=np.where(df==0,1,0)
auto_value=[]
for i in range(len(df_value)):
row_value=df_value[i,:]
if row_value.max()<=0:
indexs='0'
auto_value.append(indexs)
else:
indexs=1
for j in row_value:
if j>0:
break
indexs+=1
auto_value.append(indexs)
return ft+'_msz'+str(p),auto_value
#当月ft/(最近p个月ft的均值)
def Cav(ft,p):
df=data.loc[:,ft+'1':ft+str(p)]
auto_value = df[ft+'1']/np.nanmean(df,axis = 1 )
return ft+'_cav'+str(p),auto_value
#当月ft/(最近p个月ft的最小值)
def Cmn(ft,p):
df=data.loc[:,ft+'1':ft+str(p)]
auto_value = df[ft+'1']/np.nanmin(df,axis = 1 )
return ft+'_cmn'+str(p),auto_value
#最近p个月,每两个月间的ft的增长量的最大值
def Mai(ft,p):
arr=np.array(data.loc[:,ft+'1':ft+str(p)])
auto_value = []
for i in range(len(arr)):
df_value = arr[i,:]
value_lst = []
for k in range(len(df_value)-1):
minus = df_value[k] - df_value[k+1]
value_lst.append(minus)
auto_value.append(np.nanmax(value_lst))
return ft+'_mai'+str(p),auto_value
#最近p个月,每两个月间的ft的减少量的最大值
def Mad(ft,p):
arr=np.array(data.loc[:,ft+'1':ft+str(p)])
auto_value = []
for i in range(len(arr)):
df_value = arr[i,:]
value_lst = []
for k in range(len(df_value)-1):
minus = df_value[k+1] - df_value[k]
value_lst.append(minus)
auto_value.append(np.nanmax(value_lst))
return ft+'_mad'+str(p),auto_value
#最近p个月,ft的标准差
def Std(ft,p):
df=data.loc[:,ft+'1':ft+str(p)]
auto_value=np.nanvar(df,axis = 1)
return ft+'_std'+str(p),auto_value
#最近p个月,ft的变异系数
def Cva(ft,p):
df=data.loc[:,ft+'1':ft+str(p)]
auto_value=np.nanvar(df,axis = 1)/(np.nanmean(df,axis = 1 )+1e-10)
return ft+'_cva'+str(p),auto_value
#(当月ft) - (最近p个月ft的均值)
def Cmm(ft,p):
df=data.loc[:,ft+'1':ft+str(p)]
auto_value = df[ft+'1'] - np.nanmean(df,axis = 1 )
return ft+'_cmm'+str(p),auto_value
#(当月ft) - (最近p个月ft的最小值)
def Cnm(ft,p):
df=data.loc[:,ft+'1':ft+str(p)]
auto_value = df[ft+'1'] - np.nanmin(df,axis = 1 )
return ft+'_cnm'+str(p),auto_value
#(当月ft) - (最近p个月ft的最大值)
def Cxm(ft,p):
df=data.loc[:,ft+'1':ft+str(p)]
auto_value = df[ft+'1'] - np.nanmax(df,axis = 1 )
return ft+'_cxm'+str(p),auto_value
#( (当月ft) - (最近p个月ft的最大值) ) / (最近p个月ft的最大值) )
def Cxp(ft,p):
df=data.loc[:,ft+'1':ft+str(p)]
temp = np.nanmax(df,axis = 1 )
auto_value = (df[ft+'1'] - temp )/ temp
return ft+'_cxp'+str(p),auto_value
#最近p个月,ft的极差
def Ran(ft,p):
df=data.loc[:,ft+'1':ft+str(p)]
auto_value = np.nanmax(df,axis = 1 ) - np.nanmin(df,axis = 1 )
return ft+'_ran'+str(p),auto_value
#最近p个月中,特征ft的值,后一个月相比于前一个月增长了的月份数
def Nci(ft,p):
arr=np.array(data.loc[:,ft+'1':ft+str(p)])
auto_value = []
for i in range(len(arr)):
df_value = arr[i,:]
value_lst = []
for k in range(len(df_value)-1):
minus = df_value[k] - df_value[k+1]
value_lst.append(minus)
value_ng = np.where(np.array(value_lst)>0,1,0).sum()
auto_value.append(np.nanmax(value_ng))
return ft+'_nci'+str(p),auto_value
#最近p个月中,特征ft的值,后一个月相比于前一个月减少了的月份数
def Ncd(ft,p):
arr=np.array(data.loc[:,ft+'1':ft+str(p)])
auto_value = []
for i in range(len(arr)):
df_value = arr[i,:]
value_lst = []
for k in range(len(df_value)-1):
minus = df_value[k] - df_value[k+1]
value_lst.append(minus)
value_ng = np.where(np.array(value_lst)<0,1,0).sum()
auto_value.append(np.nanmax(value_ng))
return ft+'_ncd'+str(p),auto_value
#最近p个月中,相邻月份ft 相等的月份数
def Ncn(ft,p):
arr=np.array(data.loc[:,ft+'1':ft+str(p)])
auto_value = []
for i in range(len(arr)):
df_value = arr[i,:]
value_lst = []
for k in range(len(df_value)-1):
minus = df_value[k] - df_value[k+1]
value_lst.append(minus)
value_ng = np.where(np.array(value_lst)==0,1,0).sum()
auto_value.append(np.nanmax(value_ng))
return ft+'_ncn'+str(p),auto_value
#最近P个月中,特征ft的值是否按月份严格递增,是返回1,否返回0
def Bup(ft,p):
arr=np.array(data.loc[:,ft+'1':ft+str(p)])
auto_value = []
for i in range(len(arr)):
df_value = arr[i,:]
value_lst = []
index = 0
for k in range(len(df_value)-1):
if df_value[k] > df_value[k+1]:
break
index =+ 1
if index == p:
value= 1
else:
value = 0
auto_value.append(value)
return ft+'_bup'+str(p),auto_value
#最近P个月中,特征ft的值是否按月份严格递减,是返回1,否返回0
def Pdn(ft,p):
arr=np.array(data.loc[:,ft+'1':ft+str(p)])
auto_value = []
for i in range(len(arr)):
df_value = arr[i,:]
value_lst = []
index = 0
for k in range(len(df_value)-1):
if df_value[k+1] > df_value[k]:
break
index =+ 1
if index == p:
value= 1
else:
value = 0
auto_value.append(value)
return ft+'_pdn'+str(p),auto_value
#最近P个月中,ft的切尾均值,这里去掉了数据中的最大值和最小值
def Trm(ft,p):
df=data.loc[:,ft+'1':ft+str(p)]
auto_value = []
for i in range(len(df)):
trm_mean = list(df.loc[i,:])
trm_mean.remove(np.nanmax(trm_mean))
trm_mean.remove(np.nanmin(trm_mean))
temp=np.nanmean(trm_mean)
auto_value.append(temp)
return ft+'_trm'+str(p),auto_value
#当月ft / 最近p个月的ft中的最大值
def Cmx(ft,p):
df=data.loc[:,ft+'1':ft+str(p)]
auto_value = (df[ft+'1'] - np.nanmax(df,axis = 1 )) /np.nanmax(df,axis = 1 )
return ft+'_cmx'+str(p),auto_value
#( 当月ft - 最近p个月的ft均值 ) / ft均值
def Cmp(ft,p):
df=data.loc[:,ft+'1':ft+str(p)]
auto_value = (df[ft+'1'] - np.nanmean(df,axis = 1 )) /np.nanmean(df,axis = 1 )
return ft+'_cmp'+str(p),auto_value
#( 当月ft - 最近p个月的ft最小值 ) /ft最小值
def Cnp(ft,p):
df=data.loc[:,ft+'1':ft+str(p)]
auto_value = (df[ft+'1'] - np.nanmin(df,axis = 1 )) /np.nanmin(df,axis = 1 )
return ft+'_cnp'+str(p),auto_value
#最近p个月取最大值的月份距现在的月份数
def Msx(ft,p):
df=data.loc[:,ft+'1':ft+str(p)]
df['_max'] = np.nanmax(df,axis = 1)
for i in range(1,p+1):
df[ft+str(i)] = list(df[ft+str(i)] == df['_max'])
del df['_max']
df_value = np.where(df==True,1,0)
auto_value=[]
for i in range(len(df_value)):
row_value=df_value[i,:]
indexs=1
for j in row_value:
if j == 1:
break
indexs+=1
auto_value.append(indexs)
return ft+'_msx'+str(p),auto_value
#最近p个月的均值/((p,2p)个月的ft均值)
def Rpp(ft,p):
df1=data.loc[:,ft+'1':ft+str(p)]
value1=np.nanmean(df1,axis = 1 )
df2=data.loc[:,ft+str(p):ft+str(2*p)]
value2=np.nanmean(df2,axis = 1 )
auto_value = value1/value2
return ft+'_rpp'+str(p),auto_value
#最近p个月的均值 - ((p,2p)个月的ft均值)
def Dpp(ft,p):
df1=data.loc[:,ft+'1':ft+str(p)]
value1=np.nanmean(df1,axis = 1 )
df2=data.loc[:,ft+str(p):ft+str(2*p)]
value2=np.nanmean(df2,axis = 1 )
auto_value = value1 - value2
return ft+'_dpp'+str(p),auto_value
#(最近p个月的ft最大值)/ (最近(p,2p)个月的ft最大值)
def Mpp(ft,p):
df1=data.loc[:,ft+'1':ft+str(p)]
value1=np.nanmax(df1,axis = 1 )
df2=data.loc[:,ft+str(p):ft+str(2*p)]
value2=np.nanmax(df2,axis = 1 )
auto_value = value1/value2
return ft+'_mpp'+str(p),auto_value
#(最近p个月的ft最小值)/ (最近(p,2p)个月的ft最小值)
def Npp(ft,p):
df1=data.loc[:,ft+'1':ft+str(p)]
value1=np.nanmin(df1,axis = 1 )
df2=data.loc[:,ft+str(p):ft+str(2*p)]
value2=np.nanmin(df2,axis = 1 )
auto_value = value1/value2
return ft+'_npp'+str(p),auto_value
- 将上面衍生的方法封装成函数
#定义批量调用双参数的函数
def auto_var2(feature,p):
#global data_new
try:
columns_name,values=Num(feature,p)
data_new[columns_name]=values
except:
print("Num PARSE ERROR",feature,p)
try:
columns_name,values=Nmz(feature,p)
data_new[columns_name]=values
except:
print("Nmz PARSE ERROR",feature,p)
try:
columns_name,values=Evr(feature,p)
data_new[columns_name]=values
except:
print("Evr PARSE ERROR",feature,p)
try:
columns_name,values=Avg(feature,p)
data_new[columns_name]=values
except:
print("Avg PARSE ERROR",feature,p)
try:
columns_name,values=Tot(feature,p)
data_new[columns_name]=values
except:
print("Tot PARSE ERROR",feature,p)
try:
columns_name,values=Tot2T(feature,p)
data_new[columns_name]=values
except:
print("Tot2T PARSE ERROR",feature,p)
try:
columns_name,values=Max(feature,p)
data_new[columns_name]=values
except:
print("Tot PARSE ERROR",feature,p)
try:
columns_name,values=Max(feature,p)
data_new[columns_name]=values
except:
print("Max PARSE ERROR",feature,p)
try:
columns_name,values=Min(feature,p)
data_new[columns_name]=values
except:
print("Min PARSE ERROR",feature,p)
try:
columns_name,values=Msg(feature,p)
data_new[columns_name]=values
except:
print("Msg PARSE ERROR",feature,p)
try:
columns_name,values=Msz(feature,p)
data_new[columns_name]=values
except:
print("Msz PARSE ERROR",feature,p)
try:
columns_name,values=Cav(feature,p)
data_new[columns_name]=values
except:
print("Cav PARSE ERROR",feature,p)
try:
columns_name,values=Cmn(feature,p)
data_new[columns_name]=values
except:
print("Cmn PARSE ERROR",feature,p)
try:
columns_name,values=Std(feature,p)
data_new[columns_name]=values
except:
print("Std PARSE ERROR",feature,p)
try:
columns_name,values=Cva(feature,p)
data_new[columns_name]=values
except:
print("Cva PARSE ERROR",feature,p)
try:
columns_name,values=Cmm(feature,p)
data_new[columns_name]=values
except:
print("Cmm PARSE ERROR",feature,p)
try:
columns_name,values=Cnm(feature,p)
data_new[columns_name]=values
except:
print("Cnm PARSE ERROR",feature,p)
try:
columns_name,values=Cxm(feature,p)
data_new[columns_name]=values
except:
print("Cxm PARSE ERROR",feature,p)
try:
columns_name,values=Cxp(feature,p)
data_new[columns_name]=values
except:
print("Cxp PARSE ERROR",feature,p)
try:
columns_name,values=Ran(feature,p)
data_new[columns_name]=values
except:
print("Ran PARSE ERROR",feature,p)
try:
columns_name,values=Nci(feature,p)
data_new[columns_name]=values
except:
print("Nci PARSE ERROR",feature,p)
try:
columns_name,values=Ncd(feature,p)
data_new[columns_name]=values
except:
print("Ncd PARSE ERROR",feature,p)
try:
columns_name,values=Ncn(feature,p)
data_new[columns_name]=values
except:
print("Ncn PARSE ERROR",feature,p)
try:
columns_name,values=Pdn(feature,p)
data_new[columns_name]=values
except:
print("Pdn PARSE ERROR",feature,p)
try:
columns_name,values=Cmx(feature,p)
data_new[columns_name]=values
except:
print("Cmx PARSE ERROR",feature,p)
try:
columns_name,values=Cmp(feature,p)
data_new[columns_name]=values
except:
print("Cmp PARSE ERROR",feature,p)
try:
columns_name,values=Cnp(feature,p)
data_new[columns_name]=values
except:
print("Cnp PARSE ERROR",feature,p)
try:
columns_name,values=Msx(feature,p)
data_new[columns_name]=values
except:
print("Msx PARSE ERROR",feature,p)
try:
columns_name,values=Nci(feature,p)
data_new[columns_name]=values
except:
print("Nci PARSE ERROR",feature,p)
try:
columns_name,values=Trm(feature,p)
data_new[columns_name]=values
except:
print("Trm PARSE ERROR",feature,p)
try:
columns_name,values=Bup(feature,p)
data_new[columns_name]=values
except:
print("Bup PARSE ERROR",feature,p)
try:
columns_name,values=Mai(feature,p)
data_new[columns_name]=values
except:
print("Mai PARSE ERROR",feature,p)
try:
columns_name,values=Mad(feature,p)
data_new[columns_name]=values
except:
print("Mad PARSE ERROR",feature,p)
try:
columns_name,values=Rpp(feature,p)
data_new[columns_name]=values
except:
print("Rpp PARSE ERROR",feature,p)
try:
columns_name,values=Dpp(feature,p)
data_new[columns_name]=values
except:
print("Dpp PARSE ERROR",feature,p)
try:
columns_name,values=Mpp(feature,p)
data_new[columns_name]=values
except:
print("Mpp PARSE ERROR",feature,p)
try:
columns_name,values=Npp(feature,p)
data_new[columns_name]=values
except:
print("Npp PARSE ERROR",feature,p)
return data_new.columns.size
- 对之前数据应用封装的函数
data_new = pd.DataFrame()
for p in range(1, 12):
for inv in ['ft', 'gt']:
auto_var2(inv, p)
data_new.columns.tolist()
- 上面这种无差别聚合方法进行聚合得到的结果,通常具有较高的共线性,但信息量并无明显增加
- 评分卡模型对模型的稳定性要求远高于其性能
- 在时间窗口为1年的场景下,p值会通过先验知识,人为选择3、6、12等,而不是遍历全部取值1~12
- 在后续特征筛选时,会根据变量的显著性、共线性等指标进行进一步筛选
2.3 特征组合
- 又叫特征交叉(Feature crossing),指不同特征之间基于常识、经验、数据挖掘技术进行分段组合实现特征构造,产生包含更多信息的新特征。
- 可以通过决策树模型,基于特定指标,贪心地搜索最优的特征组合形式。上一小结最后的案例为例
- 基于上述规则可以得出以下特征
x['n1'] = x.apply(lambda x:1 if x.amount_tot>48077.5 \
and sale_amount_cnt<=3.5 else 0)
x['n2'] = x.apply(lambda x:1 if x.amount_tot>48077.5 \
and sale_amount_cnt>3.5 else 0)
- 利用决策树实现特征的自动组合,可以有效降低建模人员的工作难度
- 最近一次(current) 和历史 (history)做对比
- current/history
- current-history
2.3 最近一次(current) 和历史 (history)做对比
3. 特征变换
3.1 分箱(离散化)
1. 简介
- 特征构造的过程中,对特征做分箱处理时必不可少的过程
- 分箱就是将连续变量离散化,合并成较少的状态
2. 作用
- 离散特征的增加和减少都很容易,易于模型的快速迭代;
- 稀疏向量内积乘法运算速度快,计算结果方便存储,容易扩展;
- 分箱(离散化)后的特征对异常数据有很强的鲁棒性
- 单变量分箱(离散化)为N个后,每个变量有单独的权重,相当于为模型引入了非线性,能够提升模型表达能力
- 分箱(离散化)后可以进行特征交叉,由M+N个变量变为M*N个变量,进一步引入非线性,提升表达能力;
- 分箱(离散化)后,模型会更稳定,如对年龄离散化,20-30为一个区间,不会因为年龄+1就变成一个新的特征。
- 特征离散化以后,可以将缺失作为独立的一类带入模型
2. 方法
- 卡方分箱、决策树分箱、等频分箱、聚类分箱
- 等频分箱: 每个箱体中样本数量一致,在样本不均衡时可能无法分箱
- 按数据的分布,均匀切分,每个箱体里的样本数基本一样
- 在样本少的时候泛化性较差
- 在样本不均衡时可能无法分箱
- 特征分析常用等频分箱
- 等距分箱: 一定可以分箱无法保证箱体样本数均匀
- 按数据的特征值的间距均匀切分,每个箱体的数值距离一样
- 一定可以分箱
- 无法保证箱体样本数均匀
- 信用分统计时常用等距分箱
2.1 卡方分箱
- 定义:使用卡方检验确定最优分箱阈值
- 操作:
- 将数据按等频或等距分箱后,计算卡方值,将卡方值较小的两个相邻箱体合并
- 使得不同箱体的好坏样本比例区别放大,容易获得高IV
- 卡方分箱是利用独立性检验来挑选箱划分节点的阈值。卡方分箱的过程可以拆分为初始化和合并两步
- 初始化:根据连续变量值大小进行排序,构建最初的离散化
- 合并:遍历相邻两项合并的卡方值,将卡方值最小的两组合并,不断重复直到满足分箱数目要求
- 将数据按等频或等距分箱后,计算卡方值,将卡方值较小的两个相邻箱体合并
合并坏人比例接近平均水平的箱体,留下比例差异大的箱体
2.2 应用
- 案例:使用toad库进行分箱处理,数据集使用germancredit
- Toad 是专为工业界模型开发设计的Python工具包,特别针对评分卡的开发
- Toad 的功能覆盖了建模全流程,从 EDA、特征工程、特征筛选 到 模型验证和评分卡转化
- Toad 的主要功能极大简化了建模中最重要最费时的流程,即特征筛选和分箱。
import pandas as pd
import numpy as np
import toad
data = pd.read_csv('data/germancredit.csv')
data.replace({'good':0,'bad':1},inplace=True)
print(data.shape) # 1000 data and 20 features
data.head()
- 数据字段说明
- Status of existing checking account(现有支票帐户的存款状态)
- Duration in month(持续月数)
- Credit history(信用历史记录)
- Purpose(申请目的)
- Credit amount(信用保证金额)
- Savings account/bonds(储蓄账户/债券金额)
- Present employment since(当前就业年限)
- Installment rate in percentage of disposable income(可支配收入占比)
- Personal status and gender(个人婚姻状态及性别)
- Other debtors / guarantors(其他债务人或担保人)
- Present residence since(当前居民年限)
- Property(财产)
- Age in years(年龄)
- Other installment plans (其他分期付款计划)
- Housing(房屋状况)
- Number of existing credits at this bank(在该银行已有的信用卡数)
- Job(工作性质)
- Number of people being liable to provide maintenance for(可提供维护人数)
- Telephone(是否留存电话)
- foreign worker(是否外国工人)
- creditability 数据标签
- toad 中的combiner类用来进行分箱处理
# 初始化一个combiner类
combiner = toad.transform.Combiner()
# 训练数据并指定分箱方法,其它参数可选 # min_samples: 每箱至少包含样本量,可以是数字或者占比
combiner.fit(data,y='creditability',method='chi',min_samples = 0.05)
# 以字典形式保存分箱结果
bins = combiner.export()
#查看分箱结果
print('duration.in.month:', bins['duration.in.month'])
- 通常使用双变量图(Bivar图 Bivariate graph)来评价分箱结果。注意,信贷风险分析中Bivar图,纵轴固定为负样本占比
- 使用bin_plot()画图对分箱进行调整
import matplotlib.pyplot as plt
%matplotlib inline
from toad.plot import bin_plot
c2 = toad.transform.Combiner()
c2.fit(data[['duration.in.month','creditability']],
y='creditability', method='chi',n_bins=7)
transformed = c2.transform(data[['duration.in.month','creditability']],labels=True)
#传给bin_plot的数据必须是分箱转化之后的
bin_plot(transformed,x='duration.in.month',target='creditability')
-
图中柱形图表示每一箱的占比,折线图表示每一箱的坏样本率。一般折线图要呈现出单调的趋势
-
可以通过调整箱数实现单调趋势
c2 = toad.transform.Combiner()
c2.fit(data[['duration.in.month','creditability']],
y='creditability', method='chi',n_bins=5) # 改成5箱
transformed = c2.transform(data[['duration.in.month','creditability']],labels=True)
#传给bin_plot的数据必须是分箱转化之后的
bin_plot(transformed,x='duration.in.month',target='creditability')
改成5箱,发现坏人的比例是单调的
- 其它分箱方法:聚类分箱(k-means), 决策树分箱,等频分箱,等距分箱
- 各种分箱方法对比
for method in ['chi', 'dt', 'quantile', 'step', 'kmeans']:
c2 = toad.transform.Combiner()
c2.fit(data[['duration.in.month','creditability']],
y='creditability', method=method, n_bins=5)
bin_plot(c2.transform(data[['duration.in.month','creditability']],labels=True),
x='duration.in.month',target='creditability')
-
从单调性和模型稳定性角度考虑一般使用卡方分箱
-
多值无序类别特征需要做encoding处理, 常见encoding方法:Onehot Encoding、Label Encoding、WOE Encoding
-
Onehot Encoding
-
Label Encoding 统计出同一个特征不同去值下的逾期率作为数值标签
- 婚姻状态 婚姻状态 统计出不同婚姻状态下的逾期率作为数值标签
未婚 dpd rate
已婚 dpd rate
离异 dpd rate
丧偶 dpd rate
缺点:数据量少的情况下,某些数据可能有偏差
- 婚姻状态 婚姻状态 统计出不同婚姻状态下的逾期率作为数值标签
-
WOE Encoding
WOE(Weight of Evidence) 反映单特征在好坏用户区分度的度量
W O E k = l o g ( p g o o d k / p b a d k ) WOE_k=log(p^k_{good}/p^k_{bad}) WOEk=log(pgoodk/pbadk) 好用户比例/坏用户比例
- 使用toad计算woe
from sklearn.model_selection import train_test_split
X_train,X_test,Y_train,Y_test =
train_test_split(data.drop('creditability',axis=1),data['creditability'],test_size=0.25,random_state=450)
data_train = pd.concat([X_train,Y_train],axis=1)
#增加一列区分训练/测试的特征
data_train['type'] = 'train'
data_test = pd.concat([X_test,Y_test],axis=1)
data_test['type'] = 'test'
#设置分箱边界
adj_bin = {'duration.in.month': [9, 12, 18, 33]}
c2 = toad.transform.Combiner()
c2.set_rules(adj_bin)
data_ = pd.concat([data_train,data_test],axis = 0)
#分箱
temp_data = c2.transform(data_[['duration.in.month','creditability','type']])
#绘制badrate_plot图
from toad.plot import badrate_plot, proportion_plot
badrate_plot(temp_data, target = 'creditability', x = 'type', by = 'duration.in.month')
#绘制每一箱占比情况图
proportion_plot(temp_data['duration.in.month'])
- 上面第一张图中的第一箱和第二箱的bad_rate存在倒挂,说明bad_rate不单调,需要调整。可以将第一箱和第二箱进行合并
# 假定将第一箱、第二箱合并
adj_bin = {'duration.in.month': [9,18,33]}
c2.set_rules(adj_bin)
temp_data = c2.transform(data_[['duration.in.month','creditability','type']])
badrate_plot(temp_data, target = 'creditability', x = 'type', by = 'duration.in.month')
#将特征的值转化为分箱的箱号。
binned_data = c2.transform(data_train)
#计算WOE
transer = toad.transform.WOETransformer()
#对WOE的值进行转化,映射到原数据集上。对训练集用fit_transform,测试集用transform.
data_tr_woe = transer.fit_transform(binned_data, binned_data['creditability'], exclude=
['creditability','type'])
data_tr_woe.head()
1 划分数据集
2 在train test分别计算不同分箱中badrate,发现5个分箱在训练集和测试集上趋势不一致
3 调整分箱成4个
- WOE理解:当前组中好用户和坏用户的比值与所有样本中这个比值的差异。差异通过对这两个比值取对数来表示
- WOE越大,差异越大,这个分组里的好用户的可能性就越大
- WOE越小,差异越小,这个分组里的好用户的可能性也就越小。
- 分箱结果对WOE结果有直接影响,分箱不同,WOE映射值也会有很大的不同
- 箱的总数在5~10箱(可以适当调整,通常不超过10箱)
- 并且将每一箱之间的负样本占比差值尽可能大作为箱合并的基本原则
- 每一箱的样本量不能小于整体样本的5%,原则是每一箱的频数需要具有统计意义
- 三种encoding的利弊
优势 | 劣势 | |
---|---|---|
Onehot Encoding | 简单易处理、稳定、无需归一化、不依赖历史数据 | 数据过于稀疏 |
Label Encoding | 区分效果好,维度小 | 需统计历史数据、不稳定、需要归一化 |
WOE Encoding | 区分效果好,维度小,不需要归一化 | 需统计历史数据、不稳定 |
- 多值有序类别型特征
- 学历:高中以下,大专,本科,硕士,博士
- 一定程度上学历高低能直接对应用户的信用风险,可以当做有序特征
- 可以把多值有序特征转换为1,2,3…的数值
- 高中以下 → 1, 大专 → 2 ,本科 → 3,硕士→4,博士→5
4. 用户时间序列缺失值处理
- 用户时间序列缺失值处理
- 优先考虑补零:大多数特征都是计数,缺失用0补 充
- 用户没有历史购物记录: max_gmv min_gmv 都可以用0补充
- 用0填充缺失值带来的问题
- cur/history_avg: 0/0 cur/history_avg:1/0
- 根据风险趋势填补缺失值 (违约概率大小 无历史购物记录违约概率>有一单历史购物记录>有两单)
用户没有历史购物记录 cur/history_avg : 0/0? 可以填充-2
用户有一单历史购物记录 cur/history_avg : 1/0? 可以填充-1
用户有两单历史购物记录 cur/history_avg : 1/1 可以计算出>0的值
- 用户最后一次逾期距今天数,如果是白户如何填补缺失值?
- 如果缺失值比较多的时候,考虑单独做成特征
- 举例:用户授权GPS序列特征 gps_count_last_3month
- 缺失意味着用户未授权GPS权限
- 缺失有明显业务含义,可以填补业务默认值
- 授信额度(用初始额度)
缺失值 | 处理 |
---|---|
一般计数类特征 | 优先考虑用0填充 |
有风险趋势 | 按风险趋势填补 |
缺失数值过多 | 考虑新增是否缺失的特征列 |
有业务含义 | 填补业务默认值 |
5. 时间序列数据的未来信息
5.1 简介
- 以借贷2发生的时间为观测点,下表中的未来信息会把大量退货行为的用户认为是坏客户,但上线后效果会变差
5.2 特征构建时的补救方法
- 对未来信息窗口外的订单计算有效单的特征 net order,nmv对未来信息窗口内订单只计算一般特征 order,GMV
- 对未来信息窗口内订单只计算一般特征 order,GMV
- 历史信贷特征也非常容易出现未来信息
- 举例:
- 信用卡 每月1日为账单日,每月10日为还款日,次月10日左右为M1
- 举例:
- 在上图所示的截面时间(如3月5日)是看不到2月账单的逾期DPD30的情况的
- 但如果数据库没有快照表会导致我们可以拿到2月账单的DPD30情况
- 解决方案跟上面例子一样,分区间讨论,可以把账单分成3类
- 当前未出账账单
- 最后一个已出账账单
- 其他已出账账单 (只有这个特征可以构建逾期类特征)
- 未来信息处理小结
- 及时增加快照表
- 没有快照表的情况下,将数据区分为是否有未来信息的区间,分别进行特征构造
3. 用户关联特征
3.1 评价一个没有内部数据的新客
- 使用外部第三方数据
- 把新用户关联到内部用户,使用关联到的老客信息评估
3.2用户特征关联
- 可以考虑用倒排表做关联
- 用户→[特征1,特征2,特征3…]
- 特征→[用户1, 用户2,用户3…]
3. 应用
举例:用户所在地区的统计特征
- 将用户申请时的GPS转化为geohash位置块
- geohash:基本原理是将地球理解为一个二维平面,将平面递归分解成更小的子块,每个子块在一定经纬度范围内拥有相同
的编码
- geohash:基本原理是将地球理解为一个二维平面,将平面递归分解成更小的子块,每个子块在一定经纬度范围内拥有相同
- 对每个大小合适的位置块,统计申请时点GPS在该位置块的人的信用分
- 当新申请的人,查询其所在的位置块的平均信用分作为GPS倒排表特征
- 倒排表的组成:关键主键+统计指标
- 关键主键:新用户通过什么数据和平台存量用户发生关联
- 统计指标:使用存量用户的什么特征去评估这个新客户
- 常见统计指标
- 常见关联主键
- 信贷业务的特征要求:
- 逻辑简单
- 容易构造
- 容易排查错误
- 有强业务解释性
- 构造特征要从两个维度看数据:归纳+演绎
- 归纳:从大量数据的结果总结出规律(相关关系)
- 演绎:从假设推导出必然的结果(因果关系)