机器学习与金融风控项目篇-day03-业务规则挖掘与特征构建-特征衍生
一. 业务规则(特征)挖掘
规则挖掘简介
两种常见的风险规避手段
-
AI模型
-
规则
如何使用规则进行风控
-
使用一系列判断逻辑对客户群体进行区分,不同群体逾期风险有显著差别
-
举例:多头借贷数量是否超过一定数量
-
-
如果一条规则将用户划分到高风险组,则直接拒绝,如果划分到低风险组则进入到下一规则
规则和AI模型的优缺点
-
规则,可以快速使用,便于业务人员理解,但判断相对简单粗暴一些,单一维度不满足条件直接拒绝
-
AI模型,开发周期长,对比使用规则更复杂,但更灵活,用于对风控精度要求高的场景
可以通过AI模型辅助建立规则引擎,决策树很适合规则挖掘的场景
规则挖掘案例
案例背景
-
某互联网公司拥有多个业务板块,每个板块下都有专门的贷款产品
-
外卖平台业务的骑手可以向平台申请“骑手贷”
-
电商平台业务的商户可以申请“网商贷”
-
网约车业务的司机可以向平台申请“司机贷”
-
-
公司有多个类似的场景,共用相同的规则引擎及申请评分卡,贷款人都是该公司的兼职人员
-
近期发现,“司机贷”的逾期率较高
-
整个金融板块30天逾期率为1.5%
-
“司机贷”产品的30天逾期超过了5%
-
-
给司机增加额外的规则减少违约率
期望解决方案
-
现有的风控架构趋于稳定
-
希望快速开发快速上线,解决问题
-
尽量不使用复杂的方法
-
考虑使用现有数据挖掘出合适的业务规则
-
数据字典
变量类型 | 基础变量名 | 释义 |
---|---|---|
数值型 | oil_amount | 加油升数 |
discount_amount | 折扣金额 | |
sale_amount | 促销金额 | |
amount | 总金额 | |
pay_amount | 实际支付金额 | |
coupon_amount | 优惠券金额 | |
payment_coupon_amount | 支付优惠券金额 | |
分类型 | channel_code | 渠道 |
oil_code | 油品品类(规格) | |
scene | 场景 | |
source_app | 来源端口(1货车帮、2微信) | |
call_source | 订单来源(1:中化扫描枪 2:pos 3:找油网 4:油掌柜5:司机自助加油 6 油站线) | |
class_new | 用户评级 | |
日期时间 | create_dt | 账户创建时间 |
oil_actv_dt | 放款时间 | |
标签 | bad_ind | 正负样本标记 |
代码演示
特征构造(少->多)
原始数据的特征太少,考虑在原始特征基础上衍生出一些新的特征来,将特征分成三类分别处理
-
数值类型变量
:按照id分组后,采用多种方式聚合
,衍生新特征 -
分类类型变量
,按照id分组后,聚合查询条目数量
,衍生新特征 -
其它:日期时间类型,是否违约(标签),用户评级等不做特征衍生处理
# 原始列(不用作特征构建) org_list = ['uid','create_dt','oil_actv_dt','class_new','bad_ind'] # 特征工程: 挖掘 -> 构造新的列 -> 特征构造(衍生) # 衍生就是各种聚合, 不用关心有用否, 后期会进行特征筛选 # 聚合列(正常可也使用原有的列, 比较构造出来的列, 看那些影响大) agg_list = ['oil_amount','discount_amount','sale_amount','amount','pay_amount','coupon_amount','payment_coupon_amount'] # 类别型特征, 用于特征构建(衍生), 衍生方式是统计数量 count_list = ['channel_code','oil_code','scene','source_app','call_source'] # 上述方式是通用的做法, 实际公司有自己的特征构造方式, 直接用
数据预处理
填充缺失值
# 用放款日期填充创建账户日期 # 对create_dt列进行时间哎填充, 可以使用oil_actv_dt来填充create_dt缺失值 # 自定函数时间填充 def fill_time(create_dt, oil_actv_dt): if str(create_dt) == 'NaT': create_dt = oil_actv_dt return create_dt # 填充create_dt列 df2['create_dt'] = df2.apply(lambda line:fill_time(line['create_dt'], line['oil_actv_dt']), axis=1) df2
计算天数
# 计算创建账户日期到放款日期的天数 df2['dtn'] = (df2['oil_actv_dt'] - df2['create_dt']).apply(lambda x:x.days) # df2['dtn'] = (df2['oil_actv_dt'] - df2['create_dt']).dt.days # 效果同上 df2.tail(100) # 过滤近半年的数据 df = df2[df2['dtn'] < 180] df
分组去重
base = df[org_list] base['dtn'] = df['dtn'] base = base.sort_values(['uid','create_dt'],ascending = False) # 按照uid和create_dt降序排列(方便保留最近的数据) base = base.drop_duplicates(['uid'],keep = 'first') # 去除同一用户的不同标签(同一用户只有一条数据), 防止模型训练效果不佳 base.shape
特征衍生
数值衍生
# 数值型衍生 gn = pd.DataFrame() for i in agg_list: tp = df.groupby('uid').apply(lambda df:len(df[i])).reset_index() tp.columns = ['uid',i + '_cnt'] if gn.empty: gn = tp else: gn = pd.merge(gn,tp,on = 'uid',how = 'left') #求历史特征值大于0的个数 tp = df.groupby('uid').apply(lambda df:np.where(df[i]>0,1,0).sum()).reset_index() tp.columns = ['uid',i + '_num'] if gn.empty: gn = tp else: gn = pd.merge(gn,tp,on = 'uid',how = 'left') #求和 tp = df.groupby('uid').apply(lambda df:np.nansum(df[i])).reset_index() tp.columns = ['uid',i + '_tot'] if gn.empty: gn = tp else: gn = pd.merge(gn,tp,on = 'uid',how = 'left') #求平均值 tp = df.groupby('uid').apply(lambda df:np.nanmean(df[i])).reset_index() tp.columns = ['uid',i + '_avg'] if gn.empty: gn = tp else: gn = pd.merge(gn,tp,on = 'uid',how = 'left') #求最大值 tp = df.groupby('uid').apply(lambda df:np.nanmax(df[i])).reset_index() tp.columns = ['uid',i + '_max'] if gn.empty: gn = tp else: gn = pd.merge(gn,tp,on = 'uid',how = 'left') #求最小值 tp = df.groupby('uid').apply(lambda df:np.nanmin(df[i])).reset_index() tp.columns = ['uid',i + '_min'] if gn.empty: gn = tp else: gn = pd.merge(gn,tp,on = 'uid',how = 'left') #求方差 tp = df.groupby('uid').apply(lambda df:np.nanvar(df[i])).reset_index() tp.columns = ['uid',i + '_var'] if gn.empty: gn = tp else: gn = pd.merge(gn,tp,on = 'uid',how = 'left') #求极差 tp = df.groupby('uid').apply(lambda df:np.nanmax(df[i]) -np.nanmin(df[i]) ).reset_index() tp.columns = ['uid',i + '_ran'] if gn.empty: gn = tp else: gn = pd.merge(gn,tp,on = 'uid',how = 'left')
类别衍生
# 类别衍生 gc = pd.DataFrame() for i in count_list: tp = df.groupby('uid').apply(lambda df: len(set(df[i]))).reset_index() tp.columns = ['uid',i + '_dstc'] if gc.empty: gc = tp else: gc = pd.merge(gc,tp,on = 'uid',how = 'left')
拼接衍生后字段
拼接
# 拼接base和gn fn = pd.merge(base,gn,on = 'uid',how = 'left') # 拼接fn和gc fn = pd.merge(fn,gc,on = 'uid',how = 'left') fn.shape
填充空值
# 填充拼接后的控制 fn.isna().sum() fn.fillna(0, inplace=True)
模型训练
模型训练
from sklearn.tree import DecisionTreeRegressor x = fn.drop(['uid','oil_actv_dt','create_dt','bad_ind','class_new'],axis = 1) y = fn['bad_ind'] dtree = DecisionTreeRegressor(max_depth = 2,min_samples_leaf = 500,min_samples_split = 5000) dtree.fit(x,y)
结果可视化
# 结果可视化 from sklearn.tree import plot_tree import matplotlib.pyplot as plt plot_tree(dtree,feature_names=x.columns, class_names=['bad_ind'], filled=True, rounded=True) plt.show()
day03-特征工程与特征构造
一. 数据准备
略
有专业的人员来做
数据的内在逻辑
数据库建模:
用于业务系统, 后端开发工程师
数仓建模:
用于构建数仓, 是数仓工程师来建模
如果没有, 可以使用pandas/sql来做
样本设计和特征框架
定义观察期样本
-
确定观察期(定X时间切面)和表现期(定Y的标签)
-
确认样本数目是否合理
数据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)
梳理特征框架
RFM生成新特征
举例 行为评分卡中的用户账单还款特征
用户账单关键信息:时间,金额,还款,额度
总结
数据准备的逻辑
数据来源(征信数据)
梳理数据内在逻辑(关联关系)
查看数据分布(EDA)
二. 特征构造
静态信息特征和时间截面特征
用户静态信息特征
用户的基本信息(半年内不会变化)
如: 姓名, 性别,年龄, 籍贯, 政治面貌, 国籍
用户时间截面特征
未来信息当前时间截面之后的数据(时间戳
)
时间截面数据在取数的时候要小心,避免使用未来信息
产生未来信息最直接的原因:缺少快照表
快照表,每天照个相片 23:00点 把当天的数据 备份一份
-
快照表只会保存当天最终的状态
日志表,每一次操作都记一次, 不会update,只有insert 操作一次记录一次
-
每一次操作都会记录下来
金融相关数据原则上都需要快照表记录所有痕迹(额度变化情况,多次申请的通过和拒绝情况...)
缺少快照表的可能原因
-
快照表消耗资源比较大,为了性能不做
-
原有数据表设计人员疏忽,没做
-
借用其他业务数据(如电商)做信贷
举例
首次借贷 二次借贷 爬虫授权 三次借贷
——————————————————————→
用户 | 借款 | 授权爬虫 | 逾期 |
---|---|---|---|
u1 | l11 | N | 0 |
u1 | l12 | N | 0 |
u1 | l13 | Y | 0 |
u2 | l21 | N | 0 |
u2 | l22 | N | 0 |
u2 | l23 | Y | 1 |
u3 | l11 | N | 0 |
u3 | l12 | N | 0 |
u3 | l13 | Y | 0 |
实际存储
用户 | 授权 |
---|---|
u1 | Y |
u2 | Y |
u3 | Y |
用户 | 借款 | 逾期 |
---|---|---|
u1 | l11 | 0 |
u1 | l12 | 0 |
u1 | l13 | 0 |
u2 | l21 | 0 |
u2 | l22 | 0 |
u2 | l23 | 1 |
u3 | l11 | 0 |
u3 | l12 | 0 |
u3 | l13 | 0 |
join 结果
用户 | 借款 | 授权爬虫 | 逾期 |
---|---|---|---|
u1 | l11 | Y | 0 |
u1 | l12 | Y | 0 |
u1 | l13 | Y | 0 |
u2 | l21 | Y | 0 |
u2 | l22 | Y | 0 |
u2 | l23 | Y | 1 |
u3 | l11 | Y | 0 |
u3 | l12 | Y | 0 |
u3 | l13 | Y | 0 |
解决方案:加入快照的存储
用户 | 授权 | 时间 |
---|---|---|
u1 | Y | t3 |
u2 | Y | t3 |
u3 | Y | t3 |
时间序列特征
用户时间序列特征
从观察点往前回溯一段时间的数据
时间序列: 一段时间内的数据
时间序列特征衍生
原理
特征聚合:将单个特征的多个时间节点取值进行聚合。特征聚合是传统评分卡建模的主要特征构造方法。
举例,计算每个用户的额度使用率,记为特征ft,按照时间轴以月份为切片展开
-
申请前30天内的额度使用率ft1
-
申请前30天至60天内的额度使用率ft2
-
申请前60天至90天内的额度使用率ft3
-
申请前330天至360天内的额度使用率ft12
-
得到一个用户的12个特征
注意事项
在机器学习阶段的模型训练中, 不提倡直接做特征 而是先跑baseline模型 当baseline模型达不到逾期, 可以做一些和特征相关的操作 10个特征 AUC 0.61 1. 筛选特征 2. 特征衍生
方式1: 聚合(数值)
import pandas as pd import numpy as np data = pd.read_excel('../data/textdata.xlsx') data.head()
显示结果
customer_id ft1 ft2 ft3 ft4 ft5 ft6 ft7 ft8 ft9 ... gt3 gt4 gt5 gt6 gt7 gt8 gt9 gt10 gt11 gt12 0 111 9 11.0 12 13 18 10 12 NaN NaN ... 10 0 18 10 12 NaN NaN NaN NaN NaN 1 112 11 -11.0 10 10 13 13 10 NaN NaN ... 10 10 13 13 10 NaN NaN NaN NaN NaN 2 113 0 11.0 10 12 6 10 0 25.0 10.0 ... 10 12 6 10 0 25.0 10.0 NaN NaN NaN 3 114 -7 -1.0 9 8 7 0 -19 10.0 11.0 ... 10 10 12 0 -19 10.0 11.0 NaN NaN NaN 4 115 11 NaN 6 10 0 17 19 10.0 30.0 ... 6 10 0 17 19 10.0 30.0 15.0 NaN NaN
可以根据这个时间序列进行基于经验的人工特征衍生,例如计算最近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=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)
上面这种无差别聚合方法进行聚合得到的结果,通常具有较高的共线性,但信息量并无明显增加,影响模型的鲁棒性和稳定性。
评分卡模型对模型的稳定性要求远高于其性能
-
在时间窗口为1年的场景下,p值会通过先验知识,人为选择3、6、12等,而不是遍历全部取值1~12
-
在后续特征筛选时,会根据变量的显著性、共线性等指标进行进一步筛选
方式2: 当前和历史对比
最近一次(current) 和历史 (history)做对比
-
current/history
-
current-history
方式3: 统计(类别)
统计数量
用户时间序列缺失值处理
用户时间序列缺失值处理
-
优先考虑补零:大多数特征都是计数,缺失用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填充 |
有风险趋势 | 按风险趋势填补 |
缺失数值过多 | 考虑新增是否缺失的特征列 |
有业务含义 | 填补业务默认值 |
时间序列数据未来信息
以借贷2发生的时间为观测点,下表中的未来信息会把大量退货行为的用户认为是坏客户,但上线后效果会变差
特征构建补救方法
-
对未来信息窗口外的订单计算有效单的特征 net order,NMV
NMV:Net Merchandise Value(商品净值)
-
对未来信息窗口内订单只计算一般特征 order,GMV
GMV:Gross Merchandise Volume(商品交易总值)
历史信贷特征也非常容易出现未来信息
举例:
信用卡 每月1日为账单日,每月10日为还款日,次月10日左右为M1
在上图所示的截面时间(如3月5日)是看不到2月账单的逾期DPD30的情况的
但如果数据库没有快照表会导致我们可以拿到2月账单的DPD30情况
解决方案跟上面例子一样,分区间讨论,可以把账单分成3类
-
当前未出账账单
-
最后一个已出账账单
-
其他已出账账单 (只有这个特征可以构建逾期类特征)
未来信息处理小结
-
及时增加快照表
-
没有快照表的情况下,将数据区分为是否有未来信息的区间,分别进行特征构造
特征变换
分箱(离散化)
概念
-
特征构造的过程中,对特征做分箱处理时必不可少的过程
-
分箱就是将连续变量离散化,合并成较少的状态
分箱作用
-
离散特征的增加和减少都很容易,易于模型的快速迭代
-
分箱(离散化)后的特征对异常数据有很强的
鲁棒性
-
单变量分箱(离散化)为N个后,每个变量有单独的权重,
相当于为模型引入了非线性,能够提升模型表达能力
-
分箱(离散化)后可以进行特征交叉,由M+N个变量变为M*N个变量,进一步引入非线性,提升表达能
-
分箱(离散化)后,模型会更稳定
,如对年龄离散化,20-30为一个区间,不会因为年龄+1就变成一个新的特征 -
特征离散化以后,可以将缺失作为独立的一类带入模型
分箱方法
常用分箱方法:
卡方分箱
, 决策树分箱, 等频分箱, 聚类分箱
等频分箱
-
按数据的分布,均匀切分,每个箱体里的样本数基本一样
-
在样本少的时候泛化性较差
-
在样本不均衡时可能无法分箱
等距分箱
-
按数据的特征值的间距均匀切分,每个箱体的数值距离一样
-
一定可以分箱
-
无法保证箱体样本数均匀
★卡方分箱
使用卡方检验确定最优分箱阈值
-
将数据按等频或等距分箱后,计算卡方值,将卡方值较小的两个相邻箱体合并
使得不同箱体的好坏样本比例区别放大,容易获得高IV
-
卡方分箱是利用独立性检验来挑选箱划分节点的阈值。卡方分箱的过程可以拆分为初始化和合并两步
-
初始化:根据连续变量值大小进行排序,构建最初的离散化
-
合并:遍历相邻两项合并的卡方值,将卡方值最小的两组合并,不断重复直到满足分箱数目要求
-
[22-35] | (35-45] | (45-55] | (55-65] | 总计 | |
---|---|---|---|---|---|
good | 3 | 2 | 2 | 1 | 8 |
bad | 1 | 2 | 2 | 3 | 8 |
p | 50% | ||||
p(good+bad) | 2 | 2 | 2 | 2 | - |
chi2 | (1-2)^2/2=1/2 | (2-2)^2/2=0 | (2-2)^2/2=0 | (3-2)^2/2=1/2 | - |
合并坏人比例接近平均水平的箱体,留下比例差异大的箱体
案例-卡方分箱
数据集使用germancredit
-
Toad 是专为工业界模型开发设计的Python工具包,特别针对评分卡的开发
-
Toad 的功能覆盖了建模全流程,从EDA、特征工程、特征筛选到模型验证和评分卡转化
-
Toad 的主要功能极大简化了建模中最重要最费时的流程,即特征筛选和分箱。
第一次使用toad,需要安装:
pip install toad==0.1.0 -i https://pypi.tuna.tsinghua.edu.cn/simple/
案例代码实现:
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()
显示结果
(1000, 21)
status.of.existing.checking.account duration.in.month credit.history purpose credit.amount savings.account.and.bonds present.employment.since installment.rate.in.percentage.of.disposable.income personal.status.and.sex other.debtors.or.guarantors ... 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 0 ... < 0 DM 6 critical account/ other credits existing (not ... radio/television 1169 unknown/ no savings account ... >= 7 years 4 male : single none ... real estate 67 none own 2 skilled employee / official 1 yes, registered under the customers name yes 0 1 0 <= ... < 200 DM 48 existing credits paid back duly till now radio/television 5951 ... < 100 DM 1 <= ... < 4 years 2 female : divorced/separated/married none ... real estate 22 none own 1 skilled employee / official 1 none yes 1 2 no checking account 12 critical account/ other credits existing (not ... education 2096 ... < 100 DM 4 <= ... < 7 years 2 male : single none ... real estate 49 none own 1 unskilled - resident 2 none yes 0 3 ... < 0 DM 42 existing credits paid back duly till now furniture/equipment 7882 ... < 100 DM 4 <= ... < 7 years 2 male : single guarantor ... building society savings agreement/ life insur... 45 none for free 1 skilled employee / official 2 none yes 0 4 ... < 0 DM 24 delay in paying off in the past car (new) 4870 ... < 100 DM 1 <= ... < 4 years 3 male : single none ... unknown / no property 53 none for free 2 skilled employee / official 2 none yes 1
数据字段说明
-
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
数据标签