智能风控:原理、算法与工程实践
上QQ阅读APP看书,第一时间看更新

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采取何种策略,要根据利率来做线性规划,从而实现风险定价。这些内容不是本书的重点,在这里不做过多解释。但客群的逾期率越大,通常给予其越低的额度和越高的实际利率。