在信贷风控领域有两种比较常见的风险规避手段,分别是规则引擎和风控模型。通常规则引擎是使用一组简单的规则判断逻辑对客户客群进行划分,使得不同客群的客户的期望风险存在显著的差异,然后利用这些规则,快速进行风险划分。而风控模型一般使用机器学习的手段来预测用户的违约风险,相比较规则,机器学习模型虽然精度相对较高但是其更加复杂,且从建模到投产往往要经历一个比较长的时间周期。因此对于一些精度要求相对较低,或者在前期需要进行快速客群划分的常见,一般会采用规则挖掘的方式进行,并结合规则引擎,快速完成规则的上线及投产。对于一些精度要求较高的常见,会采用规则引擎粗筛+模型精选的方式进行风控决策。本文以某公司的“油品贷”为例,全称以代码的方式简单展示一下基于CART回归树,以均方差最小化为目标的风险规则挖掘全流程,关于机器学习中常用的决策树,可以参考Python数据挖掘之决策树(ID3、C4.5、CART)。
一、相关依赖包引入
# 引入需要的包
import numpy as np
import pandas as pd
import graphviz as g
from sklearn import tree
二、导入数据集,并查看数据状况
# 导入数据集
data = pd.read_excel('./data/data_for_tree.xlsx')
# 查看前5行数据详情
data.head()

2.1、查看数据离散型变量的基本情况
# 数据整体描述
data.describe()

2.2、查看数据集维度情况
# 数据维度情况
data.shape
(50609, 19)
2.3、查看数据基本情况
# 数据基本信息
data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 50609 entries, 0 to 50608
Data columns (total 19 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 uid 50609 non-null object
1 oil_actv_dt 50609 non-null datetime64[ns]
2 create_dt 45665 non-null datetime64[ns]
3 total_oil_cnt 46426 non-null float64
4 pay_amount_total 46426 non-null float64
5 class_new 50609 non-null object
6 bad_ind 50609 non-null int64
7 oil_amount 45665 non-null float64
8 discount_amount 45665 non-null float64
9 sale_amount 45665 non-null float64
10 amount 45665 non-null float64
11 pay_amount 45665 non-null float64
12 coupon_amount 45665 non-null float64
13 payment_coupon_amount 45663 non-null float64
14 channel_code 50609 non-null int64
15 oil_code 50609 non-null int64
16 scene 50609 non-null int64
17 source_app 50609 non-null int64
18 call_source 50609 non-null int64
dtypes: datetime64[ns](2), float64(9), int64(6), object(2)
memory usage: 7.3+ MB
2.4、查看各字段缺失情况
# 各字段的缺失情况
data.isna().mean()
uid 0.000000
oil_actv_dt 0.000000
create_dt 0.097690
total_oil_cnt 0.082653
pay_amount_total 0.082653
class_new 0.000000
bad_ind 0.000000
oil_amount 0.097690
discount_amount 0.097690
sale_amount 0.097690
amount 0.097690
pay_amount 0.097690
coupon_amount 0.097690
payment_coupon_amount 0.097730
channel_code 0.000000
oil_code 0.000000
scene 0.000000
source_app 0.000000
call_source 0.000000
dtype: float64
2.5、查看正负样本的分布情况
# 正负样本的分布情况
data.groupby(data.bad_ind).agg({"bad_ind":np.size})
bad_ind
bad_ind
0 49710
1 899
2.6、样本的时间跨度情况
# 样本的时间跨度
data['create_dt'].min(), data['create_dt'].max()
(Timestamp('2017-05-16 00:00:00'), Timestamp('2018-10-29 00:00:00'))
2.7、样本在时间上的分布
# 样本的在时间上的分布情况,以月为单位进行聚合
data.groupby([pd.DatetimeIndex(data['create_dt']).year, pd.DatetimeIndex(data['create_dt']).month]).agg({"create_dt":np.size})

三、特征工程
3.1、根据特征变量类型和加工方式不同进行划分
# org_lst 不需要做特殊变换,保留原始内容,然后直接去重
# agg_lst 数值型变量做聚合
# dstc_lst 离散型变量做count
org_lst = ['uid','create_dt','oil_actv_dt','class_new','bad_ind']
agg_lst = ['oil_amount','discount_amount','sale_amount','amount','pay_amount','coupon_amount','payment_coupon_amount']
dstc_lst = ['channel_code','oil_code','scene','source_app','call_source']
# 拷贝不同类型特征的数据,保留底表
df = data[org_lst].copy()
df[agg_lst] = data[agg_lst].copy()
df[dstc_lst] = data[dstc_lst].copy()
3.2、缺失值填充
def time_isna(x, y):
return y if str(x) == 'NaT' else x
# 按'uid','create_dt'进行逆序排序
df2 = df.sort_values(['uid','create_dt'], ascending = False)
# 用oil_actv_dt来对缺失的creat_dt做补全,
df2['create_dt'] = df2.apply(lambda x: time_isna(x.create_dt, x.oil_actv_dt), axis = 1)
3.3、样本截取
# 截取放款日和创建日期之差在6个月内的数据。
df2['dtn'] = (df2.oil_actv_dt - df2.create_dt).apply(lambda x :x.days)
df = df2[df2['dtn'] < 180]
df.head()

3.4、重复样本去重,保留最新
# 对org_list变量求历史贷款天数的最大间隔,并且去重,保留最新的一条数据
base = df[org_lst]
base['dtn'] = df['dtn']
base = base.sort_values(['uid','create_dt'],ascending = False)
base = base.drop_duplicates(['uid'],keep = 'first')
base.shape
(11099, 6)
3.5、特征衍生——连续型变量
# 对连续型变量进行聚合衍生
gn = pd.DataFrame()
for i in agg_lst:
# 统计当前特征值个数
tp = pd.DataFrame(df.groupby('uid').apply(lambda df:len(df[i])).reset_index())
tp.columns = ['uid',i + '_cnt']
if gn.empty == True:
gn = tp
else:
gn = pd.merge(gn,tp,on = 'uid',how = 'left')
# 统计当前特征值大于0的个数
tp = pd.DataFrame(df.groupby('uid').apply(lambda df:np.where(df[i] > 0, 1, 0).sum()).reset_index())
tp.columns = ['uid',i + '_num']
if gn.empty == True:
gn = tp
else:
gn = pd.merge(gn,tp,on = 'uid',how = 'left')
# 对当前特征的历史数据求和
tp = pd.DataFrame(df.groupby('uid').apply(lambda df:np.nansum(df[i])).reset_index())
tp.columns = ['uid',i + '_tot']
if gn.empty == True:
gn = tp
else:
gn = pd.merge(gn,tp,on = 'uid',how = 'left')
# 求当前特征历史数据均值
tp = pd.DataFrame(df.groupby('uid').apply(lambda df:np.nanmean(df[i])).reset_index())
tp.columns = ['uid',i + '_avg']
if gn.empty == True:
gn = tp
else:
gn = pd.merge(gn,tp,on = 'uid',how = 'left')
# 求当前特征历史数据最大值
tp = pd.DataFrame(df.groupby('uid').apply(lambda df:np.nanmax(df[i])).reset_index())
tp.columns = ['uid',i + '_max']
if gn.empty == True:
gn = tp
else:
gn = pd.merge(gn,tp,on = 'uid',how = 'left')
# 求当前特征历史数据最小值
tp = pd.DataFrame(df.groupby('uid').apply(lambda df:np.nanmin(df[i])).reset_index())
tp.columns = ['uid',i + '_min']
if gn.empty == True:
gn = tp
else:
gn = pd.merge(gn,tp,on = 'uid',how = 'left')
# 求当前特征历史数据方差
tp = pd.DataFrame(df.groupby('uid').apply(lambda df:np.nanvar(df[i])).reset_index())
tp.columns = ['uid',i + '_var']
if gn.empty == True:
gn = tp
else:
gn = pd.merge(gn,tp,on = 'uid',how = 'left')
# 求当前特征历史数据极差
tp = pd.DataFrame(df.groupby('uid').apply(lambda df:np.nanmax(df[i]) -np.nanmin(df[i]) ).reset_index())
tp.columns = ['uid',i + '_ran']
if gn.empty == True:
gn = tp
else:
gn = pd.merge(gn,tp,on = 'uid',how = 'left')
# 求当前特征历史数据变异系数,避免除0,使用0.01进行平滑
tp = pd.DataFrame(df.groupby('uid').apply(lambda df:np.nanmean(df[i]) / (np.nanvar(df[i]) + 0.01)).reset_index())
tp.columns = ['uid',i + '_cva']
if gn.empty == True:
gn = tp
else:
gn = pd.merge(gn,tp,on = 'uid',how = 'left')
3.6、特征衍生——离散型变量
# 对离散型变量进行计数
gc = pd.DataFrame()
for i in dstc_lst:
tp = pd.DataFrame(df.groupby('uid').apply(lambda df: len(set(df[i]))).reset_index())
tp.columns = ['uid',i + '_dstc']
if gc.empty == True:
gc = tp
else:
gc = pd.merge(gc,tp,on = 'uid',how = 'left')
3.7、变量组装
# 变量组合
fn = pd.merge(base,gn,on= 'uid')
fn = pd.merge(fn,gc,on= 'uid')
# 对组合后的缺失值进行填充
fn = fn.fillna(0)
fn.shape
(11099, 74)
四、决策树构建
4.1、决策树训练
# 移除训练集中的无关列
train = fn.drop(['uid','oil_actv_dt','create_dt','bad_ind','class_new'],axis = 1)
# 构建标签列
y = fn.bad_ind
# 采用CART树进行规则挖掘
r_tree = tree.DecisionTreeRegressor(
# r_tree = tree.DecisionTreeClassifier(
max_depth = 3,
min_samples_leaf = 500, min_samples_split=5000)
r_tree = r_tree.fit(train, y)
4.2、决策树可视化
# 使用graphviz进行可视化展示
import graphviz
dot_data = tree.export_graphviz(
r_tree,
out_file = None,
feature_names = train.columns,
class_names = ['good','bad'],
filled=True,
rounded=True,
special_characters=True)
graph = graphviz.Source(dot_data)
graph

五、规则挖掘
这里我们采用CART回归树进行规则挖掘,CART树是一种基于均方差的二叉决策树,在每层分化的时候会选择使得均方差最小的特征作为当前的节点,并进行划分。使用CART树的主要原因是:在二分类场景下,CART叶节点的输出就是当前节点标签的均值,直观反映出正负样本的占比,在风控场景下,授信通过的群体中由更小的负样本的占比,因此是用CART树更符合当前风控的业务要求。
上一章节中我们最终得到了上图所示的两层决策树。在二分类情况下,均值和标签为一的样本在总样本中的占比是等价的,可以看出,样本被特征amount_to
和amount_num
划分成了三个群体,其中负样本的占比分别是:7.4%,3%和1.2%。
这样我们分别可以得到如下三条策略:
rule_A: amount_tot <= 48077.5
rule_B: amount_tot > 48077.5 && amount_num <= 3.5
rule_C: amount_tot > 48077.5 && amount_num > 3.5
如果执行rule_A
可以使得整体负样本占比控制在7.4%左右;如果执行rule_B
可以使得整体负样本占比控制在3%左右;rule_C
最严格,可以将整体负样本占比控制在1.2%
左右。在实际的业务中,采用什么样的拒绝和通过策略取决于当前产品的额度和利率等,并不是说越严格的拒绝策略就是越好的,往往需要从整体利润的角度出发,对于不同的客群,应当予以不同的定价额度和实际利率,从而保证在控制整体风险的同时,实现当前信贷产品的利润最大化。
信贷风控建模实战系列
信贷风控建模实战(一)——建模流程总览
信贷风控建模实战(二)——策略生成及规则挖掘
信贷风控建模实战(三)——评分卡建模之逻辑回归
信贷风控建模实战(四)——评分卡建模之XGBoost
信贷风控建模实战(五)——特征工程
信贷风控建模实战(六)——异常检测
信贷风控建模实战(七)——群组划分or聚类
信贷风控建模实战(八)——风控基础概念
网友评论