这一小节首先介绍决策模型的基本概念以及其背后的数学原理,然后介绍如何通过Python来实现决策树模型,从而为之后的员工离职预测模型的搭建做铺垫。
点击下载。
决策树模型是机器学习各种算法模型中比较好理解的一个模型,它的基本原理便是通过对一系列问题进行if/else的推导,最终实现相关决策。
下图所示为一个典型的决策树模型:员工离职预测模型的简单演示。该决策树首先判断员工满意度是否小于5,答案为“是”则认为该员工会离职,答案为“否”则接着判断其收入是否小于10,000元,答案为“是”则认为该员工会离职,答案为“否”则认为该员工不会离职。
这只是个简单演示,之后要讲的实际的员工离职预测模型会根据大数据搭建一个稍微复杂的模型,不过决策树模型的核心原理就是上图所示的内容了。并且商业实战中不会单纯根据“满意度”和“收入<10,000”两个特征就判断是否离职,而是根据多个特征来预测离职概率,并根据相应的阈值来判断是否离职,如离职概率超过50%即认为该员工会离职。
这里解释几个决策树模型的重要关键词:根节点、父节点、子节点和叶子节点。
父节点和子节点是相对的,子节点由父节点根据某一规则分裂而来,然后子节点作为新的父节点继续分裂,直至不能分裂为止。根节点则和叶子节点是相对的,根节点是没有父节点的节点,即初始节点,叶子节点则是没有子节点的节点,即最后的节点。决策树模型的关键即是如何选择合适的节点进行分裂。
举例来说,在上图中,最上面的“满意度<5”就是根节点,它也是个父节点,分裂成两个子节点“离职”和“收入<10,000”,其中子节点“离职”因为不再分裂了,不再有子节点了,所以也称之为叶子节点;另外一个子节点“收入<10,000”也是其下面两个节点的父节点,最后的“离职”及“不离职”则为叶子节点。
在实际应用中,企业会通过已有的数据来看离职员工都符合何种特征,如查看他们的满意度、收入、工龄、月工时、项目数等,然后选择相应的特征进行节点分裂,便可以搭建出类似上图所示的决策树模型。利用该决策树模型就可以预测员工离职情况,根据数据分析结果做好相应的应对措施。
决策树概念本身并不复杂,主要就是通过连续的逻辑判断来得到最后的结论,其关键在于如何建立出这样一棵“树”来。比如根节点应该选择哪一个特征,选“满意度<5”作为根节点和选“收入<10,000”作为根节点会起到不同的效果。其次,收入作为一个连续变量,是选“收入<10,000”作为一个节点,还是选“收入<100,000”作为一个节点都是有讲究的。下面就来讲解一下决策树模型的建树依据。
决策树模型的建树依据主要用到的是基尼系数的概念。基尼系数(gini)用于计算一个系统中的失序现象,也即系统的混乱程度。基尼系数越高,系统混乱程度越高,建立决策树模型的目的就是降低系统的混乱程度,从而到达合适的数据分类效果,基尼系数的计算公式如下:
其中为类别
在样本T中出现的频率,即类别为
的样本占总样本个数的比率。∑为求和公式,即把所有的
进行求和。
例如,对于一个全部都是离职员工的样本来说,里面只有一个类别:离职员工,其出现的频率是100%,所以该系统基尼系数为,表示该系统没有混乱,或者说该系统的“纯度”很高。而如果样本里一半是离职员工,另一半是未离职员工,那么类别个数为2,每个类别出现的频率都为50%,所以其基尼系数为
,也即其混乱程度很高。
当引入某个用于进行分类的变量(比如“满意度<5”),则分割后的基尼系数公式为:
其中、
划分成两类的样本量,gini(T1)和gini(T2)为划分后的两类各自的基尼系数。
例如,一个初始样本中有1000个员工,其中已知有400人离职,600人不离职。其划分前该系统的基尼系数为,那么下面采用两种不同的划分方式来决定初始节点:1、根据“满意度<5”进行分类;2、根据“收入<10,000”进行分类。
划分方式1:以“满意度<5”为初始节点进行划分,划分后的基尼系数为0.3,如下图所示。
划分方式2:以“收入<10,000”为初始节点进行划分,划分后的基尼系数为0.45,如下图所示。
可以看到未划分时的基尼系数为0.48,以“满意度<5”为初始节点进行划分后的基尼系数为0.3,而以“收入<10,000”为初始节点进行划分后的基尼系数为0.45。基尼系数越低表示系统的混乱程度越低,区分度越高,能够比较好地作为一个分类预测模型,因此这里选择“满意度<5”作为初始节点。这里演示了如何选择初始节点,初始节点下面的节点也是用类似的方法来进行选择。
同理,对于“收入”这一变量来说,是选择“收入<10,000”还是选择“收入<100,000”进行划分,也是通过计算在这两种情况下划分后的基尼系数来进行判断。若还有其他的变量,如“工龄”、“月工时”等,也是通过类似的手段计算划分后的系统的基尼系数,来看如何进行节点的划分,从而搭建一个较为完善的决策树模型。采用基尼系数进行运算的决策树也称之为CART决策树斜体样式。
补充知识点:信息熵
这里补充另一种衡量系统混乱程度的经典手段:信息熵,供感兴趣的读者参考。
信息熵的作用和基尼系数是基本一致的,都是用来衡量系统的混乱程度,从而进行合适的节点划分。信息熵H(X)的公式如下所示:
其中X表示的是随机变量,随机变量的取值为(X1, X2, X3……),在n分类问题中,便有n个取值,例如在员工离职预测模型案例中,X的取值就是两种:“离职”与“不离职”;pi表示随机变量X取值为Xi发生的概率,且有∑pi = 1。此外注意这里的对数函数是以2为底即是。
同样举例来说,对于一个全部都是离职员工的样本来说,里面只有一个类别:离职员工,其出现的频率是100%,所以该系统信息熵为,表示该系统没有混乱。而如果样本里一半是离职员工,另一半是未离职员工,那么类别个数为2,每个类别出现的频率都为50%,所以其信息熵为
,也即其混乱程度很高。
当引入某个用于进行分类的变量(比如“满意度<5”),则根据变量A划分后的信息熵也被称之为条件熵,其公式为:
其中S1、S2为划分成两类的样本量,H(X1)和H(X2)为划分后的两类各自的信息熵。
与之前计算基尼系数减少值类似,这里同样是计算信息熵的减少值(原系统熵值 - 划分后的系统熵值),该减少值称之为熵增益或信息增益,其值越大越好,越大表明在进行分类后的混乱程度越低,也即分类越准确。
信息增益的计算公式如下所示:
以之前的例子来解释信息熵的概念与使用,初始样本中有1000个员工,其中已知有400人离职,600人不离职。其划分前该系统的信息熵为,可见混乱程度较高,下面采用两种不同的划分方式来决定初始节点:1、根据“曾经离职”进行分类;2、根据“收入<10,000”进行分类。
方式1:以“满意度<5”为初始节点进行划分,如下图所示,划分后的信息熵为0.65,熵增益或者说信息增益为0.32。
方式2:以“收入<10,000”为初始节点进行划分,如下图所示,划分后的基尼系数为0.96,熵增益或者说信息增益为0.046。
根据方式1划分后的信息增益为0.32,大于根据方式2划分后的信息增益0.046,因此我们选择根据方式1来进行决策树的划分,这样能更好的降低系统的混乱程度,从而进行更加合理的分类。这个和之前用基尼系数来计算的最终结论都是一样的。
在决策树模型搭建中,因为基尼系数涉及是平方运算,而信息熵涉及的则是复杂的一点的log对数函数运算,因此目前决策树模型默认使用基尼系数来进行运算,这样运算速度会较快。
商业实战中的数据量通常很大,再计算不同情况下的基尼系数或者信息熵就不是人力所能完成的,这时候就需要利用机器不停地训练来找到最佳的分裂节点,而在Python中,则有相应的Scikit-Learn库来帮助快速建立一个决策树模型,如果是通过第1章所讲的通过Anaconda安装的Python,那么这个库已经自动安装好了,下面我们就来讲解一下决策树模型的简单代码实现。
决策树模型既可以做分类分析(即预测分类变量值),也可以做回归分析(即预测连续变量值),分别对应的模型为分类决策树模型(DecisionTreeClassifier)及回归决策树模型(DecisionTreeRegressor)。
1.分类决策树模型(DecisionTreeClassifier)
分类决策树模型简单代码演示如下所示:
from sklearn.tree import DecisionTreeClassifier
X = [[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]]
y = [1, 0, 0, 1, 1]
model = DecisionTreeClassifier(random_state=0)
model.fit(X, y)
print(model.predict([[5, 5]]))
第1行代码引入分类决策树模型:DecisionTreeClassifier;
第2行代码中X是特征变量,共有5个训练数据,每个数据有2个特征,如数据[1, 2],它的第一个特征的数值为1,第二个特征的数值为2;
第3行代码中y是目标变量,共有两个类别:0和1;
第5行引入模型并设置随机状态参数random_state为数字0,这个数字本身没有特殊含义,可以换成别的数字。它是一个种子参数,使得每次代码运行的结果都一致,该参数将在本节的补充知识点讲解;
第6行通过fit()函数训练模型;最后1行通过predict()函数进行预测,预测结果如下:
[0]
可以看到对于数据[5, 5]来说,它被分类到了0这一类别。
如果要同时预测多个数据,则可以写成如下形式:
print(model.predict([[5, 5], [7, 7], [9, 9]]))
预测结果如下:
[0 0 1]
为方便大家理解,利用5.2.3节将讲到的决策树可视化技巧将决策树可视化,如下图所示:
首先讲一些基本概念:图中的X[0]表示的是数据的第一个特征,X[1]表示的则是数据的第二个特征;gini表示的该节点的基尼系数(以根节点为例,它的基尼系数计算公式为:);samples表示该节点的样本数;value表示的是各个种类所占的数量,例如根节点中[2, 3]表示分类为0的样本数为2,分类为1的样本数为3;class表示的是该区块划分的类别,它是由value中哪个类别的数量多来决定的,比如根节点中分类为1的样本数(3)大于分类为0的样本数(2),所以该节点的分类为1,其余依次类推。
最上面的节点,也就是根节点是以X[1]是否小于等于7作为节点划分依据,如果满足该条件(即为True),则划分到左边的子节点,否则(即为False)划分到右边的节点。以数据[5, 5]为例,在根节点的时候,它满足X[1](也即第二个特征数值)小于等于7的条件,所以被划分到左边的子节点。在该子节点又进行一次判断,判断X[0]是否小于等于2,因为X[0]值为5,不满足该条件,所以划分到该子节点的右边的节点,而该节点中的类别class为0,所以[5, 5]这个数据在该决策树模型下就被预测为类别0了。
补充知识点:random_state参数的作用解释
在引入决策树模型的时候,我们设置了random_state随机状态参数,设置这个参数的原因是,决策树模型会优先选择使整个系统基尼系数下降最大的划分方式来进行节点划分,但是有可能(尤其当数据量较少的时候),根据不同的划分方式获得的基尼系数下降是一样的。下图所示为不设置random_state参数时多次运行后获得的不同的决策树:
可以看到它们的节点划分方式是不同的,这样会导致同样的数据的预测结果有所不同,例如数据[7, 7]在左边的决策树中会被预测为类别0,而在右边的决策树中会被预测为类别1。
此时有的读者就会有疑问了,为什么模型训练后会产生两颗不同的树呢,哪棵树是正确的呢?其实两颗树都是正确的,出现这种情况的原因,是因为根据“X[1]<=7”或者“X[0]<=6”进行节点划分时产生的基尼系数下降是一样的(都是0.48 - (0.60.444 + 0.40) = 0.2136),所以无论以哪种形式进行节点划分都是合理的。产生这一现象的原因大程度是因为数据量较少所以容易产生不同划分方式产生的基尼系数下降是一样的情况,当数据量较大时出现该现象的几率则较小。
总的来说,对于同一个模型,不同的划分方式可能会导致最后的预测结果会有所不同,设置random_state参数(可以设置成0,也可以设置成1或123等任意数字)则能保证每次的划分方式都是一致的,使得每次运行的结果相同。这个概念对于初学者来说还是挺重要的,因为初学者往往会发现怎么每次运行同一个模型出来的结果都不一样,从而一头雾水,如果出现这种情况,那么设置一下random_state参数即可。
2.回归决策树模型(DecisionTreeRegressor)
除了进行分类分析外,决策树还可以进行回归分析,即预测连续变量,此时的决策树便被称之为回归决策树,回归决策树模型简单代码演示如下所示:(以银行客户价值为例子来理解)
from sklearn.tree import DecisionTreeRegressor
X = [[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]]
y = [1, 2, 3, 4, 5]
model = DecisionTreeRegressor(max_depth=2, random_state=0)
model.fit(X, y)
print(model.predict([[9, 9]]))
其中X是特征变量,其共有2个特征;y是目标变量,它是一个连续变量;第5行引入模型,并设置决策树最大深度参数max_depth为2以及随机状态参数random_state为0;第6行通过fit()函数训练模型;最后1行通过predict()函数进行预测,预测结果如下:
[4.5]
可以看到,对于[9, 9]这一数据,其预测拟合值为4.5。
回归决策树模型的概念和分类决策树基本一致,最大的不同就是其切分标准不再是信息熵或是基尼系数,而是均方误差MSE,均方误差MSE的计算公式如下所示:
其中n为样本数量,为实际值,
为拟合值(预测值)。
为方便大家理解,利用5.2.3节将讲到的决策树可视化技巧将决策树可视化,如下图所示:
图中的X[0]表示的是数据的第一个特征,X[1]表示的则是数据的第二个特征;mse表示的该节点的均方误差;samples表示该节点的样本数;注意这里的value表示的是该节点的拟合值,在回归决策树里,节点中所有数据的均值作为该节点的拟合值,对于最终的叶子节点而言,其拟合值就是最终的回归模型预测值。
举例来说,对于根节点,它里面一共有5个数据,这里是将节点中所有数据的均值作为该节点的拟合值,因此对于该节点来说,其拟合值为(1+2+3+4+5)/5=3,因此其均方误差MSE如下所示为数字2,和程序获得的结果是一致的。
对于回归决策树而言,其目的就是使得最终系统的均方误差最小,其节点划分依据也是基于这个理念,如根节点是根据“X[1]<=5”进行划分的,这个使得系统的均方误差下降最大(2-(0.40.25 + 0.60.667) = 1.5)。
如果不限制决策树的深度,那么决策树将一直往下延伸,直到所有叶子节点中的均方误差MSE都等于0为止,这里因为设置了树的最大深度参数max_depth为2,所以决策树在根节点往下共有两层,如果不设置这一参数,那么右下角的节点还将继续分裂,直至所有节点的均分误差值都为0为止。这里设置最大深度参数max_depth的原因,一是为了方便演示拟合的效果(拟合结果是4.5而不是一个整数,显得是回归结果,而不是分类结果),二是为了防止模型出现过拟合的现象(过拟合的知识点可参见本书3.2.3节)。在实战中我们也通常会设置最大深度参数max_depth主要防止模型出现过拟合的现象。
至于[9, 9]这一数据为什么拟合结果是4.5,相信大家看上面的图应该就一目了然了。
在实际应用,分类决策树模型用的相对较多一些,不过分类决策树和回归决策树模型都很重要,之后8、9、10章讲的的集成模型随机森林模型、AdaBoost模型、GBDT模型、XGBoost与LightGBM模型都是基于决策树模型进行搭建的。这里了解了决策树模型的简单使用后,下一节我们将结合具体商业案例讲解如何使用Python搭建决策树模型。
补充知识点:过拟合与欠拟合
如下图所示,所谓过度拟合(简称过拟合),是指模型在训练样本中拟合程度过高,虽然它很好地贴合了训练集数据,但是却丧失了泛化能力,模型不具有推广性(即如果换了训练集以外的数据就达不到较好的预测效果),导致在测试数据集中表现不佳。此外与过拟合相对的则是欠拟合,欠拟合是指模型拟合程度不高,数据距离拟合曲线较远,或指模型没有很好地捕捉到数据特征,不能够很好地拟合数据。
本节将通过搭建员工离职预测模型来学习决策树模型在人才决策领域的应用,并会讲解如何评估决策树模型,最后通过可视化的方式呈现决策树模型。
员工离职预测模型的目的是通过已有的员工信息和离职表现来搭建合适的模型,从而预测之后的员工是否会离职。
1.数据读取与预处理
首先读取员工信息以及其交易离职表现,即是否离职记录,代码如下:
import pandas as pd
df = pd.read_excel('员工离职预测模型.xlsx')
df.head()
运行结果如下表所示,其中共有15000组历史数据,其中前3571个为离职员工数据,后11429个为非离职员工数据,其中“离职”列中,数字1代表离职,数字0代表未离职。我们的目的就是根据这些历史数据搭建决策树模型来预测之后的员工离职可能性。
因为Python数学建模中无法识别文本内容,而在原始数据中,“工资”被分成了三个等级“高”、“中”、“低”。所以“工资”列中的内容需要进行数值处理,这里通过pandas库中的中replace()函数进行处理,其中“高”用“2”表示,“中”用“1”表示,“低”用“0”表示。
df = df.replace({'工资': {'低': 0, '中': 1, '高': 2}})
df.head()
文本处理后,结果如下图所示:
除了通过replace()函数来进行文本数据处理外,在11.1节还有讲Get_dummies哑变量处理以及Label Encoding编号处理两种处理手段,感兴趣的读者可以进行阅读。
数据表中“离职”作为目标变量,剩下的字段作为特征变量,通过一个员工的特征来判断他是否会离职。为了方便演示,这里只选取了6个特征变量,在商业实战中用到的特征变量会比案例中的多得多。接下来进入决策树模型的搭建,这是大部分机器学习模型搭建中的常规步骤。
2.提取特征变量和目标变量
首先将特征变量和目标变量单独提取出来,代码如下:
X = df.drop(columns='离职')
y = df['离职']
通过drop()函数删除“是否离职”这一列,将剩下的数据作为特征变量赋值给变量X。然后通过DataFrame提取列的方式提取“是否离职”这一列作为目标变量,并赋值给变量y。
3.划分训练集和测试集
提取完特征变量后,我们需要将原来的15000个数据拆分为训练集及测试集。顾名思义,训练集拿来做训练,而测试集拿来检验模型训练的结果。
划分训练集和测试集的代码如下:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=123)
第1行代码便是从Scikit-Learn库中引入train_test_split()函数;
第2行代码则是通过train_test_split()函数划分训练集和测试集,其中X_train,y_train为训练集数据,X_test,y_test则为测试集中的数据。
train_test_split()函数的前两个参数X和y便是之前提取的特征变量和目标变量;test_size是测试集数据所占的比例,这里选择的是20%数据为测试数据,也即0.2。如果数据的数量很多,也可以将其设置为0.1,也即分配相对较少的数据用来测试,分配更多比例的数据用来训练。
因为通过train_test_split()函数每次划分数据都是是随机的,所以如果想每次划分数据产生的内容都是一致的,可以设置random_state参数,这里设置的数字123没有特殊含义,它只是相当于一个种子参数,使得这样每次划分时内容都是一致的,也可以设置成其他值。
划分后的数据如下图所示:
4.模型训练及搭建
划分为训练集和测试集之后,就可以从Scikit-Learn库中引入决策树模型进行模型训练了,代码如下:
from sklearn.tree import DecisionTreeClassifier
model = DecisionTreeClassifier(max_depth=3, random_state=123)
model.fit(X_train, y_train)
其中第一行代码从Scikit-Learn库中引入分类决策树模型:DecisionTreeClassifier;
第二行代码将决策树模型赋值给model变量,同时设置模型参数max_depth为3,即树的最大深度为3,该模型参数的概念在之后5.3节的参数调优章节还会有讲解,并设置随机状态参数random_state为数字123,这个数字没有特殊意义,只是使得每次运行结果一致;
第三行代码通过fit()函数来进行模型的训练,传入的参数就是前面获得的训练集数据。
至此,一个决策树模型便已经搭建完成了,这里把决策树模型搭建的代码汇总如下:
# 1.读取数据与简单预处理
import pandas as pd
df = pd.read_excel('员工离职预测模型.xlsx')
df = df.replace({'工资': {'低': 0, '中': 1, '高': 2}})
# 2.提取特征变量和目标变量
X = df.drop(columns='离职')
y = df['离职']
# 3.划分训练集和测试集
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=123)
# 4.模型训练及搭建
from sklearn.tree import DecisionTreeClassifier
model = DecisionTreeClassifier(max_depth=3, random_state=123)
model.fit(X_train, y_train)
模型搭建完成后就可以利用模型来进行预测了,此时前面的测试集就可以发挥作用了,即利用测试集来进行预测并评估模型的预测效果。
本小节将介绍如何直接预测是否离职,以及预测不离职&离职的概率,最后将介绍如何合理地对一个模型进行评估。
1.直接预测是否离职
搭建模型的目的是为了利用它来预测数据,这里把测试集中的数据导入到模型中来进行预测,代码如下,其中model就是上一节搭建的决策树模型。
y_pred = model.predict(X_test)
通过打印y_pred[0:100],查看结果如下所示,其中0和1为预测的结果,0为预测不离职,1为预测离职。
利用创建DataFrame相关知识点,将预测的y_pred和测试集实际的y_test汇总到一起,其中y_pred是一个numpy.ndarray一维数组结构,y_test为Series一维序列结构,所以需要用list()函数将其转换为列表,代码如下:
a = pd.DataFrame() # 创建一个空DataFrame
a['预测值'] = list(y_pred)
a['实际值'] = list(y_test)
通过a.head()将生成的DataFrame前五行打印输出如下。
可以看到测试数据集中前五组数据的预测准确度为100%,如果要查看整体的预测准确度,可以采用如下代码:
from sklearn.metrics import accuracy_score
score = accuracy_score(y_pred, y_test)
通过将score打印输出,结果如下:
0.9573
发现整个模型在测试集上的预测准确度为0.9573,即3000个测试集数据中,有2872人预测结果和实际结果相符。
此外,我们也可以通过模型自带的score函数来查看模型的预测准确度,代码如下:
model.score(X_test, y_test)
将其打印,获得的结果同样为0.9573。
2.预测不离职&离职概率
其实分类决策树模型本质预测的并不是准确的0或1的分类,而是预测其属于某一分类的概率,可以通过如下代码查看预测属于各个分类的概率:
y_pred_proba = model.predict_proba(X_test)
此时获得的y_pred_proba就是预测的属于各个分类的概率,它是一个二维数组,可以直接打印y_pred_proba,其左侧一列数为分类为0的概率,右侧一列数为分类为1的概率;也可以通过如下代码将其装换为DataFrame格式方便查看:
b = pd.DataFrame(y_pred_proba, columns=['不离职概率', '离职概率'])
通过打印b.head()查看此时获取的表格前5行,如下所示:
之前直接预测是否离职时,其实是看属于哪种分类的概率最大,如第一行数据,其中不离职的概率为0.9852,大于离职的概率0.0147,所以才预测其不离职。
有些细心的读者可能已经发现,上面一些概率是一样的,比如有第一个和第二个员工的不离职概率都是0.9852,离职概率都是0.0147,在下一小节将模型可视化呈现后大家就会明白这些概率的计算原理了。
如果想查看离职概率,即查看y_pred_proba的第二列,可以采用如下代码,这个是二维数组选取列的方法,其中逗号前的“:”表示所有行,逗号后面的数字1则表示第二列,如果把数字1改成数字0,则提取第一列不离职概率。
y_pred_proba[:,1]
3.模型预测效果评估
在第四章逻辑回归模型4.3节提到过,对于分类模型而言,我们不仅关心其预测的准确度,更关心下面两个指标:命中率(所有实际离职的员工中被预测为离职的比率)和假警报率(所有实际不离职的员工中被预测为离职的比率),也即通过两者绘制的ROC曲线来评判模型。
我们希望在相同阈值的情况下,假警报率尽量小,命中率尽可能的高,也即ROC曲线尽可能的陡峭,其对应的AUC值(ROC曲线下的面积)尽可能的高。
在Python实现上,通过4.3节讲过的代码就可以求出在不同阈值下的命中率(TPR)以及假警报率(FPR)的值,从而可以绘制ROC曲线。
from sklearn.metrics import roc_curve
fpr, tpr, thres = roc_curve(y_test, y_pred_proba[:,1])
其中第一行代码引入roc_curve()函数。第二行代码传入测试集目标变量y_test的值,以及预测的离职概率,然后通过roc_curve()函数计算出不同阈值下的命中率和假警报率,并将三者赋值给变量fpr(假警报率)、tpr(命中率)、thres(阈值),此时获得fpr、tpr、thres为三个一维数组。注意roc_curve()函数返回的是一个含有3个元素的元组,其中默认第一个元素为假警报率、第二个元素为命中率,第三个元素为阈值,所以在第二行代码中写变量顺序的时候要按fpr、tpr、thres的顺序来写。
通过4.3节相关代码可以查看不同阈值下的假警报率和命中率,代码如下:
a = pd.DataFrame() # 创建一个空DataFrame
a['阈值'] = list(thres)
a['假警报率'] = list(fpr)
a['命中率'] = list(tpr)
此时表格a如下表所示:
其中有几个注意的点,第一行表示只有当一个员工被预测离职的概率>=200%,才判定其会离职,因为概率不会超过100%,所以此时不会有人被判定为离职,即所有人都不会被预测为离职,那么命中率和假警报率都为0(在本书4.3.2节也解释了为什么会有这样一个没有什么意义的阈值的原因)。第二行表示只有当一个员工预测为离职的概率>=100%(因为概率不会超过100%,所以其实就是预测为离职的概率为100%),才判定其会离职,如上表所示,此时命中率为24.7%,也即所有实际离职的员工中被预测为离职的比率为24.7%,在这种极端的阈值条件下,该命中率已经很高了,这个其实这个在下一节学完模型可视化呈现中会有更好的理解;
第三行表示只有当一个员工预测为离职的概率>=94.6%,才判定其会离职,此时的命中率为67.8%,假警报率为0.82%,其余以此类推。
已知了不同阈值下的假警报率和命中率,可通过matplotlib库可绘制ROC曲线,代码如下:
import matplotlib.pyplot as plt
plt.plot(fpr, tpr)
plt.show()
绘制的ROC曲线如下图所示,可以看到这个ROC曲线还是很陡峭的。
通过如下代码则可以快速求出模型的AUC值:
from sklearn.metrics import roc_auc_score
score = roc_auc_score(y_test, y_pred_proba[:,1])
其中第一行代码引入roc_auc_score()函数。第二行代码传入测试集目标变量y_test的值,以及预测的离职概率。将获得AUC值打印出来为:0.973,可以说预测效果还是不错的。
4.特征重要性评估
模型搭建完成后,有时我们希望能够知道各个特征变量的重要程度,即哪些特征变量在模型中起的作用更大,而在决策树模型中,通过如下一行代码即可查看特征重要性:
model.feature_importances_
获得结果如下,这些特征重要性之和为1。
array([0, 0.59810862, 0.14007392, 0.10638659, 0.00456495, 0.15086592])
在决策树模型中,特征重要性的大小在于该变量对于整体的基尼系数下降的贡献度大小,该特征变量的对模型整体的基尼系数下降的贡献越大,那么其特征重要性则越大。举个例子,模型分裂到最后的叶子节点,整个系统的基尼系数下降数值为0.3,如果所有根据特征A进行分裂的节点产生的基尼系数下降的数值和为0.15,那么特征A的特征重要性则为50%,也即0.5。
对于特征变量不多的模型,我们通过上面一行代码即可查看各个特征变量的重要性,但是如果特征变量多了之后,可以使用如下的代码将特征重要性和变量名称一一对应:
features = X.columns # 获取特征名称
importances = model.feature_importances_ # 获取特征重要性
# 通过二维表格形式显示
importances_df = pd.DataFrame()
importances_df['特征名称'] = features
importances_df['特征重要性'] = importances
importances_df.sort_values('特征重要性', ascending=False)
其中前两行代码获取特征名称及特征重要性,然后通过2.2.1节构造二维表格DataFrame的相关知识点将特征名称和特征重要性整合到一个二维表格中,最后通过sort_values()函数按特征重要性进行排序,其中设置ascending参数为False表示按逆序排序(即从大到小排序)。
补充说一句,其中整理成二维表格也可以通过如下一行代码实现,注意此时表格是横向显示的,即其行索引为特征名称和特征重要性,所以需要通过.T方法来将其转置为竖向显示。
importances_df = pd.DataFrame([features, importances], index=['特征名称', '特征重要性']).T
此时获取表格内容如下表所示,这样特征名称和特征重要性就一一对应上了。
可以看到重要性最高的便是第一个特征变量:满意度,这个也的确符合常理,如果员工对工作的满意度高,其离职的概率相对较小,反之则比较大。此外,其他重要的是:考核得分和工龄;工资在该模型中的特征重要性为0,也就是说它们没有发挥作用,这个某种程度是因为我们限制了决策树的深度为3层(max_depth=3),所以工资这个特征变量没有发挥作用的机会,如果增大决策树的最大深度,那么工资可能会发挥作用,这个在5.3.2节也有验证。另外一个更重要的原因是在本案例中,工资不是具体的数值,而是被分为了“高”、“中”、“低”三个区间,这种划分方式过于宽泛,从而工资这一变量在决策树模型中发挥作用较小,如果这里的工资是员工实际的工资收入,如10,000元,那么应该会发挥更大的作用。
如果想将决策树模型可视化展示出来,可以使用Python的graphviz插件。因为模型可视化呈现主要是为了演示和教学,在真正实战中应用较少,所以对于graphviz的安装与使用,感兴趣的读者可以参考笔者编写的教程:https://shimo.im/docs/Dcgw8H6WxgWrc8hq/ ,或扫描如下二维码进行查看,此外在本书配套的源代码中也会提供相应的代码和教程。
这里简单提一下graphviz插件使用的核心知识点:
第一步是安装graphviz插件,其安装官方网站为:https://graphviz.gitlab.io/download/,如果是Windows系统,则下载msi文件并安装,安装完成后需要配置环境变量,具体配置方法参考上面给的教程链接或通过二维码扫码观看。
第二步是安装graphviz库,使用pip install graphviz即可安装相关库。
安装完毕后即可使用,其核心代码如下,通过如下代码即可快速将决策树模型进行可视化。如果想生成包含中文的可视化图片则相对麻烦一些,不过笔者也研究出了相应的解决办法,感兴趣的读者参考上面给的教程链接或通过二维码扫码观看。
from sklearn.tree import export_graphviz
import graphviz
dot_data = export_graphviz(model, out_file=None) # 这里的model就是模型名
graph = graphviz.Source(dot_data) # 将决策树模型模型可视化
graph.render('决策树可视化') # 生成决策树可视化PDF文件
下图所示便是通过graphviz生成的可视化决策树模型:
可以看到从初始节点往下的确就只有3层树状结构了,这个也即是5.2.1小节所设置的模型参数max_depth,即树的最大深度。设置最大深度的原因,一个是为了方便演示,另外一个原因这是因为如果树的深度过深则会导致模型的过度拟合,预测效果下降。
下面将根据上面这幅图来讲解几个重要的知识点来加深大家对决策树模型的理解。
知识点1:节点各元素含义
这里介绍下图中每个节点里内容的含义,除了叶子节点外,每个节点都有5个元素:分裂依据、gini(当前基尼系数)、samples(当前样本数量)、value(样本中各类别的数量)、class(分类类别)。
以根节点为例,其分裂依据是根据满意度是否小于等于4.65作为分裂依据的;它当前的基尼系数为0.365;其含有的样本总数为12000;其中value中的左边的数值9120表示“是否离职”中的0,也即不离职员工,右边的数值2880表示“是否离职”中的1,也即离职员工;最后一行的class表示分类,因为在这个节点上不离职员工数(9120)多于离职员工数(2880),所以class为不离职,不过根节点的class没有什么意义,我们主要看最后叶子节点的class情况。此外,最后的叶子节点因为已经分裂完毕,所以不再有分裂依据这一项。
知识点2:节点划分与其依据验证
根节点分裂完后产生两个子节点,其中左边的子节点中大部分为离职员工(共3367人,其中1325人未离职、2042人离职,该节点的基尼系数为0.477);而右边的节点中大部分为不离职员工(共8633人,其中7795人未离职、838人离职,该节点的基尼系数为0.175),这也的确符合现实中如果满意度较低则出现离职可能性较大的经验。
通过5.1.2小节中的知识点计算经过根节点分裂后的系统的基尼系数为3367/120000.477 + 8633/120000.175 = 0.260(此时基尼系数下降值为0.365 - 0.260 = 0.105),这个也是机器通过不停的训练和计算获得的最优解,如果通过别的方式进行根节点分裂后的系统基尼系数一定会比这个大,基尼系数的下降值一定比这个小。
知识点3:特征重要性与整棵树的关系
这里还可以通过上一节计算特征变量的特征重要性的代码来验证上面的观点:
model.feature_importances_
其打印输出结果如下,分别对应6个特征变量的特征重要性。
array([0, 0.59810862, 0.14007392, 0.10638659, 0.00456495, 0.15086592])
可以看到这里重要性最高的便是满意度。同样这里可以更好地解释下为什么工资这一特征变量的特征重要性为0,这个是因为该因素在该模型中没有发挥作用,这可以从可视化的图形中看出,每一个分叉的节点都没有依据“工资”这个特征变量进行分裂,所以说这个特征变量并没有发挥作用。如果把max_depth最大深度设置更大一些,让决策树可以往下继续分裂,那么这个特征变量可能将发挥作用,使得特征重要性不再是0。
此外,我们之前提到过,在决策树模型中,特征重要性的大小在于该变量对于整体的基尼系数下降的贡献度大小,这里可以利用这个可视化后的决策树,通过演示第二个特征变量“满意度”的特征重要性为什么为0.598来验证下该观点。
首先需要计算整体的基尼系数下降,如下图所示,根据不同叶子节点的样本数进行权重求和,新系统的基尼系数为0.0814,整体基尼系数下降为0.365 - 0.0814=0.2836。
以“满意度”为例,在上面的决策树中它共在根节点和下部中间的节点发挥了作用,对系统产生的基尼系数下降分别为0.105(0.365 - (3367/120000.477 + 8633/120000.175) = 0.105)和0.0646(1971/12000*0.484 - ((714/12000)*0 + (1257/12000)*0.142) = 0.0646),两者之和为0.1696,这个就是“满意度”这一特征变量在整个模型中发挥的作用,将其除以整体的基尼系数下降值便是它的特征重要性:0.1696/0.2836=0.598,这个与代码的获得特征重要性一致。
知识点4:叶子停止分裂的依据
叶子停止分裂的依据主要有两个:已经分裂结束无法再分裂,或者达到限定的分裂条件。比如右下角的叶子节点的基尼系数都为0了,也就是说这个叶子节点的纯度已经最高了(即里面所有的元素都是同一类别),已经不需要也无法再分裂了。而有些叶子节点的基尼系数还没有到达0的,因为限制了树的最大深度为3,所以也不会继续向下分裂了。此外,叶子节点因为不需要继续分裂,所以不存在分裂依据这一元素。
知识点5:不离职&离职概率与叶子节点的关系
注意,之前在5.2.2小节提到的不离职&离职概率的计算就是基于叶子节点,如果分到了左下角第三个叶子节点,那么其不离职概率为0,离职概率为100%,所以判断其为离职;如果分到最左边叶子节点,这个节点里总共有1295个数据,不离职的有70人,离职的有1225人,那么如果一个新的员工被分到该叶子节点,那么判定该员工离职概率为1225/1295=0.946,不离职概率则为70/1295=0.054,因为离职概率大于不离职概率,所以判定其离职,其余则依次类推。
知识点6:ROC曲线与叶子节点的关系
此外,感兴趣的读者可以将其余叶子节点所反映出来的离职概率计算一下(从左边叶子节点至右边的叶子节点,各节点离职概率分别为94.6%、5.94%、100%、7.72%、1.47%、100%、4.58%、71.4%),再观察上一小节绘制ROC曲线时用到的阈值,**会发现上一小节绘制ROC曲线时使用的阈值并不是随意选取的,而就是这些不同叶子节点反映出来的离职概率。**ROC曲线的绘制就是以这些离职概率的值作为阈值来看不同阈值下的命中率(TPR)和假警报率(FPR)。此外,因为这里很多叶子节点的离职概率是100%,这也解释了为什么之前5.2.2小节取100%作为阈值,仍然有24.7%命中率的原因。
通过这个图便能够较好的理解决策树的运行逻辑,当来了一个新的数据的时候,就会从最上面的根节点开始进行判断,如果满足满意度<=4.65,则划分到左边的节点进行之后一系列的判断,如果不满足则分到右边的节点进行之后一系列的判断,最终这个新的数据会被划分到其中的一个叶子节点中去,从而完成对数据的预测。
补充知识点:决策树可视化 - graphviz插件的安装及使用
这里简单讲解下决策树可视化技巧:graphviz插件的安装及使用供感兴趣的读者参考。其详细原理可以参考本书附赠的源代码文件中的PDF文档,这里主要讲解核心要点。
1.graphviz插件安装
首先需要安装一下graphviz插件,其下载地址为:https://graphviz.gitlab.io/download/,以Windows版本为例,在下载网站上选择下图框中内容:Stable 2.38 Windows install packages。
然后下载跳转界面中的msi文件:graphviz-2.38.msi,如下图所示。下载完该msi文件后点击文件进行安装,注意,要记住安装的文件路径,之后进行环境变量部署的时候会用到,通常默认路径为:C:\Program Files (x86)\Graphviz2.38\,建议不要修改。
2.安装graphviz库
以Windows系统为例,Win + R组合键调出系统运行框,输入cmd后点击确定,在弹出框内输入pip install graphviz,按一下回车键等待安装结束即可。如果在Jupyter Notebook编辑器中则通过在代码框中运行代码!pip install graphviz即可。
3.graphviz库的使用
通过如下代码就可以生成一个可视化的决策树模型:
from sklearn.tree import export_graphviz
import graphviz
import os
os.environ['PATH'] = os.pathsep + r'C:\Program Files (x86)\Graphviz2.38\bin'
dot_data = export_graphviz(model, out_file=None, class_names=['0', '1'])
graph = graphviz.Source(dot_data)
graph.render("result") # 导出成PDF文件
前2行引入使用graphviz的相关库;
第3,4行设置环境变量,这样才能在Python中真正使用graphviz插件,这个属于手动设置环境变量的方法(如果想在整个电脑系统而非单个代码文件设置环境变量,可以参考本章源代码文件中附赠的PDF文档),这里graphviz插件的安装路径为:“C:\Program Files (x86)\Graphviz2.38”,环境变量部署就是将软件所在文件路径中的bin文件夹部署到运行环境中,如果是其他安装路径,那么自行修改即可,
第5行通过export_graphviz()方法将之前搭建的决策树模型model转换为字符串格式并赋值给dot_data,其中注意需要设定out_file参数为None,这样获得的才是字符串格式;
第6行代码则是将dot_data转换成可视化的格式;
第7行代码则是通过render()方法将图像输出出来,通过上述代码默认是输出一个名为result的PDF文件,PDF文件如下图所示:
其中X[1]就表示第2个特征变量:满意度,X[3]则表示第4个特征变量:工程数量,X[5]则表示第6个特征变量:工龄;gini则表示该节点的基尼系数;samples则表示该节点中的样本数,比如说第一个节点,也即根节点中的12000也即训练集中的样本数量;value则表示不同种类所占的个数,比如说根节点中value左边的9120则表示不离职员工的数量,2880则表示离职员工的数量,class=0则是认为该节点为未流失节点。
如果想生成本小节开始展示的彩色且含有中文的可视化结果(因为graphviz不直接支持中文,所以需要进行如下特殊处理),可以采用如下代码。该代码看着稍有些复杂,不过对于读者朋友而言,使用时也只需调整第7行代码export_graphviz()函数括号中的model(模型名称)、feature_names(特征名称)和class_names(类别名称)3个内容即可。
from sklearn.tree import export_graphviz
import graphviz
import os # 以下两行是环境变量配置,使得Python可以使用graphviz插件
os.environ['PATH'] = os.pathsep + r'C:\Program Files (x86)\Graphviz2.38\bin'
# 生成dot_data
dot_data = export_graphviz(model, out_file=None, feature_names=X_train.columns, class_names=['不离职', '离职'], rounded=True, filled=True) # rounded和字体有关,filled设置颜色填充
# 将生成的dot_data内容导入到txt文件中
f = open('dot_data.txt', 'w')
f.write(dot_data)
f.close()
# 修改字体设置,避免中文乱码!
import re
f_old = open('dot_data.txt', 'r')
f_new = open('dot_data_new.txt', 'w', encoding='utf-8')
for line in f_old:
if 'fontname' in line:
font_re = 'fontname=(.*?)]'
old_font = re.findall(font_re, line)[0]
line = line.replace(old_font, 'SimHei')
f_new.write(line)
f_old.close()
f_new.close()
# 以PNG的图片形式存储生成的可视化文件
os.system('dot -Tpng dot_data_new.txt -o 决策树模型.png')
print('决策树模型.png已经保存在代码所在文件夹!')
# 以PDF的形式存储生成的可视化文件
os.system('dot -Tpdf dot_data_new.txt -o 决策树模型.pdf')
print('决策树模型.pdf已经保存在代码所在文件夹!')
机器学习的各个模型其实都有些内置的参数,比如上面提到的决策树模型很重要的一个参数:max_depth(树的最大深度),这种参数也被称之为超参数,除了max_depth外,决策树模型还有些常用参数,如criterion(特征选择标准)、min_samples_leaf(叶子节点的最少样本数)等,更多参数可以参考本节的补充知识点:决策树模型的超参数。
在大多数情况下,使用模型的默认参数也能获得较好的结果及预测准确度,然而如果想要将模型精益求精,那么就需要对模型的超参数进行调优,例如max_depth取3,还是取max_depth的默认值None(不设置最大深度,即分裂到所有叶子节点的基尼系数都为0)都是有考究的,如果max_depth取的过小可能会导致模型欠拟合,而如果取的过大,则容易导致模型过拟合(欠拟合和过拟合的知识点可参见上一章4.2.3节),因此便需要一个手段来调节这些参数。
这一节便将介绍调整模型参数的常用方法:GridSearch网格搜索以及参与其搭配使用的K折交叉验证,在学习GridSearch网格搜索之前,首先来了解下K折交叉验证的基本原理。
在机器学习中,因为训练集和测试集划分数据是随机的**,所以我们有时会重复地使用数据,来更好地评估模型的有效性,并选出最好的模型,该做法称之为交叉验证。具体而言就是对原始样本数据进行切分,然后组合成为多组不同的训练集和测试集,用训练集训练模型,用测试集评估模型。某次的训练集可能是下次的测试集,故而称作交叉验证。**
交叉验证有三种方法,分为简单交叉验证、K折交叉验证和留一交叉验证。其中K折交叉验证应用较为广泛,K折交叉验证是指将数据集随机等分为K份,每次选取K-1份为训练集训练模型,然后用剩下的1份作为测试集,得到K个模型后将这K个模型的平均测试效果作为最终的模型效果。
举例来说,下图所示便是3折交叉验证,即将数据随机等分为3份,然后每次随机选取2份数据作为训练集,剩下的1份作为测试集,反复3次,得到3次不同的测试效果,综合来看着3次不同的测试结果能更加准确的对模型进行评估。
通常来说,如果训练数据集相对较小,则增大k值,这样在每次迭代过程中将会有更多的数据用于模型训练,同时算法时间延长;如果训练集相对较大,则减小k值,这样降低模型在不同的数据块上进行重复拟合的性能评估的计算成本,在平均性能的基础上获得模型的准确评估。
除了更加精确地对模型进行评估,交叉验证另外一个重要的作用就是利用更加精确的评估结果对模型进行参数调优,它经常配合下一节要讲到的GridSearch网格搜索一起使用。
补充知识点:K折验证的代码实现
通过如下代码可以实现K折交叉验证,并获得每次验证的得分情况:
from sklearn.model_selection import cross_val_score
acc = cross_val_score(model, X, y, cv=5)
其中第1行代码引入交叉验证的函数cross_val_score函数;第2行代码使用cross_val_score函数进行交叉验证,其中分别传入模型名称(model)、特征变量数据(X)、目标变量数据(y)、交叉验证的次数(cv)。这里cv=5即表示交叉验证5次,每次随机取4/5的数据训练,1/5的数据用来测试(如果不填该参数,则cv值默认为3)。此外,这里没有设置scoring参数,即认为选择默认值’accuracy’(准确度)进行打分。
打印acc可以看到5次交叉验证得到的打分如下所示:
array([0.96666667, 0.96066667, 0.959 , 0.96233333, 0.91366667])
通过打印如下代码获得这五个得分的平均分:
acc.mean()
获得结果如下:
0.9524666666666667
上面交叉验证默认以准确度作为评价标准,如果想以ROC曲线的AUC值作为评分标准,则可以设置scoring参数为’roc_auc’,代码如下:
acc = cross_val_score(model, X, y, scoring='roc_auc', cv=5)
在之后要讲到的GridSearch网格搜索中,我们其实不再需要使用cross_val_socre函数,届时将使用GridSearchCV函数同时进行交叉验证和参数调优,所以关于cross_val_socre函数简单了解即可。
网格搜索是一种穷举搜索的调参手段:遍历所有的候选参数,循环建立模型并对模型的有效性和准确性进行评估,选取表现最好的参数作为最终结果。以决策树模型最大深度max_depth为例,我们可以在[1, 3, 5, 7, 9]这些不同的值中遍历,以准确度或者ROC曲线的AUC值作为评判标准来搜索最合适的max_depth值。如果要同时调节多个模型参数,例如模型有2个参数,第一个参数有4种可能,第二个参数有5种可能,所有的可能性列出来可以表示成4*5的网格,遍历的过程像是在网格(Grid)里搜索(Search),因此该方法也称之为GridSearch网格搜索。
1.单参数的参数调优
这里先以1个参数(max_depth)为例进行网格搜索演示,来快速了解机器学习如何进行参数调优。使用Scikit-Learn库中的GridSearchCV()方法对上方的决策树模型进行参数调优。
from sklearn.model_selection import GridSearchCV
parameters = {'max_depth': [1, 3, 5, 7, 9]}
model = DecisionTreeClassifier()
grid_search = GridSearchCV(model, parameters, scoring='roc_auc', cv=5)
第1行代码从Scikit-Learn库中引入GridSearchCV()方法;
第2行代码指定决策树模型中待调优参数max_depth的参数范围;
第3行代码构建决策树模型并将其赋值给变量model;
第4行代码将分类器和待调参的参数范围传入GridSearchCV()模型,并设置cv=5表示取5折交叉验证,即交叉验证5次,其默认值为3,这里设置scoring='roc_auc’表示以ROC曲线的AUC评分作为模型评价准则,如果不设置则默认以准确度评分’accuracy’为评价准则。
下面我们将数据传入网格搜索模型并输出参数最优值:
grid_search.fit(X_train, y_train) # 传入测试集数据并开始进行参数调优
grid_search.best_params_ # 输出参数的最优值
因为max_depth参数我们设置了5个候选项,又设置了5折交叉验证(cv=5),这样对于每个候选项模型都会运行5遍(因此模型共运行5*5=25遍),每个候选项都通过5折交叉验证获得一个平均分,根据平均分进行排序,参数最优值如下:
{'max_depth': 7}
即针对本案例,决策树的深度设置为7时最优,因为其5折交叉验证得到的平均分最高。
补充知识点:批量生成调参所需数据
此外,如果不想一个数字一个数字的敲参数值,可以使用2.1.3节的np.arange()函数,例如通过如下代码,便可以构造1到9,间隔为2的数据集(np.arange(1, 10, 2)中第一个元素1表示起始位置;第二个元素10表示终止位置,因为左闭右开的属性,所以取不到,这里也可以换成数字11;第三个元素2表示步长,即间隔为2,如果想数据密集些,可以将其设置为1)。
import numpy as np
np.arange(1, 10, 2) # 非Jupyter Notebook编辑器需要print函数将其打印出来
这样之前参数调优中第2行代码就可以简写为:
parameters = {'max_depth': np.arange(1, 10, 2)}
2.参数调优的效果检验
下面我们根据新的参数建模,并通过查看新模型的预测准确度以及ROC曲线的AUC值来验证参数调整后是否提高了模型的有效性。
首先重新搭建决策树模型,并将训练集数据传入其中:
model = DecisionTreeClassifier(max_depth=7) # 根据max_depth=7重新搭建模型
model.fit(X_train, y_train)
把测试集中的数据导入模型进行预测并通过Scikit-Learn库中的accuracy_score查看整体预测准确度:
y_pred = model.predict(X_test)
from sklearn.metrics import accuracy_score
score = accuracy_score(y_pred, y_test)
通过将score打印输出,发现整个模型在测试集上的预测准确度为0.982,即3000个测试集数据中,有2946人预测结果和实际结果相符。原模型在测试集上的预测准确度为0.957,现在提高到0.982,说明调参之后模型预测准确度有所上升。其实准确度也有可能下降,因为参数调优时我们是按ROC曲线的AUC值来进行评分的(scoring=‘roc_auc’)。
查看完预测准确度后,我们来查看ROC曲线的AUC值,首先通过如下代码查看预测属于各个分类的概率:
y_pred_proba = model.predict_proba(X_test)
如果想单纯的查看离职概率,可以查看y_pred_proba的第二列,即采用如下代码:
y_pred_proba[:,1]
通过如下代码则可以快速求出模型的AUC值:
from sklearn.metrics import roc_auc_score
score = roc_auc_score(y_test.values, y_pred_proba[:, 1])
将获得的AUC值打印出来为:0.987,原来获得的AUC值为0.973,相比之下可以证明调参后模型的有效性的确有所提高。
补充知识点:决策树深度增加时,特征重要性的改变
调参之后决策树模型的深度从3增加到7,树的子节点和叶子节点都有所增加,特征的重要性也可能发生变化。使用如下代码查看各个特征的重要性:
model.feature_importances_
其打印输出结果如下,分别对应5个特征变量的特征重要性。
array([0.00059222, 0.52728113, 0.13163818, 0.1116004 , 0.07759157,
0.1512965 ])
3.多参数调优
除了可以进行单参数调优外,GridSearch网格搜索还可以进行多参数同时调优,下面我们选择DecisionTreeClassifier()模型三个超参数:max_depth(最大深度),criterion(特征选择标准)和min_samples_split(子节点往下分裂所需的最小样本数),使用GridSearchCV()方法进行多参数调优,各参数含义可参考本节补充知识点:决策树模型的超参数。
from sklearn.model_selection import GridSearchCV
# 指定决策树分类器中各个参数的范围
parameters = {'max_depth': [5, 7, 9, 11, 13], 'criterion':['gini', 'entropy'], 'min_samples_split':[5, 7, 9, 11, 13, 15]}
# 构建决策树分类器
model = DecisionTreeClassifier() # 因为要进行参数调优,所以不需要传入参数了
# 网格搜索
grid_search = GridSearchCV(model, parameters, scoring='roc_auc', cv=5)
grid_search.fit(X_train, y_train)
# 输出参数的最优值
grid_search.best_params_
参数最优值如下:
{'criterion': 'entropy', 'max_depth': 11, 'min_samples_split': 13}
因此criterion(特征选择标准)设置为信息熵(‘entropy’);max_depth(最大深度)设置为11;min_samples_split(子节点往下分裂所需的最小样本数)设置为13时模型最优。引入模型的时候该采用如下写法:
model = DecisionTreeClassifier(criterion='entropy', max_depth=11, min_samples_split=13)
此时的预测准确度为0.9823;ROC曲线的AUC值为0.988,这两个值较之前的单参数调优获得结果都有所提升,感兴趣的读者可以自行验证,配套源代码中也有相关代码。
注意点1:多参数调优和分别单参数调优的区别
多参数调优和单参数分别调优是有区别的,比如有的读者为了省事,对上面的3个参数进行3次单独的单参数调优,然后将结果汇总,这样的做法其实是不严谨的。因为在进行单参数调优的时候,是默认其他参数取默认值的,那么该参数和其他参数都不取默认值的情况就没有考虑进来,也即忽略了多个参数对模型的组合影响。以上面的代码示例来说,使用多参数调优时,它是526=60种组合可能,而如果是进行3次单参数调优,则只是5+2+6=13种组合可能。
因此,如果只需要调节一个参数,那么可以使用单参数调优,如果需要调节多个参数,则推荐使用多参数调优。
注意点2:参数取值是给定范围的边界
另外一点需要需要注意的是,如果使用GridSearchCV()方法所得到的参数取值是给定范围的边界,那么有可能存在范围以外的取值使得模型效果更好,因此需要我们额外增加范围,继续调参。举例来说,倘若上述代码中获得的最佳max_depth值为设定的最大值13,那么实际真正合适的max_depth可能更大,此时便需要将搜索网格重新调整,如将max_depth的搜索范围变成[9, 11, 13, 15, 17],再重新参数调优。
此外,如果不想一个数字一个数字的敲,可以使用2.1.3节的np.arange()函数,例如通过如下代码,便可以构造9到17,间隔为2的数据集(第一个元素9表示起始位置;第二个元素19表示终止位置,因为左闭右开的属性,所以取不到,这里也可以换成数字18;第三个元素2表示步长,即间隔为2):
np.arange(9, 19, 2)
补充知识点1:决策树的前剪枝和后剪枝
这里补充讲解决策树模型中常被提前的一个知识点:决策树剪枝,决策树剪枝的目的是为了防止构建的决策树出现过拟合。决策树剪枝分为前剪枝和后剪枝,两者的定义如下:
前剪枝:从上往下进行剪枝,通常利用超参数进行剪枝,例如限制树的最大深度(max_depth)便可以减去该最大深度下面的节点了。
后剪枝:从下往上剪枝,大多是根据业务需求剪枝,比如在违约预测模型中,认为45%和50%违约概率的两个叶子节点都是高危人群,那么就把这两个叶子节点合并成一个节点。
在实际应用中,往往前剪枝应用的更加广泛一些,上面的参数调优其实也起到了一定前剪枝的作用。
补充知识点2:分类决策树模型的超参数
下面是分类决策树模型DecisionTreeClassifier()模型常用的一些超参数及它们的解释:
1.criterion:特征选择标准,取值为"entropy"信息熵和"gini"基尼系数,默认选择"gini"。
2.splitter:取值为"best"和"random",“best"在特征的所有划分点中找出最优的划分点,适合样本量不大的情况,“random"随机地在部分划分点中找局部最优的划分点,适合样本量非常大的情况,默认选择"best”。
3.max_depth:决策树最大深度,取值为int或None,一般数据或特征比较少的时候可以不设置,如果数据或特征比较多时,可以设置最大深度进行限制。默认取‘None’。
4.min_samples_split:子节点往下划分所需的最小样本数,默认取2,如果子节点中的样本数小于该值则停止分裂。
5.min_samples_leaf:叶子节点的最少样本数,默认取1,如果小于该数值,该叶子节点会和兄弟节点一起被剪枝(即剔除该叶子节点和其兄弟节点,并停止分裂)。
6.min_weight_fraction_leaf:叶子节点最小的样本权重和,默认取0,即不考虑权重问题,如果小于该数值,该叶子节点会和兄弟节点一起被剪枝(即剔除该叶子节点和其兄弟节点,并停止分裂)。如果较多样本有缺失值或者样本的分布类别偏差很大,则需考虑样本权重问题。
7.max_features:在划分节点时所考虑的特征值数量的最大值,默认取None,可以传入int型或float型数据。如果是float型数据,表示百分数。
8.max_leaf_nodes:最大叶子节点数,默认取None,可以传入int型数据。
9.class_weight:指定类别权重,默认取None,可以取"balanced”,代表样本量少的类别所对应的样本权重更高,也可以传入字典指定权重。该参数主要是为防止训练集某些类别的样本过多,导致训练的决策树过于偏向这些类别。除了此处指定class_weight,还可以使用过采样和欠采样的方法处理样本类别不平衡的问题,过采样和欠采样将在第十一章:数据预处理讲解。
10.random_state:当数据量较大,或特征变量较多时,可能在某个节点划分时,会碰上两个特征变量的信息熵增益或者基尼系数减少量是一样的情况,那么此时决策树模型默认是随机从中选一个特征变量进行划分,这样可能会导致每次运行程序后生成的决策树不太一致。如果设定random_state参数(如设置为123)可以保证每次运行代码时,各个节点的分裂结果都是一致的,这在特征变量较多,树的深度较深的时候较为重要。
补充知识点3:树模型在金融大数据风控领域中的应用
除了员工离职预测模型外,在金融大数据风控领域,决策树模型也有很大的应用空间,以银行的信贷违约预测模型为例,通常便会用到逻辑回归模型以及决策树模型。
逻辑回归模型的优劣势:变量不需要太多,不容易过拟合,泛化能力较强,可能1年才换一次模型,但是逻辑回归模型有时不够精确,不能有效剔除潜在违约人员。
树模型(决策树、随机森林、XGBoost等模型)的优劣势:不太稳定(一个变量可以反复用),容易造成过拟合,泛化能力较弱,一段时间后换一波人可能就不行了,但它的拟合度强,区分度高,可以快速去掉坏人。
因此实际应用中,以基于逻辑回归的评分卡模型为基础(稳定性强、半年到1年更新一次,但不够精确,KS值不够大) + 决策树等树模型(不太稳定,可能要一个月更新一次,但拟合度强,区分度高,可以在第一波快速去掉坏人)。
总结来说,决策树模型作为机器学习的经典算法模型,有其独特的优势,比如其对异常值不敏感、可解释性强等,不过其也却也存在一些缺点,如结果不稳定、容易造成过拟合等问题。更重要的是决策树模型是之后很多重要集成模型的基础,如之后8、9、10章讲的的集成模型随机森林模型、AdaBoost模型、GBDT模型、XGBoost与LightGBM模型都是建立在决策树模型基础之上,因此决策树模型一定要好好掌握。
点击下载。
前剪枝:利用参数剪枝,从上往下,例如限制树的最大深度,设置阈值等。
前剪枝就是在树的构建过程(只用到训练集),设置一个阈值(样本个数小于预定阈值或GINI指数小于预定阈值),使得当在当前分裂节点中分裂前和分裂后的误差超过这个阈值则分列,否则不进行分裂操作。举例说明,
这是之前我们构造的决策树模型,假设我们设置节点个数<150时停止分裂,那这个节点在之后的生长过程中就会停止继续分裂,而作为一个叶子节点存在。
构建新的决策树模型,并规定节点样本最小值为150,其他代码部分均和之前相同,这里不再展示,
model = DecisionTreeClassifier(min_samples_split=150)
决策树模型可视化后,由于设置参数后生成的决策树结构过于庞大,这里只截取一部分,
我们可以看到,在第一个图中我们圈出一个节点,由于设置了节点最小样本数这个阈值,所以在之后的生长中一直作为叶子节点存在,而没有继续向下生成新的子节点。另外我们还发现在第四层中也有一些样本数小于阈值的节点停止了分裂。
后剪枝:从下往上剪枝,是在决策树模型生成后,对决策树模型进行修改。我们在这里详细讲解一种悲观剪枝法(Pessimistic Error Pruning,PEP)。
PEP剪枝是把一颗子树用一个叶子节点来替代(就是用子树的根来代替)。
PEP算法首先确定这个叶子的经验错误率(empirical)为(E+0.5)/N,0.5为一个调整系数。对于一颗拥有L个叶子的子树,则子树的错误数和实例数都是就应该是叶子的错误数和实例数求和的结果,则子树的错误率为e,
否可以用一个叶子节点替换一棵子树,还要满足一个条件,A是我们要使用的替代节点的错判个数,之后还要加上0.5,最终替换标准,
其中是标准差,因为在构建模型过程中,子树的错误个数是一个随机变量,可以近似看成是二项分布,就可以根据二项分布的标准差公式算出标准差,就可以确定是否应该剪掉这个树枝了。子树中有N的实例,就是进行N次试验,每次实验的错误的概率为e,符合B(N,e)的二项分布,根据公式,均值为Ne,方差为Ne(1-e),标准差为方差开平方。
我们举个例子,
我们想要尝试用上图这个子树的根节点T1来代替整个子树,每个节点有两个数据,从左向右分别是节点样本分类的正确个数和错误个数。这棵子树有三个叶子节点,即L=3,子树有16条数据,N=16,其中4条正确,12条错误,计算子树错误率为
计算标准差,
,子树的错误数
为所有叶子的错误数,
如果用根节点替换子树,则,
因为满足的条件,因此,我们可以将这棵子树替换成根节点。
本节将通过客户违约预测模型的搭建来学习决策树模型在金融领域的应用,并会讲解衡量一个模型预测效果的优劣的一些方法,最后将把决策树模型通过可视化的方式呈现出来.
客户违约预测模型的目的是通过已有的客户信息和违约表现来搭建合适的模型,从而预测之后的客户是否会违约。首先通过6.2.2节的pandas库读取数据相关知识读取客户的证信数据以及其交易表现,即是否违约记录,代码如下:
import pandas as pd
df = pd.read_excel('客户信息及违约表现.xlsx')
运行结果如下表所示,其中共有1000组历史数据,其中前400个为违约客户数据,后600个为非违约客户数据。因为Python数学建模中无法识别文本内容,所以“性别”及“是否违约”栏中的内容已经进行了数值处理,其中“性别”栏中0表示男,1表示女,“是否违约”栏中0表示不违约,1表示违约。我们的目的就是根据这些历史数据搭建决策树模型来预测之后的客户违约可能性。
其中是否违约作为目标变量,剩下的字段作为特征变量,通过一个借款客户的特征来判断他会不会违约。这里为了方便演示,只选取了5个特征变量,在商业实战中,用到的特征变量远比这里的案例多得多。下面便是决策树模型搭建,也是大部分机器学习模型搭建中的常规步骤。
1.提取特征变量和目标变量
通过如下代码将特征变量和目标变量单独提取出来,代码如下:
X = df.drop(columns='是否违约')
y = df['是否违约']
通过6.2.3小节讲的drop()函数删除“是否违约”这一列,将剩下的数据作为特征变量赋值给变量X,这里再补充一个删除列的方法:df.drop(‘是否违约’, axis=1),其中axis=1表示按列进行处理。然后通过DataFrame提取列的方式提取“是否违约”这一列作为目标变量,并赋值给变量y。
df.columns
X = df[['收入', '年龄', '性别', '历史授信额度', '历史违约次数']]
X
2.划分训练集和测试集
X_train = X[0:800]
y_train = y[0:800]
X_test = X[800:]
y_test = y[800:]
提取完特征变量后,我们需要将原来的1000个数据拆分为训练集及测试集。顾名思义,训练集拿来做训练,而测试集拿来检验训练的结果。
通常我们会根据样本量的大小来划分训练集和测试集,当样本量大的时候,可以划分多一点的比例的数据给训练集,比如有10万组数据的时候,我们可以设定9:1的比例来划分训练集和测试集。这里有1000个数据,并不算多,所以按8:2的比例来划分训练集和测试集。
划分训练集和测试集的代码如下:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
第一行代码便是从Scikit-Learn库中引入train_test_split()函数。第二行代码则是通过train_test_split()函数进行训练集合测试集的划分,其中X_train,y_train为训练集数据,X_test,y_test则为测试集中的数据。其中train_test_split()函数的前两个参数X和y便是之前提取的特征变量和目标变量;test_size则是测试集数据所占的比例,这里选择的是20%,也即0.2。数据的划分是随机的,我们可以将划分后的数据打印出来看下,如下图所示:
因为每次运行程序时,train_test_split()函数都是随机划分数据的,如果想每次划分数据产生的内容都是一致的,可以设置random_state参数,代码如下:
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=1)
其中random_state的数字1没有特殊含义,可以换成别的数字,它只是相当于一个种子参数,使得这样每次划分数据的时候,内容都是一致的。
3.模型训练及搭建(核心)
划分为训练集和测试集之后,就可以从Scikit-Learn库中引入决策树模型进行模型训练了,代码如下:
from sklearn.tree import DecisionTreeClassifier
clf = DecisionTreeClassifier(max_depth=3)
clf = clf.fit(X_train, y_train)
其中第一行代码从Scikit-Learn库中引入分类决策树模型:DecisionTreeClassifier。第二行代码将决策树模型赋值给clf变量,同时设置模型参数max_depth为3,即树的最大深度为3,该模型参数的概念在16.3.3小节还会有讲解。第三行代码则是通过fit()方法来进行模型的训练,其中传入的参数就是上一步骤获得的训练集数据。
至此,一个决策树模型其实便已经搭建完成了,这里把决策树模型搭建的代码汇总一下,可以看到其代码本身并不复杂,主要需要理解其背后的原理,这样才能对建立的模型心中有数。
import pandas as pd
df = pd.read_excel('客户信息及违约表现.xlsx')
# 1.提取特征变量和目标变量
X = df.drop(columns='是否违约')
y = df['是否违约']
# 2.划分训练集和测试集
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=123)
# 3.模型训练及搭建
from sklearn.tree import DecisionTreeClassifier
clf = DecisionTreeClassifier(max_depth=3)
clf = clf.fit(X_train, y_train)
模型搭建完成后就可以利用模型来进行预测了,此时之前划分的测试集就可以发挥作用了,我们可以利用测试集来进行预测并评估模型的预测效果。
本小节将介绍如何直接预测是否违约,以及预测不违约&违约的概率,最后将介绍如何合理地对一个模型进行评估。
1.直接预测是否违约
y_pred = clf.predict([[0, 10, 1, 10, 1]])
y_pred
y_pred = clf.predict([[1000000000000, 30, 1, 100000, 0], [1000000000000, 10, 1, 100000, 0]])
y_pred
搭建模型的目的便是希望利用它来预测数据,这里把测试集中的数据导入到模型中来进行预测,代码如下,其中clf就是上一节搭建的决策树模型。
y_pred = clf.predict(X_test)
预测的y_pred如下图所示,0和1为预测的结果,0为预测会不违约,1为预测会违约。
利用6.2.1小节创建DataFrame相关知识点,将预测的y_pred和测试集实际的y_test汇总到一起,其中y_pred是一个numpy.ndarray一维数组结构,y_test为Series一维序列结构,所以这里都用list()函数将其转换为列表,代码如下:
a = pd.DataFrame() # 创建一个空DataFrame
a['预测值'] = list(y_pred)
a['实际值'] = list(y_test)
通过print(a.tail())可以将生成的DataFrame后五行打印输出如下。
可以看到这里对测试数据集中的全部数据进行了预测,且最后五组数据的预测准确度达到了80%,如果要查看整体的预测准确度,可以采用如下代码:
from sklearn.metrics import accuracy_score
score = accuracy_score(y_pred, y_test)
通过将score打印输出,发现整个模型在测试集上的预测准确度为0.825,即200个测试集数据中,有165人预测结果和实际结果相符。
2.预测不违约&违约概率
其实分类决策树模型本质预测的并不是准确的0或1的分类,而是预测其属于某一分类的概率,可以通过如下代码查看预测属于各个分类的概率:
y_pred_proba = clf.predict_proba(X_test)
此时获得的y_pred_proba就是预测的属于各个分类的概率,它是一个二维数组,下表展示的便是最后五组数据的不违约&违约概率。第一列数据是预测为第一类结果0,也即不违约的概率,第二列数据则是预测为第二类结果1,也即违约的概率,这两个概率的和为1。
之前直接预测是否违约时,本质其实是看属于哪种分类的概率最大,比如最后一行数据,其中违约的概率为0.75,大于不违约的概率0.25,所以才预测其违约。二分类问题默认是以0.5作为阈值来预测属于哪一类,因为如果某一类的概率大于0.5,则该类的概率必然大于另一类。实际应用也可以根据需要调节阈值,比如设定只要违约概率大于0.3,就认为该用户会违约。
有的细心的读者可能已经发现,上面一些概率是一样的,比如中间两个客户的不违约概率都是0.56,这个在下一小节将模型可视化呈现后大家就会明白这些概率的计算原理了。
如果想单纯的查看违约概率,即查看y_pred_proba的第二列,可以采用如下代码,这个是二维数组选取列的方法,其中逗号前的“:”表示所有行,逗号后面的数字1则表示第二列,如果把数字1改成数字0,则提取第一列非违约概率。
y_pred_proba[:,1]
3.模型预测效果评估
之前已经利用准确度来衡量了模型的预测效果,不过在商业实战中一般不会以准确度作为模型的评估标准,因为准确度很多时候并不可靠。举个例子,倘若100个客户里有10个人违约,而如果模型预测所有客户都不会违约,虽然这个模型没有过滤掉一个违约客户,但是模型的预测准确度仍然能达到90%,显然这个较高的准确度并不能反映模型的优劣。在商业实战中,我们更关心下面两个指标。
其中TP、FP、TN、FN的含义如下表所示,这个表也叫作混淆矩阵。
真正率计算的便是在所有实际违约的人中,预测为违约的比例(抓住坏人的命中率),也称命中率或召回率;而假正率则是计算在所有实际没有违约的人当中,预测为违约的比例(误伤好人的假警报率),也称假警报率。
以上面提到的例子为例,100客户中有10个人违约,模型预测所有客户都不会违约,如下表所示,那么模型的假正率(FPR)为0,即没有误伤一个好人,但是此时模型的真正率(TPR)也为0,即没有揪出一个坏人,那么此时便明白即使是高达90%的预测准确度也是没有意义的。
一个优秀的客户违约预测模型,我们希望命中率(TPR)尽可能的高,即能尽可能地揪出坏人,同时也希望假警报率(FPR)能尽可能的低,即不要误伤好人。然而这两者往往成正相关性,因为一旦当调高阈值,比如认为违约率超过90%的才认定为违约,那么会导致假警报率很低,但是命中率也很低;而如果降低阈值的话,比如认为违约率超过10%就认定为违约,那么命中率就会很高,但是假警报率也会很高。因此为了衡量一个模型的优劣,数据科学家根据不同阈值下的命中率和假警报率绘制了如下的曲线图,称之为ROC曲线。
ROC曲线的横坐标为假警报率(FPR),其纵坐标为命中率(TPR),在某一个阈值条件下,我们希望命中率能尽可能的高,而假警报率尽可能的低。
举例来说,某一检测样本总量为100,其中违约客户为20人,当阈值为20%的时候,即违约概率超过20%的时候即认为用户会违约,模型A和模型B预测出来的违约客户都是40人。如果模型A预测违约的40人中有20人的确违约,那么命中率达20/20=100%,此时假警报率为20/80=25%,如果模型B预测违约的40人中有15人的确违约,那么其命中率为15/20=75%,假警报率为25/80=31.25%。那么此时认为模型A是一个较优的模型。因此,对于不同模型,我们希望在相同的阈值条件下命中率越高,假警报率越低。
如果把假警报率理解为代价的话,那么命中率就是收益,所以也可以说在相同阈值的情况下,我们希望假警报率(代价)尽量小的情况下,命中率(收益)尽可能的高,该思想反映在图形上也就是这个曲线尽可能的陡峭,曲线越靠近左上角说明在同样的在同样的阈值条件下,命中率越高,假警报率越小,模型越完善。换一个角度来理解,一个完美的模型是在不同的阈值下,假警报率(FPR)则接近于0,而命中率(TPR)都接近于1,该特征反映在图形上,就是曲线非常接近(0,1)这个点,也即曲线非常陡峭。
数值比较上可以使用AUC值来衡量模型的好坏,AUC值(Area Under Curver)指在曲线下面的面积,该面积的取值范围通常为0.5到1,0.5表示随机判断,1则代表完美的模型,在商业实战中,因为存在很多扰动因子,AUC值能达到0.75以上就已经可以接受了,如果能达到0.85以上,则为非常不错的模型了。在Python实现上,通过如下代码就可以求出在不同阈值下的假警报率(FPR)以及命中率(TPR)的值,从而可以绘制ROC曲线。
from sklearn.metrics import roc_curve
fpr, tpr, thres = roc_curve(y_test.values, y_pred_proba[:,1])
其中第一行代码引入roc_curve()函数。第二行代码传入测试集目标变量y_test的值,以及预测的违约概率,并通过roc_curve()函数计算出不同阈值下的假警报率和命中率,并将三者赋值给变量thres、fpr、tpr,此时获得thres、fpr、tpr为三个一维数组。
通过如下代码可以将三者合并成一个二维数据表,代码如下:
a = pd.DataFrame() # 创建一个空DataFrame
a['阈值'] = list(thres)
a['假警报率'] = list(fpr)
a['命中率'] = list(tpr)
此时便可以查看在不同阈值下的假警报率和命中率,如下表所示:
可以看到阈值越高,假警报率越低,但相应的命中率也会下降。
已知了不同阈值下的假警报率和命中率,就可以通过7.2小节数据可视化相关知识点绘制ROC曲线,代码如下:
import matplotlib.pyplot as plt
plt.plot(fpr, tpr)
plt.show()
绘制的ROC曲线如下图所示:
而通过如下代码则可以快速求出模型的AUC值:
from sklearn.metrics import roc_auc_score
score = roc_auc_score(y_test.values, y_pred_proba[:,1])
其中第一行代码引入roc_auc_score()函数。第二行代码传入测试集目标变量y_test的值,以及预测的违约概率。将获得AUC值打印出来为:0.846,可以说预测效果还是不错的。
如果想将决策树模型可视化展示出来,可以使用Python的graphviz插件,下图所示便是上面训练出来的决策树模型。因为模型可视化呈现主要是为了演示和教学,在真正实战中并不怎么会用到,所以对于graphviz的安装与使用,感兴趣的读者可以自行查阅或参考如下网址:https://shimo.im/docs/lUYMJX0TEjoncFZk /。
可以看到从初始节点往下的确就只有3层树状结构了,这个也即是16.3.1小节所设置的模型参数max_depth,即树的最大深度。设置最大深度的原因,一个是为了方便演示,另外一个原因这是因为如果树的深度过深则会导致模型的过度拟合,预测效果下降。所谓过度拟合(简称过拟合),是指模型在训练样本中拟合程度过高,导致在测试数据集中表现不佳。与之相对的则是欠拟合,欠拟合是指模型拟合程度不高,数据距离拟合曲线较远,或指模型没有很好地捕捉到数据特征,不能够很好地拟合数据。通过下图可以更直观地理解过拟合和欠拟合的概念。
这里再介绍下图中每个节点里内容的含义,除了叶子节点外,每个节点都有4个元素:分裂依据、gini(当前基尼系数)、samples(当前样本数量)、value(样本中各类别的数量)。以根节点为例,其分裂依据是根据历史违约次数是否小于0.5作为分裂依据的,它当前的基尼系数为0.482,其含有的样本总数为800,其中value中的左边的数值475表示“是否违约”中的0,也即不违约客户,右边的数值325表示“是否违约”中的1,也即违约客户。
根节点分裂完后产生两个子节点,其中左边的子节点中大部分为非违约客户(共484人,其中381人未违约、103人违约),而右边的节点中大部分为违约客户(共316人,其中94人未违约、222人违约),这也的确符合现实中如果历史违约次数较少则出现违约可能性较小的经验。通过16.2.2小节中的知识点计算经过根节点分裂后的系统的基尼系数为484/8000.335 + 316/8000.418 = 0.3678。这个也是机器通过不停的训练和计算获得的最优解,如果通过别的方式进行根节点分裂后的系统基尼系数一定会比这个大。这里还可以通过如下代码来计算特征变量的特征重要性(feature importance)来验证上述观点:
clf.feature_importances_
其打印输出结果如下,分别对应5个特征变量的特征重要性。
array([0. , 0.35084118, 0. , 0.15794548, 0.49121334])
可以看到这里重要性最高的便是最后一个特征变量:历史违约次数。这里也解释下为什么第一个特征变量:收入和第三个特征变量:性别的特征重要性为0,这个是因为这两个因素在该模型中没有发挥作用。这个也可以从可视化的图形中看出,每一个分叉的节点都没有依据这两个特征变量进行分裂,所以说这两个特征变量并没有发挥作用。如果把max_depth最大深度设置更大一些,让决策树可以往下继续分裂,那么这两个特征变量可能将发挥作用,特征重要性不再是0。有的读者可能还会疑惑收入的特征重要性怎么还没有年龄高,这个是因为本案例中选取的数据是源于优质客户,即收入较高群体,违约客户和非违约客户的收入差距并不大,所以收入的特征重要性不高。
最后的叶子节点有的因为已经分裂结束了,比如左下角和右下角的两个叶子节点的基尼系数都为0了,也就是说这两个叶子节点的纯度已经最高了(即里面所有的元素都是同一类别),已经不需要再分裂了。而有些基尼系数还没有到达0的,因为限制了树的最大深度为3,所以也不会继续向下分裂了。叶子节点因为不需要继续分裂,所以不存在分裂依据这一元素。之前在16.3.2小节提到的不违约&违约概率的计算就是基于叶子节点,如果分到了最右下角的叶子节点,那么其不违约概率为0,违约概率为100%,如果分到该叶子节点左边的叶子节点,那么其不违约概率为381/443=0.86,违约概率则为62/443=0.14,其余则依次类推。那么其不违约概率为82/147=56,违约概率则为65/147=0.44,其再往左的叶子节点的违约概率则为21/28=0.75,其余则依次类推。此外,感兴趣的读者可以将其余叶子节点所反映出来的违约概率计算一下,再观察上一小节绘制ROC曲线时用到的阈值,会发现上一小节使用的阈值并不是随意选取的,而就是这些不同叶子节点反映出来的违约概率。ROC曲线的绘制就是以这些违约概率的值作为阈值来看不同阈值下的命中率(TPR)和假警报率(FPR )。
通过这个图,我们便能够较好的理解决策树的运行逻辑,当来了一个新的数据的时候,就会从最上面的根节点开始进行判断,如果满足历史违约次数<=0.5则划分到左边的节点进行之后一系列的判断,如果不满足则分到右边的节点进行之后一系列的判断,最终这个新的数据会被划分到其中的一个叶子节点中去,从而完成对数据的预测。
决策树模型作为机器学习的经典算法模型,有其独特的优势,比如其对异常值不敏感、可解释性强等,不过其也却也存在一些缺点,如结果不稳定、容易造成过拟合等问题。所以在商业实战中常使用一个基于决策树模型的集成算法模型:随机森林模型,随机森林是通过多个决策树模型共同搭建,其结果更稳定且不易过拟合。由于本书篇幅原因,随机森林模型就不在本书详细讲解,具体内容可以参考笔者的下一本书《Python大数据分析与机器学习商业案例实战》。