决策树是一种直观且功能强大的机器学习算法,广泛应用于分类和回归任务。本文将深入探讨构建决策树所需的数学基础,详细讲解决策树的工作原理,并通过 Python 实现一个完整的决策树分类器,帮助你从理论到实践全面掌握这一算法。
决策树算法的核心:
用层层递进的问题把数据不断细分,直到每个子集足够‘纯净’
绘制决策树我们需要考虑的问题:
- 哪个节点作为根节点?哪些节点作为中间节点,那些节点作为叶子节点。
- 节点如何分裂
- 节点分裂标准的依据
决策树的数学基础
决策树分类标准:
- ID3算法(最不重要,几乎不用)
- C4.5算法(依赖于ID3算法)
- CART算法
要理解决策树算法,我们需要掌握几个关键的数学概念,这些概念构成了决策树构建的理论基础。
1.ID3:
ID3 算法的核心数学基础是信息熵和信息增益。为了让这些概念更易于理解,我们将通过一个具体的例子来详细解释它们的计算过程。
示例数据集
我们将使用一个简单的 "是否适合打网球" 的数据集,包含 4 个特征和 14 个样本:
编号 | 天气 | 温度 | 湿度 | 风速 | 是否打球 |
---|---|---|---|---|---|
1 | 晴朗 | 热 | 高 | 弱 | 否 |
2 | 晴朗 | 热 | 高 | 强 | 否 |
3 | 多云 | 热 | 高 | 弱 | 是 |
4 | 下雨 | 适中 | 高 | 弱 | 是 |
5 | 下雨 | 凉爽 | 正常 | 弱 | 是 |
6 | 下雨 | 凉爽 | 正常 | 强 | 否 |
7 | 多云 | 凉爽 | 正常 | 强 | 是 |
8 | 晴朗 | 适中 | 高 | 弱 | 否 |
9 | 晴朗 | 凉爽 | 正常 | 弱 | 是 |
10 | 下雨 | 适中 | 正常 | 弱 | 是 |
11 | 晴朗 | 适中 | 正常 | 强 | 是 |
12 | 多云 | 适中 | 高 | 强 | 是 |
13 | 多云 | 热 | 正常 | 弱 | 是 |
14 | 下雨 | 适中 | 高 | 强 | 否 |
在这个数据集中,我们的目标是根据天气、温度、湿度和风速这四个特征来预测是否是否适合打网球。
2.信息熵 (Entropy) 的计算
信息熵用于衡量数据集的不确定性或混乱程度。对于整个数据集,我们先计算其信息熵。
在 14 个样本中:
- 9 个样本为 "是"(适合打球)
- 5 个样本为 "否"(不适合打球)
信息熵公式:
pi:表示该数据的概率
计算过程:
- p(是) = 9/14 ≈ 0.643
- p(否) = 5/14 ≈ 0.357
H(S) = -[p(是) *log2(p(是)) + p(否) *log2(p(否))
把数据带入得出为 0.939
整个数据集的信息熵为 0.939,表明存在一定的不确定性。
信息增益 (Information Gain) 的计算
信息增益用于衡量使用某个特征划分数据集后,不确定性减少的程度。我们以 "天气" 特征为例计算其信息增益。
"天气" 特征有三个可能的取值:晴朗、多云、下雨
步骤 1:计算每个取值子集的熵
-
天气 = 晴朗(共 5 个样本):
- 2 个 "是",3 个 "否"
- p(是)=2/5=0.4,p(否)=3/5=0.6
- H(晴朗) = -[0.4 *log2(0.4) + 0.6 *log2(0.6)]
- H(晴朗) = -[0.4 * (-1.322) + 0.6 *(-0.737)]
- H(晴朗) = -[(-0.529) + (-0.442)] = 0.971
-
天气 = 多云(共 4 个样本):
- 4 个 "是",0 个 "否"
- p(是)=1.0,p(否)=0.0
- H(多云) = -[1.0 *log2(1.0) + 0.0 *log_2(0.0)] = 0\
-
天气 = 下雨(共 5 个样本):
- 3 个 "是",2 个 "否"
- p(是)=3/5=0.6,p(否)=2/5=0.4
- H(下雨) = -[0.6 \log2(0.6) + 0.4 \log2(0.4)] = 0.971\)
步骤 2:计算条件熵
条件熵是所有子集熵的加权平均: H(S|天气) = 5/14H(晴朗) + 4/14H(多云) + 5/14H(下雨)
(H(S|天气) = 0.347 + 0 + 0.347 = 0.694
步骤 3:计算信息增益
信息增益 = 原始熵 - 条件熵: IG(S, 天气) = H(S) - H(S|天气)
IG(S, 天气) = 0.939 - 0.694 = 0.245
3. 比较不同特征的信息增益
为了选择最佳分裂特征,我们需要计算所有特征的信息增益。
温度特征的信息增益
温度有三个取值:热、适中、凉爽
- 热:4 个样本(2 是,2 否),H=1.0
- 适中:6 个样本(4 是,2 否),H=0.918
- 凉爽:4 个样本(3 是,1 否),H=0.811
条件熵: (H(S|温度) = 4\14* 1.0 + 6\14*0.918 + 4\14* 0.811 = 0.892
信息增益: IG(S, 温度) = 0.939 - 0.892 = 0.047
湿度特征的信息增益
湿度有两个取值:高、正常
- 高:7 个样本(3 是,4 否),H=0.985
- 正常:7 个样本(6 是,1 否),H=0.592
条件熵: H(S|湿度) = 7\14* 0.985 + 7\14* 0.592 = 0.788
信息增益:IG(S, 湿度) = 0.939 - 0.788 = 0.151
风速特征的信息增益
风速有两个取值:弱、强
- 弱:8 个样本(6 是,2 否),H=0.811
- 强:6 个样本(3 是,3 否),H=1.0
条件熵:
H(S|风速) = 8\14*0.811 6\14*1.0 = 0.892
信息增益: IG(S, 风速) = 0.939 - 0.892 = 0.047
4. 结果比较与特征选择
各特征的信息增益:
- 天气:0.245
- 湿度:0.151
- 温度:0.047
- 风速:0.047
根据 ID3 算法,我们选择信息增益最大的特征作为根节点的分裂特征。在这个例子中,"天气" 特征的信息增益最大(0.245),因此被选为第一个分裂特征。
这意味着使用 "天气" 特征划分数据能最大程度地降低不确定性,是当前最有效的分类依据。
5. 递归计算过程
在选择 "天气" 作为根节点后,我们需要对每个子节点递归地进行相同的计算:
-
天气 = 晴朗的子集中:
- 我们需要计算剩余特征(温度、湿度、风速)的信息增益
- 选择信息增益最大的特征作为该节点的分裂特征
-
天气 = 多云的子集中:
- 所有样本都属于 "是" 类别,熵为 0
- 这是一个叶节点,不需要进一步分裂
-
天气 = 下雨的子集中:
- 同样计算剩余特征的信息增益
- 选择最佳特征继续分裂
通过这种递归计算,我们可以构建出完整的决策树。
二、C4.5算法
C4.5 算法是 Ross Quinlan 在 ID3 算法基础上提出的改进版本,解决了 ID3 算法的诸多局限性。
C4.5 算法的核心改进 :
- 使用信息增益率替代信息增益:解决了 ID3 偏向于选择取值较多的特征的问题
- 支持连续值处理:能直接处理连续型特征,无需手动离散化
- 规则提取:可以将决策树转换为易于理解的规则集
C4.5计算公式为:
IV(A):特征A的固有值
计算方式如下(在ID3的基础上实例):
前面计算出了各特征信息增益:
- 数据集总熵 H(S) = 0.939
- 天气的信息增益 IG(S, 天气) = 0.245
- 湿度的信息增益 IG(S, 湿度) = 0.151
- 温度的信息增益 IG(S, 温度) = 0.047
- 风速的信息增益 IG(S, 风速) = 0.047
计算各特征的固有值(IV):
天气特征(3 个取值:晴朗、多云、下雨):
- 晴朗:5/14,多云:4/14,下雨:5/14
- IV(天气) = -[5/14 *log2(5/14) + 4/14 *log2(4/14) + 5/14 *log2(5/14)]
- IV(天气) = -[5/14 *(-1.485) + 4/14 *(-1.807) + 5/14 * (-1.485)]\)
- IV(天气) = -[(-0.530) + (-0.516) + (-0.530)] = 1.576
天气:GR=0.245/1.576≈0.155
湿度特征(2 个取值:高、正常):
- 高:7/14,正常:7/14
- IV(湿度) = -[7/14 *log2(7/14) + 7/14 *log2(7/14)]
- IV(湿度) = -[0.5 *(-1) + 0.5 *(-1)] = 1.0
湿度:\(GR = 0.151 / 1.0 ≈ 0.151
温度特征(3 个取值:热、适中、凉爽):
- 热:4/14,适中:6/14,凉爽:4/14
- IV(温度) = -[4/14 *log2(4/14) + 6/14 *log_2(6/14) + 4/14 *log_2(4/14)]
- IV(温度) = 1.556
温度:\(GR = 0.047 / 1.556 ≈ 0.030\)
风速特征(2 个取值:弱、强):
- 弱:8/14,强:6/14
- IV(风速) = -[8/14 *log2(8/14) + 6/14 *log2(6/14)]
- IV(风速) = 0.985
风速:GR = 0.047 / 0.985 ≈ 0.048
信息增益率排序:温度(1.556)>天气(1.55)>适度(0.151)>风速(0.048)
三、CART决策树
CART(Classification and Regression Trees,分类与回归树)是一种功能强大的决策树算法,由 Breiman 等人在 1984 年提出。与 ID3 和 C4.5 不同,CART 不仅可以处理分类问题,还能有效解决回归任务,是应用最广泛的决策树算法之一。
1. 基尼不纯度(Gini Impurity)
CART 在分类问题中使用基尼不纯度作为分裂标准,衡量数据集的纯度:
S:数据集
c:类别总数
pi:第i类样本在S中所占的比例
2. 基尼增益(Gini Gain)
CART 使用基尼增益来选择最佳分裂特征:
GG(S, A): 是用特征A划分数据集S获得的基尼增益
G(S): 是原始数据集的基尼不纯度
Sv: 是S中特征A取值为v的样本子集
用具体例子计算基尼不纯度
步骤 1:计算整个数据集的基尼不纯度
在 14 个样本中:
9 个 "是",5 个 "否"
p(是) = 9/14 ≈ 0.643,p(否) = 5/14 ≈ 0.357
G(S) = 1 - (p(是)^2 + p(否)^2)
(G(S) = 1 - (0.643^2 + 0.357^2)
(G(S) = 1 - (0.413 + 0.127) = 1 - 0.540 = 0.460\)
步骤 2:计算 "天气" 特征的基尼增益
-
天气 = 晴朗(5 个样本:2 是,3 否):
- G(晴朗) = 1 - (0.4^2 + 0.6^2) = 1 - (0.16 + 0.36) = 0.48
-
天气 = 多云(4 个样本:4 是,0 否):
- G(多云) = 1 - (1.0^2 + 0.0^2) = 0
-
天气 = 下雨(5 个样本:3 是,2 否):
- G(下雨) = 1 - (0.6^2 + 0.4^2) = 1 - (0.36 + 0.16) = 0.48
-
计算基尼增益:
GG(S, 天气) = G(S) - [5/14* G(晴朗) + 4/14 *G(多云) + 5/14 * G(下雨)]
GG(S, 天气) = 0.460 - [5/14 *0.48 + 4/14 *0 + 5/14*0.48]
GG(S, 天气) = 0.460 - 0.343 = 0.117
步骤 3:比较不同特征的基尼增益
通过类似计算,我们可以得到所有特征的基尼增益:
- 天气:0.117
- 湿度:0.096
- 温度:0.016
- 风速:0.018
CART 会选择基尼增益最大的 "天气" 特征作为根节点的分裂特征。
决策树剪枝:
决策树算法在构建过程中容易出现过拟合现象像 —— 模型在训练数据上表现优异,但在新数据上泛化能力差。剪枝(Pruning)是解决这一问题的核心技术,通过移除决策树中不必要的分支,简化模型结构,提高泛化能力。为了防止过拟合
剪枝的数学定义
对于一个决策树T,其成本复杂度定义为:
其中:
- C(T) 是树T在训练数据上的误差(分类为错分率,回归为平方误差)
- |T| 是树T的叶节点数量(衡量树的复杂度)
- alpha 是正则化参数,控制复杂度惩罚的强度
构建决策树:
项目背景与目标
电信行业客户流失率高一直是困扰运营商的难题。据统计,获取新客户的成本是保留老客户的 5-10 倍。构建准确的客户流失预测模型,能够帮助企业:
- 提前识别高流失风险客户
- 制定针对性的挽留策略
- 降低客户获取成本,提高企业收益
本项目将使用决策树算法构建客户流失预测模型,决策树的优势在于模型解释性强,能够清晰展示影响客户流失的关键因素,便于业务部门理解和应用。
完整代码实现与解析
1. 环境准备与库导入
首先,我们需要导入项目所需的 Python 库:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.tree import DecisionTreeClassifier, plot_tree, export_text
from sklearn.model_selection import train_test_split, cross_val_score
from imblearn.over_sampling import SMOTE
from sklearn import metrics
import seaborn as sns
# 设置中文显示
plt.rcParams["font.family"] = ["SimHei", "WenQuanYi Micro Hei", "Heiti TC"]
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
这些库涵盖了数据处理(pandas)、数值计算(numpy)、可视化(matplotlib、seaborn)、机器学习模型(sklearn)以及不平衡数据处理(imblearn)等功能。
2. 数据加载与初步探索
数据是建模的基础,首先我们需要加载数据并进行初步探索:
# 读取数据
data = pd.read_excel("电信客户流失数据.xlsx")
# 查看数据基本信息
print(f"数据集形状: {data.shape}")
print("\n数据集前5行:")
print(data.head())
# 查看流失状态分布
churn_distribution = data['流失状态'].value_counts()
print("\n流失状态分布:")
print(churn_distribution)
print(f"流失率: {churn_distribution[1]/len(data):.2%}")
# 绘制流失状态分布图
plt.figure(figsize=(8, 6))
sns.countplot(x='流失状态', data=data)
plt.title('客户流失状态分布')
plt.xlabel('流失状态 (0:未流失, 1:已流失)')
plt.ylabel('客户数量')
plt.show()
这一步的主要目的是:
- 确认数据加载正确
- 了解数据的基本结构和特征
- 分析目标变量(流失状态)的分布情况,判断是否存在类别不平衡问题
在电信客户流失数据中,通常流失客户占比会低于未流失客户,形成不平衡数据集,这一点我们后续需要专门处理。
3. 数据预处理与划分
数据预处理是建模过程中至关重要的一步,直接影响模型性能:
# 划分特征与目标变量
X = data.drop('流失状态', axis=1)
y = data['流失状态']
# 区分类别特征和数值特征
categorical_features = X.select_dtypes(include=['object', 'category']).columns
numerical_features = X.select_dtypes(include=['int64', 'float64']).columns
print(f"类别特征: {list(categorical_features)}")
print(f"数值特征: {list(numerical_features)}")
# 对类别特征进行独热编码
if not categorical_features.empty:
X = pd.get_dummies(X, columns=categorical_features, drop_first=True)
print(f"独热编码后特征数量: {X.shape[1]}")
# 划分训练集和测试集
x_train, x_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42, stratify=y
)
print(f"训练集大小: {x_train.shape[0]}, 测试集大小: {x_test.shape[0]}")
# 处理类别不平衡
print("\n处理类别不平衡...")
sm = SMOTE(random_state=42)
x_train_res, y_train_res = sm.fit_resample(x_train, y_train)
print(f"重采样后训练集类别分布: {pd.Series(y_train_res).value_counts()}")
数据预处理主要包括:
- 特征与目标变量分离:将 "流失状态" 作为目标变量,其余作为特征变量
- 特征类型区分:区分类别特征和数值特征,因为决策树虽然可以处理两种类型特征,但类别特征通常需要编码
- 独热编码:将类别特征转换为数值形式,使模型能够处理
- 数据集划分:将数据分为训练集(70%)和测试集(30%),使用
stratify=y
确保分层抽样,保持与原数据相同的类别比例 - 类别不平衡处理:使用 SMOTE 算法对训练集进行过采样,解决流失客户样本少的问题
4. 决策树参数调优
决策树模型的性能很大程度上依赖于超参数的选择,我们使用网格搜索进行参数优化:
# 定义参数搜索范围
max_depth_choose = [3, 4, 5, 6, 7, 8, 9, 10]
min_samples_split_choose = [2, 3, 4, 5, 6, 7, 8, 9, 10]
min_samples_leaf_choose = [1, 2, 3, 4, 5]
# 存储参数和对应的分数
scores = []
params_list = []
# 网格搜索
print("开始参数网格搜索(可能需要几分钟)...")
for depth in max_depth_choose:
for split in min_samples_split_choose:
for leaf in min_samples_leaf_choose:
# 创建决策树模型
dtr = DecisionTreeClassifier(
max_depth=depth,
min_samples_split=split,
min_samples_leaf=leaf,
random_state=42
)
# 5折交叉验证,使用recall作为评分指标
cv_scores = cross_val_score(
dtr, x_train_res, y_train_res,
cv=5, scoring='recall'
)
# 存储结果
scores.append(np.mean(cv_scores))
params_list.append((depth, split, leaf))
# 找到最佳参数
best_idx = np.argmax(scores)
best_params = params_list[best_idx]
best_score = scores[best_idx]
print(f"最佳参数: 最大深度={best_params[0]}, 最小分裂样本数={best_params[1]}, 最小叶节点样本数={best_params[2]}")
print(f"最佳交叉验证召回率: {best_score:.4f}")
这里我们选择了三个关键参数进行优化:
max_depth
:树的最大深度,控制树的复杂度,防止过拟合min_samples_split
:节点分裂所需的最小样本数min_samples_leaf
:叶节点所需的最小样本数
在客户流失预测中,我们更关注召回率(recall),即尽可能多地识别出真实的流失客户,因此使用召回率作为评分指标。通过 5 折交叉验证评估不同参数组合的性能,选择表现最佳的参数。
5. 训练最佳模型
使用找到的最佳参数训练最终模型:
dtr_best = DecisionTreeClassifier(
max_depth=best_params[0],
min_samples_split=best_params[1],
min_samples_leaf=best_params[2],
random_state=42
)
# 拟合模型
dtr_best.fit(x_train_res, y_train_res)
6. 模型评估
模型训练完成后,需要全面评估其性能:
# 在训练集上的预测
y_train_pred = dtr_best.predict(x_train_res)
# 在测试集上的预测
y_test_pred = dtr_best.predict(x_test)
# 训练集评估报告
print("\n训练集分类报告:")
print(metrics.classification_report(y_train_res, y_train_pred))
# 测试集评估报告
print("\n测试集分类报告:")
print(metrics.classification_report(y_test, y_test_pred))
# 计算并绘制混淆矩阵
def plot_confusion_matrix(y_true, y_pred, title):
cm = metrics.confusion_matrix(y_true, y_pred)
plt.figure(figsize=(8, 6))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
xticklabels=['未流失', '已流失'],
yticklabels=['未流失', '已流失'])
plt.title(title)
plt.xlabel('预测结果')
plt.ylabel('实际结果')
plt.show()
plot_confusion_matrix(y_train_res, y_train_pred, '训练集混淆矩阵')
plot_confusion_matrix(y_test, y_test_pred, '测试集混淆矩阵')
模型评估主要关注以下指标:
- 精确率(Precision):预测为流失的客户中,实际流失的比例
- 召回率(Recall):实际流失的客户中,被正确预测的比例
- F1 分数:精确率和召回率的调和平均
- 准确率(Accuracy):总体预测正确的比例
通过对比训练集和测试集的表现,可以判断模型是否过拟合。混淆矩阵则直观展示了四类预测结果:真正例(TP)、假正例(FP)、真负例(TN)、假负例(FN)。
7. 特征重要性分析
决策树的一大优势是可以解释特征的重要性:
# 获取特征重要性
feature_importance = pd.DataFrame({
'特征': X.columns,
'重要性': dtr_best.feature_importances_
})
# 按重要性排序
feature_importance = feature_importance.sort_values('重要性', ascending=False)
print("前10个最重要的特征:")
print(feature_importance.head(10))
# 绘制特征重要性条形图
plt.figure(figsize=(12, 8))
sns.barplot(x='重要性', y='特征', data=feature_importance.head(10))
plt.title('特征重要性 Top 10')
plt.tight_layout()
plt.show()
特征重要性分析能够揭示影响客户流失的关键因素,例如:
- 月消费金额可能是重要因素,高消费客户可能更容易流失
- 合约期限长短可能影响流失率,短期合约客户更易流失
- 客户服务投诉次数可能与流失正相关
这些 insights 可以直接指导业务部门制定针对性的客户挽留策略。
8. 决策树可视化
为了更直观地理解模型决策过程,我们可以可视化决策树:
# 文本形式展示决策树(前5层)
tree_rules = export_text(dtr_best, feature_names=list(X.columns), max_depth=5)
print("决策树规则(前5层):")
print(tree_rules)
# 图形形式展示决策树
plt.figure(figsize=(30, 20))
plot_tree(
dtr_best,
feature_names=list(X.columns),
class_names=['未流失', '已流失'],
filled=True,
rounded=True,
fontsize=10
)
plt.title('电信客户流失预测决策树')
plt.tight_layout()
plt.savefig('telecom_churn_tree.png', dpi=300, bbox_inches='tight')
print("决策树已保存为 'telecom_churn_tree.png' 文件")
plt.show()
决策树可视化有两种形式:
- 文本形式:展示决策规则,便于程序化理解
- 图形形式:直观展示决策路径和节点分裂条件
通过决策树,我们可以清晰看到模型如何根据客户特征一步步判断其流失风险。
9. 参数影响分析
最后,我们分析关键参数对模型性能的影响:
# 分析最大深度对模型性能的影响
depth_scores = {}
for depth in max_depth_choose:
dtr = DecisionTreeClassifier(
max_depth=depth,
min_samples_split=best_params[1],
min_samples_leaf=best_params[2],
random_state=42
)
scores = cross_val_score(dtr, x_train_res, y_train_res, cv=5, scoring='recall')
depth_scores[depth] = np.mean(scores)
plt.figure(figsize=(10, 6))
plt.plot(list(depth_scores.keys()), list(depth_scores.values()), marker='o')
plt.title('最大深度对模型召回率的影响')
plt.xlabel('最大深度')
plt.ylabel('交叉验证召回率')
plt.grid(True, linestyle='--', alpha=0.7)
plt.show()
这个分析帮助我们理解:
- 随着树深度增加,模型性能如何变化
- 是否存在一个最佳平衡点,过深的树是否会导致过拟合