风控:利用决策树挖掘策略规则
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个特征,对应名称和含义如下:
特征名 | 类型 | 特征含义 |
---|---|---|
id | String | 主键 |
XINGBIE | int | 性别 |
CSNY | int | 出生年月 |
HYZK | int | 婚姻状况 |
ZHIYE | int | 职业 |
ZHICHEN | int | 职称 |
ZHIWU | int | 职务 |
XUELI | int | 学历 |
DWJJLX | int | 单位经济类型 |
DWSSHY | int | 单位所属行业 |
GRJCJS | float | 个人缴存基数 |
GRZHZT | int | 个人账户状态 |
GRZHYE | float | 个人账户余额 |
GRZHSNJZYE | float | 个人账户上年结转余额 |
GRZHDNGJYE | float | 个人账户当年归集余额 |
GRYICE | float | 个人月缴存额 |
DWYICE | float | 单位月缴存额 |
DKFFE | int | 贷款发放额 |
DKYE | float | 贷款余额 |
DKLL | float | 贷款利率 |
label | int | 是否逾期 (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