风控——利用决策树挖掘策略规则

风控:利用决策树挖掘策略规则

github 代码:
代码链接

1 风控规则的分类

在讲解规则挖掘的方法前,先介绍下风控规则的分类

1.1 从数据和业务角度可分为:

  • 准入规则:例如对申请人的年龄的限制,一般根据监管政策或产品要求来制定。
  • 黑名单类规则:包含内部黑名单和外部三方黑名单,黑名单里又可细分成逾期,法院,高限,诈骗等类别。
  • 反欺诈类规则:针对个人欺诈和团伙欺诈,一般根据设备指纹信息,通讯录,地址定位信息对欺诈行为做甄别。
  • 校验类规则:三要素一致性校验,在网状态的校验,联系人号码状态校验等。
  • 多头共债类规则:衡量借款人因多头借贷导致违约的风险,依赖第三方数据。
  • 逾期类规则:自有平台和外部的逾期行为,能直接反映借款人的信用情况。
  • 评分类规则:通过模型将弱的变量合成一个强变量分数,从评分种类可分为反欺诈分,信用分,支付分,消费分等。

1.2 从规则的严格程度可分为:

  • 硬规则:也叫严拒规则,这种规则拒绝的都是高风险人群,一般阈值确定下来就不再做变更,上述的黑名单规则,反欺诈规则,校验类规则都属于这个类别。
  • 软规则:可以灵活调整规则拒绝的阈值,用的数据一般都是连续型的数值变量,这种变量具有一定的排序性,例如信用分越低,违约风险越高。上述的多头共债逾期类规则,评分规则都可归类于此。

1.3 从规则变量的使用数目可分为:

  • 单变量规则,也称简单规则,即规则只用到了一个变量。例如“是否命中失信被执行人”,业务逻辑比较简单。
  • 多变量规则,也称复杂规则或组合规则,即规则用到了两个及以上的变量,通过变量之间的交叉组合或者权重加和来提高预测坏用户的精准度,例如“信用分<=400 且多头借贷数大于3次"。

2 常用的规则挖掘方法

2.1 基于政策或专家经验

基于政策的规则,我们根据监管要求,或者业务产品要求直接制定即可。

基于专家经验来挖掘规则,适用于两种场景:

  • 一是风控冷启动阶段,这时候无贷后样本可用,我们可以基于历史过往经验,或者参考同行业制定一些规则,例如设备反欺诈,关联风险规则。
  • 二是挖掘一些硬规则时可直接根据经验来定,比如校验类规则中,三要素匹配不一致,这类用户可能是通过买号,借号来进行申请,存在很大的欺诈风险,即使其中有些误杀,我们也应该果断的拒绝这类人群。另外根据市场环境变化,我们可基于经验做一些前瞻性的规则,像之前疫情开始爆发时,很多人的还款能力受到影响,就要及时上一些多头共债规则来提前控制风险。

2.2 基于决策树挖掘规则

2.2.1 策略挖掘自动化技术

通过机器学习或者其他方式进行策略的自动挖掘是对专家经验、数据驱动挖掘的有效补充。但是在时间实践过程中也有一些需要注意的问题:

  • 有偏性:有Y标签的样本是之前策略通过的样本,在分布上不等于初始进件的样本
  • 可解释性:不能出现违反业务认知的规则
  • 无冗余规则:反例:例如第1条规则:收入负债比超过80%拒绝。… ,第5条规则:收入负债比超过90%拒绝
  • 规则的自洽性:反例:例如第3条规则:学历本科以下拒绝。…,第7条规则:初中学历以上拒绝
  • 规则的复杂度:即不能过拟合,反例:10条字段挖掘出1000条规则。该规则即在训练集上表现很好,但是在测试集上的效果很差。

2.2.2 决策树概要

决策树是一种生成树形规则的机器学习算法,基于一堆样本和一个分类结果,通过学习这些样本得到一个决策树,这个决策树能够对新的数据给出正确的分类,决策树可以实现自动化的挖掘大批量的策略组合。

在众多的算法中,决策树整体分类准确率不高,但是部分叶子节点的准确率却可以很高,因此我们可以提取决策树的叶子规则,并筛选准确率比较高的叶子节点,作为风控策略挖掘手段,替代人工或者辅助人工,大大提高策略发现的效率。

决策树挖掘规则的优点在于,可根据信息增益,基尼系数等指标自动进行样本划分,且可对多个变量进行组合,大大减少工作量,我们可根据下面几个参数来控制树生成的逻辑:

  • max_depth: 最大树深,深度越大,产生的叶子节点越多,一般挖掘规则时设置成2-3即可-
  • min_samples_leaf: 叶子节点的最小样本量或最小占比,一般占比可设置成5%–10%
  • min_samples_split: 父节点在分裂时需要的最小样本量或最小占比,一般设置成5%–10%

不过需要注意的是,

  • 分裂时完全依赖于基尼系数等指标,缺乏业务含义,且不考虑变量本身的排序性,容易导致分出来的结果在业务上不可解释
  • 决策树容易过拟合,需要验证在跨时间维度上是否稳定
  • 相比于画格子方法,决策树是自动分裂,这样就不容易手工进行调整

关于决策树实现原理和机制,网上已有较多的资料进行讲解,本篇不再进行赘述。本文主要介绍如何利用决策树算法挖掘有效规则,实现风控策略自动化

3 利用决策树挖掘规则案例一

3.1 数据说明

测试数据集包含:个人基本信息、个人住房公积金缴存、贷款等数据信息,来预测用户是否会逾期还款。数据集包含40000带标签训练集样本,共有19个基本特征,无缺失值

标签:label是否逾期(是 = 1,否 = 0)。

特征:包含以下19个特征,对应名称和含义如下:

特征名类型特征含义
idString主键
XINGBIEint性别
CSNYint出生年月
HYZKint婚姻状况
ZHIYEint职业
ZHICHENint职称
ZHIWUint职务
XUELIint学历
DWJJLXint单位经济类型
DWSSHYint单位所属行业
GRJCJSfloat个人缴存基数
GRZHZTint个人账户状态
GRZHYEfloat个人账户余额
GRZHSNJZYEfloat个人账户上年结转余额
GRZHDNGJYEfloat个人账户当年归集余额
GRYICEfloat个人月缴存额
DWYICEfloat单位月缴存额
DKFFEint贷款发放额
DKYEfloat贷款余额
DKLLfloat贷款利率
labelint是否逾期 (0代表没逾期1代表逾期)

3.2 读取数据并构建决策树

import pandas as pd
import numpy  as np
pd.set_option('display.max_columns', None)#显示所有的列
train = pd.read_csv('train.csv').fillna(-1)

# 构建训练集
X = train.loc[:,'XINGBIE':'DKLL']
Y = train['label']

# 构造决策树
from sklearn import tree
clf = tree.DecisionTreeClassifier(
     max_depth=3,
     min_samples_leaf=50
     )
clf = clf.fit(X, Y)

3.2 决策树可视化

决策树可视化的方案比较多,主要有sklearn中的plot_tree接口、graphv可视化、dtreeviz可视化, 这里我们都写出来作为对比。对决策树进行可视化,能够帮助我们自己充分理解决策树的生成过程,如果是风控,对数据结果的可解释性也提供帮助。

3.2.1 利用plot_tree对决策树进行可视化

# 接口自带的
tree.plot_tree(clf)
plt.show()

#缺点是颜色单调,不易直观理解,不推荐

在这里插入图片描述

3.2.2 利用graphviz对决策树进行可视化

import graphviz 
dot_data = tree.export_graphviz(
                     clf, 
                     out_file=None, 
                     feature_names=X.columns,  
                     class_names=['good','bad'],  
                     filled=True, rounded=True,  
                     special_characters=True)  
graph = graphviz.Source(dot_data)  
graph

在这里插入图片描述

生成的决策树解释

  • samples:节点中观察的数量,比如根节点40000,表示数据集总共有4万个样本

  • 有多少种类别,整棵树的叶子就有多少种颜色,比如我们这里有2个类别,颜色对应是黄、绿、Gini指数越小,该节点颜色越深,也就是纯度越高。

  • value表示当前节点2种类别的样本有多少,比如下面第一棵树的根节点,value = [37243,2757],表示有37243个好样本,2757坏样本

  • class表示当前那个类别的样本最多,比如下面最右边的一棵树的根节点,class = bad,可以看到当前节点它的坏样本数是最多的。

  • gini:节点的基尼不纯度。当沿着树向下移动时,平均加权的基尼不纯度必须降低。

3.2.3 利用dtreeviz对决策树进行可视化

dtreeviz是美观且容易理解,是一款非常优秀的决策是可视化包。

from dtreeviz.trees import dtreeviz
testX = X.iloc[77,:]
viz = dtreeviz(clf,X,Y,
                feature_names=np.array(X.columns),
                class_names={0:'good',1:'bad'},
                X = testX)             

viz.view()  # 会在web 端显示

在这里插入图片描述

可视化结果已网页的方式生成,决策流程、数值分布、决策结果非常直观。

如果将树的深度增加至5层,生成的结果会更加复杂:

from sklearn import tree
clf = tree.DecisionTreeClassifier(
     max_depth=5,
     min_samples_leaf=50
     )
clf = clf.fit(X, Y)
  from dtreeviz.trees import dtreeviz
  testX = X.iloc[77,:]
  viz = dtreeviz(clf,X,Y,
                 feature_names=np.array(X.columns),
                 class_names={0:'good',1:'bad'},
                 X = testX)             
  viz.view()

在这里插入图片描述

也可以横向展示:

viz = dtreeviz(clf,X,Y,
               orientation ='LR',  # left-right orientation
               feature_names=np.array(X.columns),
               class_names={0:'good',1:'bad'},
               X = testX)       
viz.view()

截取部分显示如下:

在这里插入图片描述

如果只想可视化预测路径,则需要设置参数 show_just_path=True

在这里插入图片描述

有了决策树的可视化,可以直观地转换每条策略了。但是人为分析效率还是比较低,因此需要更高效的方式,对数据决策树上的信息进行提取,直接得到规则。

3.3 决策树规则提取

3.3.1 决策树的生成结构分析

提取规则之前,我们需要理解决策树的存储结构,探究sklearn中的决策树是如何设计和实现的。以分类决策树为例,首先看下决策树都内置了哪些属性和接口:通过dir属性查看一颗初始的决策树都包含了哪些属性(这里过滤掉了以"_"开头的属性,因为一般是内置私有属性),得到结果如下:


from sklearn.datasets import load_iris
from sklearn import tree
iris = load_iris()
clf = tree.DecisionTreeClassifier()
clf = clf.fit(iris.data, iris.target)
clf.classes_
[x for x in dir(clf) if not x.startswith('_')]

结果如下:

['apply',
 'capacity',
 'children_left',
 'children_right',
 'compute_feature_importances',
 'compute_partial_dependence',
 'decision_path',
 'feature',
 'impurity',
 'max_depth',
 'max_n_classes',
 'n_classes',
 'n_features',
 'n_leaves',
 'n_node_samples',
 'n_outputs',
 'node_count',
 'predict',
 'threshold',
 'value',
 'weighted_n_node_samples']

本文的重点是探究决策树中是如何保存训练后的"那颗树",我们进一步用鸢尾花数据集对决策树进行训练一下,而后再次调用dir函数,看看增加了哪些属性和接口:

from sklearn import tree
X,y = load_iris(return_X_y=True)
dt =  tree.DecisionTreeClassifier(max_depth=2).fit(X,y)
set(dir(dt)).difference(dir(tree.DecisionTreeClassifier()))

结果如下:

{‘classes_’, ‘max_features_’, ‘n_classes_’, ‘n_features_’, ‘n_outputs_’, ‘tree_’}

通过集合的差集,很明显看出训练前后的决策树主要是增加了6个属性(都是属性,而非函数功能),其中通过属性名字也很容易推断其含义:

  • classes_:分类标签的取值,即y的唯一值集合
  • max_features_:最大特征数
  • n_classes_:类别数,如2分类或多分类等,即classes_属性中的长度
  • n_features_in_:输入特征数量,等价于老版sklearn中的n_features_,现已弃用,并推荐n_features_in_
  • n_outputs:多输出的个数,即决策树不仅可以用于实现单一的分类问题,还可同时实现多个分类问题,例如给定一组人物特征,用于同时判断其是男/女、胖/瘦和高矮,这是3个分类问题,即3输出(需要区别理解多分类和多输出任务)
  • tree_:这个tree_就是本文的重点,是在决策树训练之后新增的属性集,其中存储了决策树是如何存储的。

我们对这个tree_属性做进一步探究,首先打印该tree_属性发现,这是一个Tree对象,并给出了在sklearn中的文件路径:

 dt.tree_

结果显示:<sklearn.tree._tree.Tree at 0x1fc2ff97cc8>

我们可以通过help方法查看Tree类的介绍:

import sklearn

help(sklearn.tree._tree.Tree)

在这里插入图片描述

内部接口文档写的很清晰了:

基于数组表示的二分类决策树,进一步地,在这个二叉树中,数组的第i个元素代表了决策树的第i个节点的信息,节点0表示决策树的根节点。那么每个节点又都蕴含了什么信息呢?我们注意到上述文档中列出了节点的文件名:_tree.pxd,查看其中,很容易发现节点的定义如下:

  • left_child:size类型(无符号整型),代表了当前节点的左子节点的索引
  • right_child:类似于left_child
  • feature:size类型,代表了当前节点用于分裂的特征索引,即在训练集中用第几列特征进行分裂
  • threshold:double类型,代表了当前节点选用相应特征时的分裂阈值,一般是≤该阈值时进入左子节点,否则进入右子节点
  • n_node_samples:size类型,代表了训练时落入到该节点的样本总数。显然,父节点的n_node_samples将等于其左右子节点的n_node_samples之和

至此,决策树中单个节点的属性定义和实现基本推断完毕,那么整个决策树又是如何将所有节点串起来的呢?,再次查看训练后决策树的tree_属性,看看它都哪些接口,仍然过滤掉内置私有属性,得到如下结果:

决策树结构探索

[x for x in dir(clf.tree_) if not x.startswith('_')]
['apply','capacity', 'children_left','children_right',
 'compute_feature_importances','compute_partial_dependence',
 'decision_path','feature',
 'impurity','max_depth',
 'max_n_classes','n_classes',
 'n_features','n_leaves',
 'n_node_samples','n_outputs','node_count',
 'predict','threshold',
 'value', 'weighted_n_node_samples']

为了进一步理解各属性中的数据是如何存储的,我们仍以鸢尾花数据集为例,训练一个max_depth=2的决策树(根节点对应depth=0),并查看如下取值:


clf.tree_.children_left
clf.tree_.children_right
clf.tree_.feature
clf.tree_.capacity
clf.tree_.threshold
clf.tree_.value
clf.tree_.impurity
clf.tree_.decision_path
from sklearn import tree
X,y = load_iris(return_X_y=True)
dt =  tree.DecisionTreeClassifier(max_depth=2).fit(X,y)
tree = dt.tree_

print('tree.node_count',tree.node_count)
print('tree.n_leaves',tree.n_leaves)
print('tree.children_left',tree.children_left)
print('tree.children_right',tree.children_right)
print('tree.feature',tree.feature)
print('tree.threshold',tree.threshold)

# 输出结果
'''
tree.node_count 5
tree.n_leaves 3
tree.children_left [ 1 -1  3 -1 -1]
tree.children_right [ 2 -1  4 -1 -1]
tree.feature [ 3 -2  3 -2 -2]
tree.threshold [ 0.80000001 -2.          1.75       -2.         -2.        ]

tree.value [[[50. 50. 50.]]

 [[50.  0.  0.]]

 [[ 0. 50. 50.]]

 [[ 0. 49.  5.]]

 [[ 0.  1. 45.]]]
'''
X,y = load_iris(return_X_y=True)
y[X[:,3]<=0.8]

# 输出结果
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0])

通过children_left和children_right两个属性的子节点对应关系,按照根-左-右的顺序,即:前序遍历。

3.3.2 老方法提取决策树规则

X = train.loc[:,'XINGBIE':'DKLL']
Y = train['label']

# 训练一个决策树,这里限制了最大深度和最小样本树

from sklearn import tree
clf = tree.DecisionTreeClassifier(
     max_depth=3,
     min_samples_leaf=50
     )
clf = clf.fit(X, Y)

# 决策树规则提取-老方法

from sklearn.tree import _tree
def tree_to_code(tree, feature_names):
    tree_ = tree.tree_
    feature_name = [
        feature_names[i] if i != _tree.TREE_UNDEFINED else "undefined!"
        for i in tree_.feature
    ]
    print ("def tree({}):".format(", ".join(feature_names)))

    def recurse(node, depth):
        indent = "  " * depth
        if tree_.feature[node] != _tree.TREE_UNDEFINED:
            name = feature_name[node]
            threshold = tree_.threshold[node]
            print("{}if {} <= {}:".format(indent, name, threshold))
            recurse(tree_.children_left[node], depth + 1)
            print("{}else:  # if {} > {}".format(indent, name, threshold))
            recurse(tree_.children_right[node], depth + 1)
        else:
            print("{}return {}".format(indent, tree_.value[node]))

    recurse(0, 1)

tree_to_code(clf,X.columns)

结果如下:

在这里插入图片描述

可以看到,老方法提取策略的结果需要人为拆解。

3.3.3 新方法提取决策树规则(利用函数提取规则)

from sklearn.tree import _tree

def Get_Rules(clf,X):
    n_nodes = clf.tree_.node_count
    children_left = clf.tree_.children_left
    children_right = clf.tree_.children_right
    feature = clf.tree_.feature
    threshold = clf.tree_.threshold
    value = clf.tree_.value

    node_depth = np.zeros(shape=n_nodes, dtype=np.int64)
    is_leaves  = np.zeros(shape=n_nodes, dtype=bool)
    stack = [(0, 0)]

    while len(stack) > 0:

        node_id, depth = stack.pop()
        node_depth[node_id] = depth

        is_split_node = children_left[node_id] != children_right[node_id]

        if is_split_node:
            stack.append((children_left[node_id],  depth+1))
            stack.append((children_right[node_id], depth+1))
        else:
            is_leaves[node_id] = True  
    feature_name = [
            X.columns[i] if i != _tree.TREE_UNDEFINED else "undefined!"
            for i in clf.tree_.feature]

    ways  = []
    depth = []
    feat = []
    nodes = []
    rules = []
    for i in range(n_nodes):   
        if  is_leaves[i]: 
            while depth[-1] >= node_depth[i]:
                depth.pop()
                ways.pop()    
                feat.pop()
                nodes.pop()
            if children_left[i-1]==i:#当前节点是上一个节点的左节点,则是小于
                a='{f}<={th}'.format(f=feat[-1],th=round(threshold[nodes[-1]],4))
                ways[-1]=a              
                last =' & '.join(ways)+':'+str(value[i][0][0])+':'+str(value[i][0][1])
                rules.append(last)
            else:
                a='{f}>{th}'.format(f=feat[-1],th=round(threshold[nodes[-1]],4))
                ways[-1]=a
                last = ' & '.join(ways)+':'+str(value[i][0][0])+':'+str(value[i][0][1])
                rules.append(last)

        else: #不是叶子节点 入栈
            if i==0:
                ways.append(round(threshold[i],4))
                depth.append(node_depth[i])
                feat.append(feature_name[i])
                nodes.append(i)             
            else: 
                while depth[-1] >= node_depth[i]:
                    depth.pop()
                    ways.pop()
                    feat.pop()
                    nodes.pop()
                if i==children_left[nodes[-1]]:
                    w='{f}<={th}'.format(f=feat[-1],th=round(threshold[nodes[-1]],4))
                else:
                    w='{f}>{th}'.format(f=feat[-1],th=round(threshold[nodes[-1]],4))              
                ways[-1] = w  
                ways.append(round(threshold[i],4))
                depth.append(node_depth[i]) 
                feat.append(feature_name[i])
                nodes.append(i)
    return rules

import pandas as pd
import numpy  as np
pd.set_option('display.max_columns', None)#显示所有的列
train = pd.read_csv('train.csv').fillna(-1)

X = train.loc[:,'XINGBIE':'DKLL']
Y = train['label']

from sklearn import tree
#训练一个决策树,对规则进行提取
clf = tree.DecisionTreeClassifier(max_depth=10,min_samples_leaf=50)
clf = clf.fit(X, Y)
Rules = Get_Rules(clf,X)

# 查看前5条规则
Rules[0:5]

输出结果:

['GRZHZT<=1.5 & DWSSHY<=14.5 & DWJJLX<=177.0 & DWJJLX<=115.0 & DKYE<=111236.2852 & DWSSHY<=4.5 & GRYJCE<=663.54 & DKYE<=67419.1094:45.0:8.0',
 'GRZHZT<=1.5 & DWSSHY<=14.5 & DWJJLX<=177.0 & DWJJLX<=115.0 & DKYE<=111236.2852 & DWSSHY<=4.5 & GRYJCE<=663.54 & DKYE>67419.1094:61.0:3.0',
 'GRZHZT<=1.5 & DWSSHY<=14.5 & DWJJLX<=177.0 & DWJJLX<=115.0 & DKYE<=111236.2852 & DWSSHY<=4.5 & GRYJCE>663.54 & GRZHYE<=45622.4883 & DKYE<=1825.5625:63.0:2.0',
 'GRZHZT<=1.5 & DWSSHY<=14.5 & DWJJLX<=177.0 & DWJJLX<=115.0 & DKYE<=111236.2852 & DWSSHY<=4.5 & GRYJCE>663.54 & GRZHYE<=45622.4883 & DKYE>1825.5625:188.0:0.0',
 'GRZHZT<=1.5 & DWSSHY<=14.5 & DWJJLX<=177.0 & DWJJLX<=115.0 & DKYE<=111236.2852 & DWSSHY<=4.5 & GRYJCE>663.54 & GRZHYE>45622.4883:46.0:4.0']

第一条后面的45.0:8.0,指的是样本标签的0:1比例,利用原始数据进行核验:


cond1 = (train['GRZHZT']<=1.5) & (train['DWSSHY']<=14.5) & (train['DWJJLX']<=177.0) & (train['DWJJLX']<=115.0) & (train['DKYE']<=111236.2852) & (train['DWSSHY']<=4.5) & (train['DWYJCE']<=663.54) & (train['DKYE']<=67419.1094)
train[cond1]['label'].value_counts()

结果输出:

0 45

1 8

Name: label, dtype: int64

最后,可以导出成表格的形式,更加清晰,提取rate较高的组合规则:

在这里插入图片描述

提高树的深度再看看,max_depth=15,可以看到规则数变成了521条,规模更大。

clf = tree.DecisionTreeClassifier(max_depth=15,min_samples_leaf=20)
clf = clf.fit(X, Y)
Rules = Get_Rules(clf,X)

# 查看规则数量
print(len(Rules))

# 遍历所有规则
for i in Rules:
    print(i)

在这里插入图片描述

在这里插入图片描述

4 参考文献

【1】https://chowdera.com/2022/220/202208080350119845.html

【2】https://mp.weixin.qq.com/s/Mj_3v2Xh7B7AnSkRP2TyJg

  • 8
    点赞
  • 75
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值