信贷风控建模实战(二)——策略生成及规则挖掘

2023-02-19  本文已影响0人  老羊_肖恩

  在信贷风控领域有两种比较常见的风险规避手段,分别是规则引擎和风控模型。通常规则引擎是使用一组简单的规则判断逻辑对客户客群进行划分,使得不同客群的客户的期望风险存在显著的差异,然后利用这些规则,快速进行风险划分。而风控模型一般使用机器学习的手段来预测用户的违约风险,相比较规则,机器学习模型虽然精度相对较高但是其更加复杂,且从建模到投产往往要经历一个比较长的时间周期。因此对于一些精度要求相对较低,或者在前期需要进行快速客群划分的常见,一般会采用规则挖掘的方式进行,并结合规则引擎,快速完成规则的上线及投产。对于一些精度要求较高的常见,会采用规则引擎粗筛+模型精选的方式进行风控决策。本文以某公司的“油品贷”为例,全称以代码的方式简单展示一下基于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()
image.png

2.1、查看数据离散型变量的基本情况

# 数据整体描述
data.describe()
image.png

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})
image.png

三、特征工程
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()
image.png

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_toamount_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聚类
信贷风控建模实战(八)——风控基础概念

上一篇 下一篇

猜你喜欢

热点阅读