1.3 规则挖掘方案
在风控领域有两种常见的风险规避手段:规则挖掘和人工智能模型。通常规则挖掘期望使用一系列判断逻辑对客户群体进行区分,使得不同分区中客户的期望风险有显著差异。如判断用户的多头借贷数量是否超过10个,超过则认为风险较高,不予通过;否则认为用户在这一维度上风险较低,进入下一条规则。人工智能模型使用机器学习手段预测用户未来违约的风险。它相比于规则引擎更加灵活,不会根据某维度信息直接将用户拒绝,但也更加复杂。通常人工智能模型从建立到部署上线需要经历相当长的一段时间。在实际应用中,风控人员更期望使用规则挖掘法,找到有区分度的规则,从而迅速解决问题。人工智能模型则更常用于对精度要求较高的场景。
与一般的策略分析方法不同,本节要介绍的方法主要通过特征工程与决策树模型相结合,利用均方差最小化原理实现规则的自动挖掘。常见的决策树(Decision Tree)算法有ID3、C4.5、CART分类树、CART回归树等。本节使用CART回归树进行规则引擎的制作。
1.案例背景
假设某互联网公司旗下拥有多个服务板块,每个板块下都有专门的贷款产品,比如旗下外卖平台的骑手可以向平台申请“骑手贷”,旗下电商平台的商户可以申请“商品贷”,旗下电商平台的用户购买商品时可以申请“分期贷”,等等。
该公司有10个类似的场景,共用相同的规则引擎及申请评分卡,贷款人都是该公司的兼职人员。最近公司发现,“骑手贷”的逾期率明显比其他场景要高很多,整个金融板块30天逾期率为1.5%,而“骑手贷”产品的30天逾期达到了5%。
考虑到现有的风控架构趋于稳定,上线排期及开发速度都有要求,如果想解决当前遇到的问题,且尽量不使用复杂的方法,最优的解决方案一定是既简单效果又好的。
2.数据预览
本次建模用到的基础变量字典如图1-6所示。
图1-6 变量释义
3.加载本次案例的包
加载本次案例包的方法如下:
1. import pandas as pd 2. import numpy as np 3. import os 4. #为画图指定路径 5. os.environ["PATH"] += os.pathsep + 'C:/Program Files (x86)/Graphviz2.38/bin/' 6. #读取数据 7. data = pd.read_excel('./data/data_for_tree.xlsx') 8. data.head()
数据预览如图1-7所示。
图1-7 数据集部分预览
由上图可以看到,用户的ID是有重复的。数据集中包含了用户多个切片下的数据表现,其中,bad_ind是用户的标签,1表示逾期用户,0表示未逾期用户。接下来通过特征工程对用户的数据进行聚合,得到将每个人用一行表示的数据。
4.特征分类
在常规的特征工程中,通常对连续型变量进行聚合处理,对离散型变量进行特征编码。离散型变量的处理方式在第2章进行系统介绍,本节只统计每个样本离散型变量的取值个数。首先将连续型变量名字存入agg_lst列表中,将离散型变量放入dstc_lst列表中。
1. org_lst = ['uid', 'create_dt', 'oil_actv_dt', 'class_new', 'bad_ind'] 2. agg_lst = ['oil_amount', 'discount_amount', 'sale_amount', 'amount', 3. 'pay_amount', 'coupon_amount', 'payment_coupon_amount'] 4. dstc_lst = ['channel_code', 'oil_code', 'scene', 'source_app', 'call_source'] 5. 6. df = data[org_lst].copy() 7. df[agg_lst] = data[agg_lst].copy() 8. df[dstc_lst] = data[dstc_lst].copy() 9. 10. base = df[org_lst].copy() 11. base = base.drop_duplicates(['uid'], keep='first')
5.对变量进行加工衍生
对连续统计型变量进行函数聚合。聚合的方法包括对历史特征值计数、求历史特征值大于0的个数、求和、求均值、求最大值、求最小值、求方差、求极差、求变异系数。
1. gn = pd.DataFrame() 2. for i in agg_lst: 3. # 计算个数 4. tp = pd.DataFrame(df.groupby('uid').apply( 5. lambda df:len(df[i])).reset_index()) 6. tp.columns = ['uid', i + '_cnt'] 7. if gn.empty == True: 8. gn = tp 9. else: 10. gn = pd.merge(gn, tp, on = 'uid', how = 'left') 11. # 求历史特征值大于0的个数 12. tp = pd.DataFrame(df.groupby('uid').apply( 13. lambda df:np.where(df[i]>0,1,0).sum()).reset_index()) 14. tp.columns = ['uid', i + '_num'] 15. if gn.empty == True: 16. gn = tp 17. else: 18. gn = pd.merge(gn, tp, on = 'uid', how = 'left') 19. # 对历史数据求和 20. tp = pd.DataFrame(df.groupby('uid').apply( 21. lambda df:np.nansum(df[i])).reset_index()) 22. tp.columns = ['uid', i + '_tot'] 23. if gn.empty == True: 24. gn = tp 25. else: 26. gn = pd.merge(gn, tp, on = 'uid', how = 'left') 27. # 对历史数据求均值 28. tp = pd.DataFrame(df.groupby('uid').apply( 29. lambda df:np.nanmean(df[i])).reset_index()) 30. tp.columns = ['uid', i + '_avg'] 31. if gn.empty == True: 32. gn = tp 33. else: 34. gn = pd.merge(gn, tp, on = 'uid', how = 'left') 35. # 对历史数据求最大值 36. tp = pd.DataFrame(df.groupby('uid').apply( 37. lambda df:np.nanmax(df[i])).reset_index()) 38. tp.columns = ['uid', i + '_max'] 39. if gn.empty == True: 40. gn = tp 41. else: 42. gn = pd.merge(gn, tp, on = 'uid', how = 'left') 43. # 对历史数据求最小值 44. tp = pd.DataFrame(df.groupby('uid').apply( 45. lambda df:np.nanmin(df[i])).reset_index()) 46. tp.columns = ['uid', i + '_min'] 47. if gn.empty == True: 48. gn = tp 49. else: 50. gn = pd.merge(gn, tp, on = 'uid', how = 'left') 51. # 对历史数据求方差 52. tp = pd.DataFrame(df.groupby('uid').apply( 53. lambda df:np.nanvar(df[i])).reset_index()) 54. tp.columns = ['uid', i + '_var'] 55. if gn.empty == True: 56. gn = tp 57. else: 58. gn = pd.merge(gn, tp, on = 'uid', how = 'left') 59. # 对历史数据求极差 60. tp = pd.DataFrame(df.groupby('uid').apply( 61. lambda df:np.nanmax(df[i])-np.nanmin(df[i])).reset_index()) 62. tp.columns = ['uid', i + '_ran'] 63. if gn.empty == True: 64. gn = tp 65. else: 66. gn = pd.merge(gn, tp, on = 'uid', how = 'left') 67. # 对历史数据求变异系数,为防止除数为0,利用0.01进行平滑 68. tp = pd.DataFrame(df.groupby('uid').apply( 69. lambda df: np.nanmean(df[i])/(np.nanvar(df[i]) \ 70. +0.01)).reset_index()) 71. tp.columns = ['uid', i + '_cva'] 72. if gn.empty == True: 73. gn = tp 74. else: 75. gn = pd.merge(gn, tp, on = 'uid', how = 'left')
6.对离散变量的历史取值进行计数
例如计算每个骑手在多少个平台上接过单。
1. gc = pd.DataFrame() 2. for i in dstc_lst: 3. tp = pd.DataFrame(df.groupby('uid').apply( 4. lambda df: len(set(df[i]))).reset_index()) 5. tp.columns = ['uid', i + '_dstc'] 6. if gc.empty == True: 7. gc = tp 8. else: 9. gc = pd.merge(gc, tp, on = 'uid', how = 'left')
7.合并衍生数据和基础用户信息
将两部分衍生数据和基础用户信息合并在一起,base中主要是用户的ID和逾期标签。
1. fn = base.merge(gn, on='uid').merge(gc, on='uid') 2. fn = pd.merge(fn, gc, on='uid') 3. fn=fn.fiuna(0) 4. fn=fn.shape
输出结果为:
(11307, 78)
8.使用CART回归树进行规则挖掘
调用sklearn包中的决策树模型对衍生特征进行拟合,得到两层的CART回归树。CART树是一种二叉树,在每一层分化的时候,会遍历每一个特征的每一个取值进行二分,并计算划分后叶节点上的均方差,然后将均方差最小的特征和特征值作为当前节点的分化依据。使用CART回归树进行规则挖掘的主要原因是,在二分类任务下,决策树叶节点的输出是当前节点标签的均值,这与负样本占比(bad rate)的意义相同。授信通过群体中有更小的负样本占比,这是风控场景下的主要优化目标,因此使用CART回归树更符合当前的业务要求。
注意,参数max_depth控制树的深度为2层,考虑到逻辑上的复杂程度,在生成规则引擎时一般不适用太深的树。参数min_samples_leaf控制每一个叶节点上的样本个数,由于一个节点上的样本过少,不具有统计意义,有非常大的可能产生过拟合,故在这里设置最小值为500。参数min_samples_split控制父节点分化的最小样本个数为5000个,当节点样本数量少于5000时,则不再分化。
1. from sklearn import tree 2. x = fn.drop(['uid', 'oil_actv_dt', 'create_dt', 'bad_ind', 'class_new'], axis=1) 3. y = fn.bad_ind.copy() 4. dtree = tree.DecisionTreeRegressor( max_depth = 2, min_samples_leaf = 500, min_samples_split = 5000) 5. dtree = dtree.fit(x, y)
9.输出决策树图像
输出决策树图像的代码如下:
1. import pydotplus 2. from IPython.display import Image 3. from sklearn.externals.six import StringIO 4. import os 5. os.environ["PATH"] += os.pathsep \ + 'C:/Program Files (x86)/Graphviz2.38/bin/' 6. dot_data = StringIO() 7. tree.export_graphviz(dtree, out_file=dot_data, 8. feature_names=x.columns, 9. class_names=['bad_ind'], 10. filled=True, rounded=True, 11. special_characters=True) 12. graph = pydotplus.graph_from_dot_data(dot_data.getvalue()) 13. Image(graph.create_png())
最终决策树的形式如图1-8所示,图中value计算的是叶节点中的正负样本标签的均值。在二分类的情况下,均值和标签为1的样本在总样本中占比是等价的,即字段value的数值和逾期率是一样的,因此可以直接在图中看到每一个叶节点的负样本占比。这也是选用CART回归树的原因之一。可以看到,样本通过两个特征被划分为3个客群。负样本占比逐渐减小,分别为0.074、0.03、0.012。
图1-8 决策树生成规则
10.实现决策树的决策逻辑
通过DataFrame中的条件判断,实现决策树的决策逻辑。
1. dff1 = fn.loc[(fn.amount_tot>9614.5)&(fn.coupon_amount_cnt>6)].copy() 2. dff1['level'] = 'past_A' 3. dff2 = fn.loc[(fn.amount_tot>9614.5)&(fn.coupon_amount_cnt<=6)].copy() 4. dff2['level'] = 'past_B' 5. dff3 = fn.loc[fn.amount_tot<=9614.5].copy() 6. dff3['level'] = 'past_C'
通过简单的逻辑判断可以实现对客户的分群,并大大减少业务损失。如果拒绝past_C类客户,则可以使整体负样本占比下降至0.021;如果将past_B也拒绝掉,则可以使整体负样本占比下降至0.012。至于实际对past_A、past_B、past_C采取何种策略,要根据利率来做线性规划,从而实现风险定价。这些内容不是本书的重点,在这里不做过多解释。但客群的逾期率越大,通常给予其越低的额度和越高的实际利率。