案例实战 | 决策树预测客户违约

2022-05-12 19:42:12 浏览数 (1)

前言

本文包含的代码知识点如下导图

数据读入

代码语言:javascript复制
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt

df = pd.read_csv('telecom_churn.csv')
df.info(); df.sample(5)

数据说明

  • obey:违约情况,1-违约;0-履约
  • gender:性别,1-男,0-女
  • AGE:年龄;
  • edu_class: 教育阶层,0-3 分别表示初中,高中,大专,大学
  • nrProm:n recent promotion / month 最近一个月的电话营销次数
  • prom:最近一周是否有打电话
  • telephone_service: 客户是否有过电话投诉

数据探索

对各分类变量进行数量上的统计

代码语言:javascript复制
# 名义变量
nominal_var = ['obey', 'gender', 'edu_class', 
               'posTrend', 'nrProm', 'prom', 'telephone_service']

for i in nominal_var:
    print(i, ':')
    print(df[i].agg(['value_counts']).T) # 转置输出更加简洁美观
    # 只有用 agg() 搭配 [] 才能使后面的 .T 发挥转置的效果
    print('='*55) # 发现数据分布还比较均匀,毕竟都是清洗过的

将连续变量转化为分类变量

连续变量的存在是使决策树模型不稳定的主要原因之一,这里使用等宽分箱

而且将连续变量转化为类别变量后,可以与其他类别变量一起,都直接使用卡方检验或方差分析,写成函数快捷操作也更方便。

等宽分箱:每个分箱中的样本量一致 等深分箱:每个分箱中的取值范围一致

代码语言:javascript复制
# 数据集中的两个连续变量
bins_label = [1, 2, 3, 4, 5]

df['AGE'] = pd.qcut(x=df['AGE'], q=5, labels=bins_label)
df['duration'] = pd.qcut(x=df['duration'], q=5, labels=bins_label)

方差分析与卡方检验

  • 多分类使用方差分析如 obey 与 AGE, edu_class, nrProm
  • 二分类用卡方检验或方差分析如 obey 与 gender,posTrend,prom,telephone_service
代码语言:javascript复制
## 利用回归模型中的方差分析
## 只有 statsmodels 有方差分析库
## 从线性回归结果中提取方差分析结果
import statsmodels.api as sm
from statsmodels.formula.api import ols # ols 为建立线性回归模型的统计学库
from statsmodels.stats.anova import anova_lm

插播一条样本量和置信水平 α_level 的注意点(置信水平 α 的选择经验)

样本量

α-level

≤ 100

10%

100 < n ≤ 500

5%

500 < n ≤ 1000

1%

n > 2000

千分之一

样本量过大,α-level 就没什么意义了。数据量很大时,p 值就没用了,样本量通常不超过 5000,所以为了证明两变量间的关系是稳定的,样本量要控制好。

代码语言:javascript复制
# 数据集样本数量:3463,这里随机选择 600 条
df = df.copy().sample(600)

# C 表示告诉 Python 这是分类变量,否则 Python 会当成连续变量使用
## 这里直接使用方差分析对所有分类变量进行检验
## 下面几行代码便是使用统计学库进行方差分析的标准姿势
lm = ols('obey ~ C(AGE)   C(edu_class)   C(gender)   C(nrProm)   
         C(posTrend)   C(prom)   C(telephone_service)', data=df).fit()
# sm.stats.anova_lm(lm, type=2) # type=2 return FataFrame
anova_lm(lm)

# Residual 行表示模型不能解释的组内的,其他的是能解释的组间的
# df: 自由度(n-1)- 分类变量中的类别个数减1
# sum_sq: 总平方和(SSM),residual行的 sum_eq: SSE
# mean_sq: msm, residual行的 mean_sq: mse
# F:F 统计量,查看卡方分布表即可
# PR(>F): P 值

上述代码框可以反复运行几次,避免随机抽样不平均或有例外,可以发现除了最不显著的 prom 和也不怎么显著的 nrProm 和 edu_class 外,其他变量都十分显著,刷新多次就会发现,来来去去都是这三个变量不怎么显著。

筛选变量

去除 prom ,edu_class 与 nrProm 这三个变量,剩下的用于后续建模

代码语言:javascript复制
data = df.drop(columns=['edu_class', 'prom', 'nrProm'])
data.sample(3)

决策树建模

定义树

代码语言:javascript复制
import sklearn.tree as tree

# 这里示范的是拍脑袋进行的建模,函数内部的参数指标都是相对比较随便选出的,
 ## 并没有结合业务背景和一些较深的数学统计学知识

# 定义一棵决策树,还没有用到数据
"""
  criterion:评价指标 -- entropy: 熵,gini: 基尼系数,两者并无优劣之分
  max_depth:决策树的最大层数,本例数据集还比较小
  min_samples_split:通常跟 min_samples_leaf 结合,两者选一个
  min_samples_leaf:每个叶子的最少样本量,表示如果少于这个数值就不继续往下划分了
  random_state:随机种子
最重要的参数除了 criterion 外,min_split 与 min_leaf 两者选一个即可
"""
clf = tree.DecisionTreeClassifier(criterion='entropy', max_depth=8,
                                  min_samples_split=5) 

拆分测试集与训练集

代码语言:javascript复制
from sklearn.model_selection import train_test_split

X = df.drop(columns=['subscriberID', 'obey']); y = df['obey']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, 
                                                    random_state=42)

训练定义好的树并进行预测

代码语言:javascript复制
clf.fit(X=X_train, y=y_train)  # 训练树

```python
# 使用训练好的树进行预测
## 两种预测方式
# 对训练集进行操作
train_est = clf.predict(X_train) # 方式1:用模型预测训练集的结果
train_est_p = clf.predict_proba(X_train)[:, 1] # 方式2:用模型预测训练集的概率

# 对测试集进行相同操作
test_est = clf.predict(X_test) 
test_est_p = clf.predict_proba(X_test)[:, 1] 

pd.DataFrame({'test_target': y_test, 'test_est': test_est, 
             'test_est_p': test_est_p}).head(7) 

模型评估

在测试集上的表现

代码语言:javascript复制
import sklearn.metrics as metrics

# 混淆矩阵,使用机器学习库建模的小缺点便是显示结果数据时不如统计学库方便查看。
print('混淆矩阵...')
print(metrics.confusion_matrix(y_test, test_est,labels=[0,1]))  
print('='*55   'n')

print('计算评估指标...')
print(metrics.classification_report(y_test, test_est))  # 计算评估指标
print('='*55   'n')

# 变量重要性指标
print('变量指标重要性...')
# 决策树是如何计算变量指标重要性的?
 ## 看受该样本影响的样本占整个样本量的多少
print(pd.DataFrame(list(zip(data.columns, clf.feature_importances_))))  
代码语言:javascript复制
fpr_test, tpr_test, th_test = metrics.roc_curve(y_test, test_est_p)
fpr_train, tpr_train, th_train = metrics.roc_curve(y_train, train_est_p)
plt.figure(figsize=[6,6])
plt.plot(fpr_test, tpr_test, color='blue')
plt.plot(fpr_train, tpr_train, color='red')
plt.title('ROC curve')
plt.show()

这里表现出了过渡拟合的情况,毕竟决策树往死里建的话可以精确到每一位用户,那模型的泛化性便大打折扣。比较好的模型是 trainning 和 test 的曲线都往左上角突出,而且两条线几乎没有间隔。这也解释了为什么我们需要对测试集和训练集都进行预测,其实是为了这一步的画图工作。上图中,红train 与 蓝test 之间有不少间隔,说明模型在训练集上的表现比训练集要好,训练集表现更好 -- 过度拟合(因为模型记住了训练集中的一些噪声点,说明可能需要回到开头处理一下异常值或者在拆分测试训练集的时候下点其他功夫如交叉验证等),又或者是建模参数的选择方面出了问题,总之需要根据实际情况探索是什么原因导致了在测试集上的表现相对减弱,即模型的泛化能力降低了)其中的一个解决办法是我们可以考虑降低模型的复杂度,拔高一下模型在测试集的表现,模型在训练集的表现稍微降一点点也没事。

至于 ROC 曲线与 Python 逻辑回归或决策树中的模型评价指标的理解,可参考文章:趣析逻辑回归模型评价指标

代码语言:javascript复制
# 上图可知,还是出现了比较严重的过拟合现象,这里分别展示模型在训练集和测试集上的表现情况
print('测试集...')
print('-'*55)
print(metrics.classification_report(y_test, test_est))  

print('n')

print('训练集...')
print('-'*55)
print(metrics.classification_report(y_train, train_est))

再次看出模型在测试集与训练集的表现上的差距还是比较大的。

模型优化

梯度优化

代码语言:javascript复制
from sklearn.model_selection import GridSearchCV

# 至于如何选择决策树的建模指标 criterion,什么时候用熵 entropy 什么时候用基尼系数 gini
 ## 在 sklearn 代码中添加一个选择即可,并没有什么特别深奥的道理,本来就是两种算法,
 ## 各有优劣,最简单粗暴的方法就是都尝试一下即可。
# 下面的各个参数的潜在数值也只是一种尝试,具体还得结合实际业务场景
param_grid = {
    'criterion': ['entropy', 'gini'],
    'max_depth':[2,3,4,5,6,7,8],
    'min_samples_split':[4,8,12,16,20,24,28] 
}
# 排列组合:2*7*7 种,还没算上交叉验证

# 在定义树的时候先不着急添加所有参数,只添加最基本的 criterion
clf = tree.DecisionTreeClassifier(criterion='entropy') 
clfcv = GridSearchCV(estimator=clf, param_grid=param_grid, 
                   scoring='roc_auc', cv=4) # scoring:评分依据,cv:交叉验证 cross validation
clfcv.fit(X_train, y_train) # 需要一定时间

重复模型评估的步骤与查看模型预测结果

果然,牺牲了小部分在训练集上的精准度,换来了模型更高的泛化性,过拟合的问题得到了较好的解决。

查看“最优”模型的各种参数,并重新建模

代码语言:javascript复制
# 下面这行代码将显示出决策树建模中各种参数的最优组合
clfcv.best_params_
代码语言:javascript复制
# 用 best_params 得出的最佳组合进行建模,需要手动输入参数。
clf = tree.DecisionTreeClassifier(criterion='gini', max_depth=3,
                                  min_samples_split=4) 
clf.fit(X_train, y_train)  #  使用训练数据建模,建模结果便是调优前后对比图中的下半部分,这里不再显示

可视化决策树

代码语言:javascript复制
class_name = ["0(obey)", '1(break)'] # 行为标签名(都是左0右1)

fig, axes = plt.subplots(nrows=1, ncols=1, figsize=(4, 4), dpi=300)
tree.plot_tree(decision_tree=clf,
              feature_names=X_train.columns,
              class_names=class_name,
              filled=True)  # filled=True: 为决策树添加色彩

0 人点赞