文章目录
一、决策树介绍
决策树是一种树形结构的监督学习算法,其核心思想是通过一系列条件判断对数据进行分类,就像人类在做决策时的逻辑判断过程。
1.1 决策树的结构特征
- 内部节点:表示一个特征上的判断(如"年龄是否大于30")
- 分支:表示判断结果的输出(如"是"或"否")
- 叶子节点:表示最终的分类结果(如"同意贷款"或"拒绝贷款")
1.2 决策树的构建三步骤
- 特征选择:从众多特征中挑选分类能力最强的特征
- 树的生成:基于选择的特征递归构建决策树
- 剪枝优化:防止过拟合,提升模型泛化能力
1.3 决策树构建例子
你有一个朋友正在考虑是否要去参加一个社交活动,他/她/他们可能会这样思考:
朋友问你:这个活动值得去吗?
于是你开始了解情况:
问:这个活动是线上还是线下?
答:线下,在市中心的一个咖啡馆。
问:天气怎么样?
答:那天天气不错。
问:有没有感兴趣的人会去?
答:有,你喜欢的那位也会去。
问:交通方便吗?
答:地铁直达,很方便。
……
于是你的大脑里就构建了这样一棵“决策树”:
绘图代码:
import graphviz
# 创建有向图,并设置中文字体
# 1. 导入Graphviz库:
# graphviz是Python与Graphviz软件交互的接口,负责生成可视化图形(需提前安装Graphviz软件)
import graphviz
# 2. 创建有向图对象,并配置中文显示:
# - comment:给图加注释(仅文档用途,不影响渲染)
# - format:指定输出文件格式(如png、pdf等)
# - node_attr/edge_attr/graph_attr:设置节点、边、全局的字体为"SimHei"(微软雅黑),解决中文乱码问题
dot = graphviz.Digraph(
comment='社交活动决策树', # 图的描述信息(可选)
format='png', # 输出文件格式
node_attr={'fontname': 'SimHei'}, # 节点文字用微软雅黑(支持中文)
edge_attr={'fontname': 'SimHei'}, # 边的标签文字用微软雅黑
graph_attr={'fontname': 'SimHei'} # 全局文字(如标题)用微软雅黑
)
# 3. 添加节点(node):
# 格式:dot.node(节点唯一标识, 显示的文字)
# 节点ID(如'A')是内部标记,显示文字是图形中实际展示的内容
dot.node('A', '活动类型') # 根节点:决策起点(活动类型)
dot.node('B', '线上') # 分支1:活动类型为线上
dot.node('C', '线下') # 分支2:活动类型为线下
dot.node('D', '天气是否好?') # 线下活动的子判断:天气条件
dot.node('E', '否') # 天气分支:不好
dot.node('F', '是') # 天气分支:好
dot.node('G', '有没有感兴趣的人?') # 天气好时的子判断:兴趣条件
dot.node('H', '否') # 兴趣分支:没有
dot.node('I', '有') # 兴趣分支:有
dot.node('J', '去参加') # 最终决策:参加
dot.node('K', '考虑其他安排') # 最终决策:不参加
# 4. 添加边(edge):
# 格式:dot.edge(起点ID, 终点ID, label=边的说明文字)
# label可选,用于解释分支条件
dot.edge('A', 'B') # 活动类型 → 线上(无额外条件,直接分支)
dot.edge('A', 'C') # 活动类型 → 线下(无额外条件,直接分支)
dot.edge('C', 'D') # 线下活动 → 判断天气
dot.edge('D', 'E', label='否') # 天气判断 → 否(边标注“否”)
dot.edge('D', 'F', label='是') # 天气判断 → 是(边标注“是”)
dot.edge('F', 'G') # 天气好 → 判断是否有感兴趣的人
dot.edge('G', 'H', label='否') # 兴趣判断 → 否(边标注“否”)
dot.edge('G', 'I', label='是') # 兴趣判断 → 是(边标注“是”)
dot.edge('I', 'J') # 有感兴趣的人 → 去参加(最终决策)
dot.edge('H', 'K') # 没有感兴趣的人 → 考虑其他安排(最终决策)
# 5. 渲染并查看图形:
# - 第一个参数:输出文件名(生成的文件会是 decision_tree_social_event.png)
# - view=True:生成后自动用系统默认程序打开图片
# 注意:运行前需确保:
# ① 系统安装了Graphviz软件(https://graphviz.org/download/)
# ② Graphviz的bin目录已加入系统环境变量(如 C:\Program Files\Graphviz\bin)
dot.render('decision_tree_social_event', view=True)
二、ID3决策树:基于信息增益的决策模型
2.1 信息增益的公式与符号解析
信息增益公式:
g
(
D
,
A
)
=
H
(
D
)
−
H
(
D
∣
A
)
g(D, A) = H(D) - H(D|A)
g(D,A)=H(D)−H(D∣A)
- g ( D , A ) g(D, A) g(D,A):特征 A A A对数据集 D D D的信息增益
-
H
(
D
)
H(D)
H(D):数据集
D
D
D的经验熵,衡量数据的不确定性
H ( D ) = − ∑ k = 1 K ∣ C k ∣ ∣ D ∣ log 2 ∣ C k ∣ ∣ D ∣ H(D) = -\sum_{k=1}^{K}\frac{|C_k|}{|D|}\log_2\frac{|C_k|}{|D|} H(D)=−k=1∑K∣D∣∣Ck∣log2∣D∣∣Ck∣
其中, K K K为类别数, ∣ C k ∣ |C_k| ∣Ck∣为第 k k k类的样本数, ∣ D ∣ |D| ∣D∣为总样本数。 -
H
(
D
∣
A
)
H(D|A)
H(D∣A):特征
A
A
A给定条件下
D
D
D的经验条件熵
H ( D ∣ A ) = ∑ i = 1 n ∣ D i ∣ ∣ D ∣ H ( D i ) = − ∑ i = 1 n ∣ D i ∣ ∣ D ∣ ∑ k = 1 K ∣ D i k ∣ ∣ D i ∣ log 2 ∣ D i k ∣ ∣ D i ∣ H(D|A) = \sum_{i=1}^{n}\frac{|D_i|}{|D|}H(D_i) = -\sum_{i=1}^{n}\frac{|D_i|}{|D|}\sum_{k=1}^{K}\frac{|D_{ik}|}{|D_i|}\log_2\frac{|D_{ik}|}{|D_i|} H(D∣A)=i=1∑n∣D∣∣Di∣H(Di)=−i=1∑n∣D∣∣Di∣k=1∑K∣Di∣∣Dik∣log2∣Di∣∣Dik∣
其中, n n n为特征 A A A的取值个数, ∣ D i ∣ |D_i| ∣Di∣为特征 A A A取第 i i i个值的样本数, ∣ D i k ∣ |D_{ik}| ∣Dik∣为 D i D_i Di中属于第 k k k类的样本数。
2.2 信息增益的意义
信息增益衡量的是某个特征对数据分类不确定性的减少程度。直观来说,它反映了在已知某个特征的取值后,我们对样本分类结果的不确定性降低了多少。
例如,在判断一个人是否适合贷款时,如果“是否有房”这一特征能显著帮助我们确定贷款审批结果,那么这个特征的信息增益就较大。换句话说,信息增益越大,该特征在分类任务中的判别能力越强。
2.3 ID3决策树案例演示:贷款申请分类
以贷款申请数据为例,计算各特征的信息增益并构建ID3决策树。
数据集:
年龄 | 有工作 | 有房子 | 信贷情况 | 类别 |
---|---|---|---|---|
青年 | 否 | 否 | 一般 | 拒绝 |
青年 | 否 | 否 | 好 | 拒绝 |
青年 | 是 | 否 | 好 | 同意 |
青年 | 是 | 是 | 一般 | 同意 |
青年 | 否 | 否 | 一般 | 拒绝 |
中年 | 否 | 否 | 一般 | 拒绝 |
中年 | 否 | 否 | 好 | 拒绝 |
中年 | 是 | 是 | 好 | 同意 |
中年 | 否 | 是 | 非常好 | 同意 |
中年 | 否 | 是 | 非常好 | 同意 |
老年 | 否 | 是 | 非常好 | 同意 |
老年 | 否 | 是 | 好 | 同意 |
老年 | 是 | 否 | 好 | 同意 |
老年 | 是 | 否 | 非常好 | 同意 |
老年 | 否 | 否 | 一般 | 拒绝 |
步骤1:计算数据集D的经验熵H(D)
数据集中共有15条样本,其中"同意"类5条,"拒绝"类10条:
H
(
D
)
=
−
5
15
log
2
5
15
−
10
15
log
2
10
15
=
0.9183
H(D) = -\frac{5}{15}\log_2\frac{5}{15} - \frac{10}{15}\log_2\frac{10}{15} = 0.9183
H(D)=−155log2155−1510log21510=0.9183
步骤2:计算各特征的条件熵和信息增益
以"年龄"特征为例,其取值为{青年, 中年, 老年}:
- 青年样本:5条(ID1-5),其中拒绝4条,同意1条
H ( 青年 ) = − 4 5 log 2 4 5 − 1 5 log 2 1 5 = 0.7219 H(青年) = -\frac{4}{5}\log_2\frac{4}{5} - \frac{1}{5}\log_2\frac{1}{5} = 0.7219 H(青年)=−54log254−51log251=0.7219 - 中年样本:5条(ID6-10),其中拒绝2条,同意3条
H ( 中年 ) = − 2 5 log 2 2 5 − 3 5 log 2 3 5 = 0.9709 H(中年) = -\frac{2}{5}\log_2\frac{2}{5} - \frac{3}{5}\log_2\frac{3}{5} = 0.9709 H(中年)=−52log252−53log253=0.9709 - 老年样本:5条(ID11-15),其中拒绝1条,同意4条
H ( 老年 ) = − 1 5 log 2 1 5 − 4 5 log 2 4 5 = 0.7219 H(老年) = -\frac{1}{5}\log_2\frac{1}{5} - \frac{4}{5}\log_2\frac{4}{5} = 0.7219 H(老年)=−51log251−54log254=0.7219 - 条件熵:
H ( D ∣ 年龄 ) = 5 15 × 0.7219 + 5 15 × 0.9709 + 5 15 × 0.7219 = 0.8049 H(D|年龄) = \frac{5}{15} \times 0.7219 + \frac{5}{15} \times 0.9709 + \frac{5}{15} \times 0.7219 = 0.8049 H(D∣年龄)=155×0.7219+155×0.9709+155×0.7219=0.8049 - 信息增益:
g ( D , 年龄 ) = 0.9183 − 0.8049 = 0.1134 g(D, 年龄) = 0.9183 - 0.8049 = 0.1134 g(D,年龄)=0.9183−0.8049=0.1134
同理计算其他特征的信息增益:
- g ( D , 有工作 ) = 0.3236 g(D, 有工作) = 0.3236 g(D,有工作)=0.3236
- g ( D , 有房子 ) = 0.4208 g(D, 有房子) = 0.4208 g(D,有房子)=0.4208
- g ( D , 信贷情况 ) = 0.3623 g(D, 信贷情况) = 0.3623 g(D,信贷情况)=0.3623
步骤3:选择信息增益最大的特征分裂
"有房子"的信息增益最大(0.4208),作为根节点分裂,得到左右子树:
- 有房子:7条样本(ID4,8,9,10,11,12,14),其中6条同意,1条拒绝
- 无房子:8条样本(ID1,2,3,5,6,7,13,15),其中2条同意,6条拒绝
步骤4:递归构建子树
对每个子树重复上述过程,最终生成完整的ID3决策树。
这是一棵用于 贷款申请分类 的决策树(基于ID3算法,通过信息熵选择分裂特征),核心判断逻辑如下:
根节点:判断“是否有房子”
- 条件:按特征取值分裂(右分支为
有房子 > 0.5
,左分支为有房子 ≤ 0.5
)。 - 信息增益:0.42(衡量该特征对分类的贡献度)。
- 样本数:15(全量样本)。
- 类别分布:拒绝6人,同意9人(
分布:拒绝=6,同意=9
)。
分支逻辑
-
右分支(有房子 > 0.5)
- 样本:6人(所有“有房”申请人)。
- 类别分布:同意6人,拒绝0人。
- 决策:直接判定为 同意(叶子节点)。
-
左分支(有房子 ≤ 0.5)
- 进入下一层判断 “是否有工作”:
第二层节点:判断“是否有工作”
- 条件:按特征取值分裂(右分支为
有工作 > 0.5
,左分支为有工作 ≤ 0.5
)。 - 信息增益:0.918(该特征对剩余样本的分类贡献度)。
- 样本数:9人(所有“无房”申请人)。
- 类别分布:拒绝6人,同意3人(
分布:拒绝=6,同意=3
)。
最终叶子节点
-
左子节点(有工作 ≤ 0.5)
- 样本:6人(无房且无工作的申请人)。
- 类别分布:同意0人,拒绝6人。
- 决策:拒绝(叶子节点)。
-
右子节点(有工作 > 0.5)
- 样本:3人(无房但有工作的申请人)。
- 类别分布:同意3人,拒绝0人。
- 决策:同意(叶子节点)。
决策树通过 “有房→有工作” 的两步判断,输出贷款决策:
- 有房 → 直接同意;
- 无房但有工作 → 同意;
- 无房且无工作 → 拒绝。
(注:LabelEncoder
按字母序编码类别,此处“同意”为0
,“拒绝”为1
,节点分布以 拒绝=X,同意=Y
格式展示。)
绘图代码:
# 导入必要的库
import pandas as pd # 用于数据处理和构建 DataFrame
from collections import Counter # 用于统计样本数量
from math import log2 # 用于计算对数
from graphviz import Digraph # 用于可视化决策树
from sklearn.preprocessing import LabelEncoder # 用于将字符串特征编码为数字
# =============================================
# 1. 数据准备
# =============================================
# 构建训练数据集,包含 5 个特征和 1 个目标变量
data = {
# 每个样本的唯一编号,容易造成过拟合
# "ID": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
# 年龄:青年、中年、老年
"年龄": ["青年", "青年", "青年", "青年", "青年", "中年", "中年", "中年", "中年", "中年",
"老年", "老年", "老年", "老年", "老年"],
# 是否有工作:是、否
"有工作": ["否", "否", "是", "是", "否", "否", "否", "是", "否", "否",
"否", "否", "是", "是", "否"],
# 是否有房子:是、否
"有房子": ["否", "否", "否", "是", "否", "否", "否", "是", "是", "是",
"是", "是", "否", "否", "否"],
# 信贷情况:一般、好、非常好
"信贷情况": ["一般", "好", "好", "一般", "一般", "一般", "好", "好", "非常好", "非常好",
"非常好", "好", "好", "非常好", "一般"],
# 类别:是否贷款成功(同意/拒绝)
"类别": ["拒绝", "拒绝", "同意", "同意", "拒绝", "拒绝", "拒绝", "同意", "同意", "同意",
"同意", "同意", "同意", "同意", "拒绝"]
}
# 将数据转换为 pandas 的 DataFrame 格式
df = pd.DataFrame(data)
# 提取特征列名列表(所有列除了最后一列)
features = list(df.columns[:-1]) # 特征:ID、年龄、有工作、有房子、信贷情况
# 目标变量列名(最后一列)
target = df.columns[-1] # 类别列名
# =============================================
# 2. 信息熵与信息增益计算
# =============================================
def entropy(y):
"""
计算信息熵 H(Y)
y: 当前样本的目标变量值(如:['拒绝', '同意', ...])
返回:当前样本的信息熵
"""
counts = Counter(y) # 统计每个类别的数量
total = len(y) # 总样本数
if total == 0:
return 0 # 防止除以 0
# 计算 -Σ(p * log2(p))
return -sum((count / total) * log2(count / total) for count in counts.values())
def conditional_entropy(X_col, y):
"""
计算条件熵 H(Y|X)
X_col: 某个特征列的值(如:['青年', '中年', ...])
y: 对应的目标变量值
返回:给定该特征下目标变量的条件熵
"""
values = set(X_col) # 获取该特征的所有唯一值
total = len(y) # 总样本数
ent = 0
for v in values:
mask = (X_col == v) # 找到该特征值对应的样本
y_sub = y[mask] # 取出这些样本的目标变量
ent += len(y_sub) / total * entropy(y_sub) # 加权求和
return ent
def info_gain(dataset, feature):
"""
计算某个特征的信息增益 IG(Y, X)
dataset: 整个数据集
feature: 当前特征名称(如:"年龄")
返回:该特征的信息增益
"""
X_col = dataset[feature] # 取出该特征列
y = dataset[target] # 取出目标变量列
return entropy(y) - conditional_entropy(X_col, y) # 信息增益 = 总熵 - 条件熵
# =============================================
# 3. 手动实现 ID3 决策树
# =============================================
def id3_tree(dataset, features):
"""
使用 ID3 算法递归构建决策树
dataset: 当前数据集
features: 当前可用特征列表
返回:当前节点的决策树结构字典
"""
target_values = dataset[target].unique() # 获取当前目标变量的唯一值
# 终止条件1:当前样本全部属于同一类
if len(target_values) == 1:
return {'label': target_values[0]} # 返回叶子节点标签
# 终止条件2:没有可用特征
if not features:
most_common = dataset[target].mode().iloc[0] # 返回多数类作为预测结果
return {'label': most_common}
# 选择信息增益最大的特征
gains = {f: info_gain(dataset, f) for f in features} # 计算每个特征的信息增益
best_feature = max(gains, key=gains.get) # 选出最大增益的特征
# 构建当前节点信息
class_counts = dict(Counter(dataset[target])) # 统计目标变量分布
node_info = {
'feature': best_feature,
'gain': gains[best_feature], # 信息增益
'class_dist': class_counts, # 类别分布
'n_samples': len(dataset), # 样本数量
'children': {} # 子节点
}
# 递归划分每个特征值
remaining_features = [f for f in features if f != best_feature] # 剩余可用特征
for raw_value in dataset[best_feature].unique(): # 遍历当前特征的每个唯一值
sub_data = dataset[dataset[best_feature] == raw_value] # 划分子数据集
node_info['children'][raw_value] = id3_tree(sub_data, remaining_features) # 递归生成子树
return node_info
# =============================================
# 4. 构建特征编码器(用于生成 CART 风格的分割条件)
# =============================================
# 保存每个特征的编码器,便于后续可视化时使用
feature_encoders = {}
for feat in features:
le = LabelEncoder() # 创建编码器
le.fit(df[feat]) # 拟合该特征的编码规则
feature_encoders[feat] = le # 保存编码器
# =============================================
# 5. 决策树可视化(CART 风格,支持中文)
# =============================================
def visualize_tree_cart_style(tree, feature_encoders, graph=None, parent_id=None, edge_label=""):
"""
使用 Graphviz 可视化决策树(CART 风格)
tree: 当前树节点结构
feature_encoders: 编码器字典
graph: 当前图对象
parent_id: 父节点 ID
edge_label: 边上的标签
返回:Graphviz 图对象
"""
from uuid import uuid4 # 用于生成唯一节点 ID
if graph is None:
# 初始化一个空图,设置字体支持中文
graph = Digraph(
'DecisionTree',
format='png',
node_attr={'fontname': 'SimHei'}, # 支持中文节点
edge_attr={'fontname': 'SimHei'}, # 支持中文边标签
graph_attr={'fontname': 'SimHei', 'rankdir': 'TB'} # 树方向从上往下
)
node_id = str(uuid4()) # 生成当前节点的唯一标识符
if 'label' in tree:
# 如果是叶子节点,显示类别标签
label = f"类别: {tree['label']}"
shape = "box" # 叶子节点用矩形
else:
# 如果是内部节点,显示特征、增益等信息
feat_name = tree['feature']
metric = round(tree.get('gain', 0), 3) # 获取信息增益
n = tree['n_samples'] # 样本数
dist = ", ".join([f"{k}={v}" for k, v in tree['class_dist'].items()]) # 分布
# 构造节点标签
label = (
f"{feat_name}\\n"
f"信息增益={metric}\\n"
f"样本数={n}\\n"
f"分布:{dist}"
)
shape = "ellipse" # 内部节点用椭圆
graph.node(node_id, label=label, shape=shape) # 添加当前节点到图中
if parent_id:
# 如果有父节点,则画边
graph.edge(parent_id, node_id, label=edge_label)
if 'children' in tree:
# 如果有子节点,递归绘制
feat_name = tree['feature']
encoder = feature_encoders[feat_name] # 获取该特征的编码器
unique_values = df[feat_name].unique() # 获取原始特征值
sorted_values = sorted(encoder.transform(unique_values)) # 编码后的排序值
for raw_value, child in tree['children'].items():
encoded_value = encoder.transform([raw_value])[0] # 把原始值转为编码值
display_label = get_edge_label(feat_name, encoded_value, sorted_values) # 生成边标签
# 递归绘制子树
visualize_tree_cart_style(child, feature_encoders, graph, node_id, display_label)
return graph
def get_edge_label(feat_name, encoded_value, sorted_values):
"""
生成边标签,例如:年龄 <= 0.5、年龄 > 0.5
feat_name: 特征名
encoded_value: 编码后的特征值
sorted_values: 排序后的编码值列表
返回:边标签字符串
"""
index = sorted_values.index(encoded_value)
if index == 0:
threshold = (sorted_values[index] + sorted_values[index + 1]) / 2
return f"{feat_name} <= {threshold:.1f}"
elif index == len(sorted_values) - 1:
threshold = (sorted_values[index - 1] + sorted_values[index]) / 2
return f"{feat_name} > {threshold:.1f}"
else:
low = (sorted_values[index - 1] + sorted_values[index]) / 2
high = (sorted_values[index] + sorted_values[index + 1]) / 2
return f"{low:.1f} < {feat_name} <= {high:.1f}"
# =============================================
# 6. 主程序:构建并可视化 ID3 决策树
# =============================================
# 构建模型
tree_id3 = id3_tree(df, features) # 构建 ID3 决策树
# 设置可视化图例格式
dot_id3 = visualize_tree_cart_style(tree_id3, feature_encoders) # 可视化 ID3 树
# 保存为 PNG 文件,并自动打开查看
dot_id3.render("id3_decision_tree_with_id", view=True)
2.4 ID3决策树缺陷
ID3决策树的核心优势是计算简单、可解释性强,但存在明显缺陷:倾向于选择取值较多的特征(如"ID"特征可能有最大信息增益),导致过拟合风险。
ID3决策树的缺陷演示:被“ID”特征误导的过拟合
- 构造含干扰特征的数据集
在原有贷款数据中加入 “ID” 特征(唯一标识,与贷款结果无实际关联),数据如下:
ID | 年龄 | 有工作 | 有房子 | 信贷情况 | 类别 |
---|---|---|---|---|---|
1 | 青年 | 否 | 否 | 一般 | 拒绝 |
2 | 青年 | 否 | 否 | 好 | 拒绝 |
3 | 青年 | 是 | 否 | 好 | 同意 |
… | … | … | … | … | … |
(共15条,ID=1~15,类别不变)
-
信息增益计算(核心对比)
① 总熵 ( H(D) )
总样本15条,同意=9,拒绝=6:
H ( D ) = − 9 15 log 2 9 15 − 6 15 log 2 6 15 ≈ 0.918 H(D) = -\frac{9}{15}\log_2\frac{9}{15} - \frac{6}{15}\log_2\frac{6}{15} \approx 0.918 H(D)=−159log2159−156log2156≈0.918② “ID”特征的信息增益
“ID”有15个唯一值(每个ID对应1条样本),每个子集 D i D_i Di 只有1条样本(纯类别,熵为0):
H ( D ∣ ID ) = ∑ i = 1 15 1 15 × 0 = 0 ⇒ g ( D , ID ) = 0.918 − 0 = 0.918 H(D|\text{ID}) = \sum_{i=1}^{15} \frac{1}{15} \times 0 = 0 \quad \Rightarrow \quad g(D, \text{ID}) = 0.918 - 0 = 0.918 H(D∣ID)=i=1∑15151×0=0⇒g(D,ID)=0.918−0=0.918③ 有效特征“有房子”的信息增益
“有房子”分两类(无房=8条,有房=7条),条件熵计算得 H ( D ∣ 有房子 ) ≈ 0.497 H(D|\text{有房子}) \approx 0.497 H(D∣有房子)≈0.497,故:
g ( D , 有房子 ) ≈ 0.918 − 0.497 = 0.421 g(D, \text{有房子}) \approx 0.918 - 0.497 = 0.421 g(D,有房子)≈0.918−0.497=0.421 -
缺陷暴露:被“ID”误导的过拟合
- 增益排序:
g(D, ID) > g(D, 有房子)
,ID3会优先选 “ID” 作为根节点。 - 逻辑荒谬:“ID”是纯标识(与贷款结果无关),但因取值唯一,决策树会分裂出15个叶子节点(每个ID对应一个结果),导致 模型仅记忆样本,无法泛化新数据(如ID=16的新样本,树无法判断)。
- 增益排序:
这就是ID3的核心缺陷:盲目追求“信息增益最大”,倾向选择取值多的特征(如ID),最终导致过拟合。
三、C4.5决策树:信息增益率的优化
3.1 信息增益率的公式与符号解析
信息增益率公式:
Gain_Ratio
(
D
,
A
)
=
g
(
D
,
A
)
IV
(
A
)
\text{Gain\_Ratio}(D, A) = \frac{g(D, A)}{\text{IV}(A)}
Gain_Ratio(D,A)=IV(A)g(D,A)
- Gain_Ratio ( D , A ) \text{Gain\_Ratio}(D, A) Gain_Ratio(D,A):特征 A A A的信息增益率
- g ( D , A ) g(D, A) g(D,A):特征 A A A的信息增益
-
IV
(
A
)
\text{IV}(A)
IV(A):特征
A
A
A的内在信息(分裂信息)
IV ( A ) = − ∑ i = 1 n ∣ D i ∣ ∣ D ∣ log 2 ∣ D i ∣ ∣ D ∣ \text{IV}(A) = -\sum_{i=1}^{n}\frac{|D_i|}{|D|}\log_2\frac{|D_i|}{|D|} IV(A)=−i=1∑n∣D∣∣Di∣log2∣D∣∣Di∣
其中, n n n为特征 A A A的取值个数, ∣ D i ∣ |D_i| ∣Di∣为特征 A A A取第 i i i个值的样本数。
3.2 信息增益率的意义
信息增益率通过引入一个称为内在信息(Intrinsic Value,IV)的指标对信息增益进行归一化处理。其目的是解决ID3算法中偏向于选择取值较多特征的问题。
内在信息 IV ( A ) \text{IV}(A) IV(A) 反映了特征 A A A 取值分布的均匀程度。分布越均匀, IV ( A ) \text{IV}(A) IV(A) 的值就越大。通过将信息增益除以 IV ( A ) \text{IV}(A) IV(A),信息增益率有效降低了那些取值数量多、但划分意义不大的特征的优先级。
可以将其理解为:信息增益是一个“原始得分”,而信息增益率是根据题目难度调整后的“加权得分”。对于取值较少但具有明显分类效果的特征,它会得到更高的认可,从而避免模型错误地偏好那些取值繁多但实际判别能力一般的特征。
3.3 C4.5决策树:信息增益率修正ID3的过拟合缺陷
含ID特征的数据集构造 :
在原贷款数据中加入ID列(取值1~15,唯一标识,与贷款结果无关),数据集如下:
ID | 年龄 | 有工作 | 有房子 | 信贷情况 | 类别 |
---|---|---|---|---|---|
1 | 青年 | 否 | 否 | 一般 | 拒绝 |
2 | 青年 | 否 | 否 | 好 | 拒绝 |
3 | 青年 | 是 | 否 | 好 | 同意 |
4 | 青年 | 是 | 是 | 一般 | 同意 |
5 | 青年 | 否 | 否 | 一般 | 拒绝 |
6 | 中年 | 否 | 否 | 一般 | 拒绝 |
7 | 中年 | 否 | 否 | 好 | 拒绝 |
8 | 中年 | 是 | 是 | 好 | 同意 |
9 | 中年 | 否 | 是 | 非常好 | 同意 |
10 | 中年 | 否 | 是 | 非常好 | 同意 |
11 | 老年 | 否 | 是 | 非常好 | 同意 |
12 | 老年 | 否 | 是 | 好 | 同意 |
13 | 老年 | 是 | 否 | 好 | 同意 |
14 | 老年 | 是 | 否 | 非常好 | 同意 |
15 | 老年 | 否 | 否 | 一般 | 拒绝 |
步骤1:计算数据集总熵
H
(
D
)
H(D)
H(D)
总样本15条,同意=9,拒绝=6:
H
(
D
)
=
−
9
15
log
2
9
15
−
6
15
log
2
6
15
≈
0.918
H(D) = -\frac{9}{15}\log_2\frac{9}{15} - \frac{6}{15}\log_2\frac{6}{15} \approx 0.918
H(D)=−159log2159−156log2156≈0.918
步骤2:计算“ID”特征的信息增益率
-
信息增益 g ( D , ID ) g(D, \text{ID}) g(D,ID):
ID有15个唯一值,每个子集仅1条样本(纯类别,熵为0):
H ( D ∣ ID ) = ∑ i = 1 15 1 15 × 0 = 0 ⇒ g ( D , ID ) = 0.918 − 0 = 0.918 H(D|\text{ID}) = \sum_{i=1}^{15}\frac{1}{15} \times 0 = 0 \quad \Rightarrow \quad g(D, \text{ID}) = 0.918 - 0 = 0.918 H(D∣ID)=i=1∑15151×0=0⇒g(D,ID)=0.918−0=0.918 -
内在信息 IV(ID) \text{IV(ID)} IV(ID):
IV(ID) = − ∑ i = 1 15 1 15 log 2 1 15 ≈ 3.907 \text{IV(ID)} = -\sum_{i=1}^{15}\frac{1}{15}\log_2\frac{1}{15} \approx 3.907 IV(ID)=−i=1∑15151log2151≈3.907 -
信息增益率:
Gain_Ratio ( D , ID ) = 0.918 3.907 ≈ 0.235 \text{Gain\_Ratio}(D, \text{ID}) = \frac{0.918}{3.907} \approx 0.235 Gain_Ratio(D,ID)=3.9070.918≈0.235
步骤3:计算“有房子”特征的信息增益率
-
信息增益 g ( D , 有房子 ) g(D, \text{有房子}) g(D,有房子):
有房=7条(同意6,拒绝1),无房=8条(同意2,拒绝6):
H ( D ∣ 有房子 ) = 7 15 ( − 6 7 log 2 6 7 − 1 7 log 2 1 7 ) + 8 15 ( − 2 8 log 2 2 8 − 6 8 log 2 6 8 ) ≈ 0.497 H(D|\text{有房子}) = \frac{7}{15}\left(-\frac{6}{7}\log_2\frac{6}{7}-\frac{1}{7}\log_2\frac{1}{7}\right) + \frac{8}{15}\left(-\frac{2}{8}\log_2\frac{2}{8}-\frac{6}{8}\log_2\frac{6}{8}\right) \approx 0.497 H(D∣有房子)=157(−76log276−71log271)+158(−82log282−86log286)≈0.497g ( D , 有房子 ) = 0.918 − 0.497 = 0.421 g(D, \text{有房子}) = 0.918 - 0.497 = 0.421 g(D,有房子)=0.918−0.497=0.421
-
内在信息 IV(有房子) \text{IV(有房子)} IV(有房子):
IV(有房子) = − 7 15 log 2 7 15 − 8 15 log 2 8 15 ≈ 0.998 \text{IV(有房子)} = -\frac{7}{15}\log_2\frac{7}{15} - \frac{8}{15}\log_2\frac{8}{15} \approx 0.998 IV(有房子)=−157log2157−158log2158≈0.998
-
信息增益率:
Gain_Ratio ( D , 有房子 ) = 0.421 0.998 ≈ 0.422 \text{Gain\_Ratio}(D, \text{有房子}) = \frac{0.421}{0.998} \approx 0.422 Gain_Ratio(D,有房子)=0.9980.421≈0.422
特征选择对比
特征 | 信息增益 ( g ) | 内在信息 IV \text{IV} IV | 信息增益率 |
---|---|---|---|
ID | 0.918 | 3.907 | 0.235 |
有房子 | 0.421 | 0.998 | 0.422 |
步骤4:递归构建子树
- 分裂子节点:以信息增益率最高的特征( “有房子” )为根节点分裂后,得到两个子节点:左子节点:无房样本(8 条,同意 2,拒绝 6)右子节点:有房样本(7 条,同意 6,拒绝 1)递归应用
- C4.5 逻辑:对每个子节点数据集,重复步骤 1-3:计算子节点数据集的总熵 H ( D i ) H(D_i) H(Di);计算各特征(年龄、有工作、信贷情况、ID)的信息增益率;选择信息增益率最大的特征分裂(若存在)。
- 终止条件(满足任一条件则停止分裂):子节点所有样本属于同一类别(如右子节点 “有房” 样本中 6 同意 1 拒绝,若继续分裂可能得到纯类别节点);信息增益率小于预设阈值(避免无意义分裂);特征已全部使用或样本数少于最小阈值。
I D 3 决策树 ID3决策树 ID3决策树
C
4.5
决策树
C4.5决策树
C4.5决策树
绘图代码:
# 导入必要的库
import pandas as pd # 用于数据处理和构建 DataFrame
from collections import Counter # 用于统计样本数量
from math import log2 # 用于计算对数
from graphviz import Digraph # 用于可视化决策树
from sklearn.preprocessing import LabelEncoder # 用于将字符串特征编码为数字
# =============================================
# 1. 数据准备(含 ID)
# =============================================
# 构建训练数据集,包含 5 个特征和 1 个目标变量
data = {
# 每个样本的唯一编号,容易造成过拟合
"ID": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
# 年龄:青年、中年、老年
"年龄": ["青年", "青年", "青年", "青年", "青年", "中年", "中年", "中年", "中年", "中年",
"老年", "老年", "老年", "老年", "老年"],
# 是否有工作:是、否
"有工作": ["否", "否", "是", "是", "否", "否", "否", "是", "否", "否",
"否", "否", "是", "是", "否"],
# 是否有房子:是、否
"有房子": ["否", "否", "否", "是", "否", "否", "否", "是", "是", "是",
"是", "是", "否", "否", "否"],
# 信贷情况:一般、好、非常好
"信贷情况": ["一般", "好", "好", "一般", "一般", "一般", "好", "好", "非常好", "非常好",
"非常好", "好", "好", "非常好", "一般"],
# 类别:是否贷款成功(同意/拒绝)
"类别": ["拒绝", "拒绝", "同意", "同意", "拒绝", "拒绝", "拒绝", "同意", "同意", "同意",
"同意", "同意", "同意", "同意", "拒绝"]
}
# 将数据转换为 pandas 的 DataFrame 格式
df = pd.DataFrame(data)
# 提取特征列名列表(所有列除了最后一列)
features = list(df.columns[:-1]) # 特征:ID、年龄、有工作、有房子、信贷情况
# 目标变量列名(最后一列)
target = df.columns[-1] # 类别列名
# =============================================
# 2. 信息熵与信息增益计算
# =============================================
def entropy(y):
"""
计算信息熵 H(Y)
y: 当前样本的目标变量值(如:['拒绝', '同意', ...])
返回:当前样本的信息熵
"""
counts = Counter(y) # 统计每个类别的数量
total = len(y) # 总样本数
if total == 0:
return 0 # 防止除以 0
# 计算 -Σ(p * log2(p))
return -sum((count / total) * log2(count / total) for count in counts.values())
def conditional_entropy(X_col, y):
"""
计算条件熵 H(Y|X)
X_col: 某个特征列的值(如:['青年', '中年', ...])
y: 对应的目标变量值
返回:给定该特征下目标变量的条件熵
"""
values = set(X_col) # 获取该特征的所有唯一值
total = len(y) # 总样本数
ent = 0
for v in values:
mask = (X_col == v) # 找到该特征值对应的样本
y_sub = y[mask] # 取出这些样本的目标变量
ent += len(y_sub) / total * entropy(y_sub) # 加权求和
return ent
def info_gain(dataset, feature):
"""
计算某个特征的信息增益 IG(Y, X)
dataset: 整个数据集
feature: 当前特征名称(如:"年龄")
返回:该特征的信息增益
"""
X_col = dataset[feature] # 取出该特征列
y = dataset[target] # 取出目标变量列
return entropy(y) - conditional_entropy(X_col, y) # 信息增益 = 总熵 - 条件熵
def split_info(X_col):
"""
计算分裂信息(Split Information),用于 C4.5 增益率计算
X_col: 某个特征列的值
返回:该特征的分裂信息
"""
counts = Counter(X_col) # 统计每个特征值出现的次数
total = len(X_col) # 总样本数
# 计算 -Σ(|D_v|/|D| * log2(|D_v|/|D|))
return -sum((count / total) * log2(count / total) for count in counts.values())
def gain_ratio(dataset, feature):
"""
计算增益率 GainRatio(Y, X)
dataset: 整个数据集
feature: 当前特征名称
返回:该特征的增益率
"""
ig = info_gain(dataset, feature) # 先计算信息增益
si = split_info(dataset[feature]) # 再计算分裂信息
return ig / si if si != 0 else float('inf') # 增益率 = IG / SI
# =============================================
# 3. 手动实现 ID3 和 C4.5 决策树
# =============================================
def id3_tree(dataset, features):
"""
使用 ID3 算法递归构建决策树
dataset: 当前数据集
features: 当前可用特征列表
返回:当前节点的决策树结构字典
"""
target_values = dataset[target].unique() # 获取当前目标变量的唯一值
# 终止条件1:当前样本全部属于同一类
if len(target_values) == 1:
return {'label': target_values[0]} # 返回叶子节点标签
# 终止条件2:没有可用特征
if not features:
most_common = dataset[target].mode().iloc[0] # 返回多数类作为预测结果
return {'label': most_common}
# 选择信息增益最大的特征
gains = {f: info_gain(dataset, f) for f in features} # 计算每个特征的信息增益
best_feature = max(gains, key=gains.get) # 选出最大增益的特征
# 构建当前节点信息
class_counts = dict(Counter(dataset[target])) # 统计目标变量分布
node_info = {
'feature': best_feature,
'gain': gains[best_feature], # 信息增益
'class_dist': class_counts, # 类别分布
'n_samples': len(dataset), # 样本数量
'children': {} # 子节点
}
# 递归划分每个特征值
remaining_features = [f for f in features if f != best_feature] # 剩余可用特征
for raw_value in dataset[best_feature].unique(): # 遍历当前特征的每个唯一值
sub_data = dataset[dataset[best_feature] == raw_value] # 划分子数据集
node_info['children'][raw_value] = id3_tree(sub_data, remaining_features) # 递归生成子树
return node_info
def c45_tree(dataset, features):
"""
使用 C4.5 算法递归构建决策树(基于增益率)
dataset: 当前数据集
features: 当前可用特征列表
返回:当前节点的决策树结构字典
"""
target_values = dataset[target].unique()
# 终止条件1:当前样本全部属于同一类
if len(target_values) == 1:
return {'label': target_values[0]}
# 终止条件2:没有可用特征
if not features:
most_common = dataset[target].mode().iloc[0]
return {'label': most_common}
# 选择增益率最大的特征
gains = {f: gain_ratio(dataset, f) for f in features}
best_feature = max(gains, key=gains.get)
# 构建当前节点信息
class_counts = dict(Counter(dataset[target]))
node_info = {
'feature': best_feature,
'gain_ratio': round(gains[best_feature], 3),
'class_dist': class_counts,
'n_samples': len(dataset),
'children': {}
}
# 递归划分每个特征值
remaining_features = [f for f in features if f != best_feature]
for raw_value in dataset[best_feature].unique():
sub_data = dataset[dataset[best_feature] == raw_value]
node_info['children'][raw_value] = c45_tree(sub_data, remaining_features)
return node_info
# =============================================
# 4. 构建特征编码器(用于生成 CART 风格的分割条件)
# =============================================
# 保存每个特征的编码器,便于后续可视化时使用
feature_encoders = {}
for feat in features:
le = LabelEncoder() # 创建编码器
le.fit(df[feat]) # 拟合该特征的编码规则
feature_encoders[feat] = le # 保存编码器
# =============================================
# 5. 决策树可视化(CART 风格,支持中文)
# =============================================
def visualize_tree_cart_style(tree, feature_encoders, graph=None, parent_id=None, edge_label=""):
"""
使用 Graphviz 可视化决策树(CART 风格)
tree: 当前树节点结构
feature_encoders: 编码器字典
graph: 当前图对象
parent_id: 父节点 ID
edge_label: 边上的标签
返回:Graphviz 图对象
"""
from uuid import uuid4 # 用于生成唯一节点 ID
if graph is None:
# 初始化一个空图,设置字体支持中文
graph = Digraph(
'DecisionTree',
format='png',
node_attr={'fontname': 'SimHei'}, # 支持中文节点
edge_attr={'fontname': 'SimHei'}, # 支持中文边标签
graph_attr={'fontname': 'SimHei', 'rankdir': 'TB'} # 树方向从上往下
)
node_id = str(uuid4()) # 生成当前节点的唯一标识符
if 'label' in tree:
# 如果是叶子节点,显示类别标签
label = f"类别: {tree['label']}"
shape = "box" # 叶子节点用矩形
else:
# 如果是内部节点,显示特征、增益等信息
feat_name = tree['feature']
metric = round(tree.get('gain', tree.get('gain_ratio', 0)), 3) # 获取指标值
metric_label = "信息增益" if 'gain' in tree else "信息增益率"
n = tree['n_samples'] # 样本数
dist = ", ".join([f"{k}={v}" for k, v in tree['class_dist'].items()]) # 分布
# 构造节点标签
label = (
f"{feat_name}\\n"
f"{metric_label}={metric}\\n"
f"样本数={n}\\n"
f"分布:{dist}"
)
shape = "ellipse" # 内部节点用椭圆
graph.node(node_id, label=label, shape=shape) # 添加当前节点到图中
if parent_id:
# 如果有父节点,则画边
graph.edge(parent_id, node_id, label=edge_label)
if 'children' in tree:
# 如果有子节点,递归绘制
feat_name = tree['feature']
encoder = feature_encoders[feat_name] # 获取该特征的编码器
unique_values = df[feat_name].unique() # 获取原始特征值
sorted_values = sorted(encoder.transform(unique_values)) # 编码后的排序值
for raw_value, child in tree['children'].items():
encoded_value = encoder.transform([raw_value])[0] # 把原始值转为编码值
display_label = get_edge_label(feat_name, encoded_value, sorted_values) # 生成边标签
# 递归绘制子树
visualize_tree_cart_style(child, feature_encoders, graph, node_id, display_label)
return graph
def get_edge_label(feat_name, encoded_value, sorted_values):
"""
生成边标签,例如:年龄 <= 0.5、年龄 > 0.5
feat_name: 特征名
encoded_value: 编码后的特征值
sorted_values: 排序后的编码值列表
返回:边标签字符串
"""
index = sorted_values.index(encoded_value)
if index == 0:
threshold = (sorted_values[index] + sorted_values[index + 1]) / 2
return f"{feat_name} <= {threshold:.1f}"
elif index == len(sorted_values) - 1:
threshold = (sorted_values[index - 1] + sorted_values[index]) / 2
return f"{feat_name} > {threshold:.1f}"
else:
low = (sorted_values[index - 1] + sorted_values[index]) / 2
high = (sorted_values[index] + sorted_values[index + 1]) / 2
return f"{low:.1f} < {feat_name} <= {high:.1f}"
# =============================================
# 6. 主程序:构建并可视化两种树
# =============================================
# 构建模型
tree_id3 = id3_tree(df, features) # 构建 ID3 决策树
tree_c45 = c45_tree(df, features) # 构建 C4.5 决策树
# 设置可视化图例格式
dot_id3 = visualize_tree_cart_style(tree_id3, feature_encoders) # 可视化 ID3 树
dot_c45 = visualize_tree_cart_style(tree_c45, feature_encoders) # 可视化 C4.5 树
# 保存为 PNG 文件,并自动打开查看
dot_id3.render("id3_decision_tree_with_id", view=True)
dot_c45.render("c45_decision_tree_with_id", view=True)
3.4 结论:C4.5如何解决过拟合
- ID3的缺陷:ID特征因信息增益最大(0.918)被优先选择,导致树分裂为15个叶子节点,完全记忆训练数据(过拟合)。
- C4.5的修正:ID的信息增益虽高,但内在信息 V ( I D ) V(ID) V(ID)=3.907极大,导致信息增益率(0.235)低于“有房子”(0.422),因此C4.5选择 “有房子” 作为根节点,避免被ID特征误导。
- 本质原因:信息增益率通过IV(A)惩罚了取值多但分类意义小的特征(如ID),使模型更关注真正有判别力的特征,降低过拟合风险。
3.5 C4.5的局限性
- 对取值少的特征仍有偏向:若某特征取值少且IV(A)小,可能被过度优先选择(如两取值特征,IV=1时信息增益率=信息增益)。
- 计算复杂度高:需额外计算IV(A),比ID3耗时更多。
通过信息增益率的归一化,C4.5在保留ID3可解释性的同时,有效缓解了“取值多特征偏好”问题,是决策树从理论到工程应用的重要改进。
四、CART决策树:基于基尼指数的二叉树模型
CART 决策树是一种基于基尼指数(或平方误差)的二叉树模型,广泛应用于分类和回归任务中。它通过最小化基尼指数来选择最优划分特征,确保每一步划分都能带来最大纯度提升。相较于 ID3 或 C4.5 等多叉树模型,CART 结构简单、计算效率高、鲁棒性强,是工业界常用的一种决策树实现方式。
4.1 基尼指数的公式与符号解析
基尼指数相关公式:
-
数据集 D D D的基尼值:
Gini ( D ) = 1 − ∑ k = 1 K ( ∣ C k ∣ ∣ D ∣ ) 2 \text{Gini}(D) = 1 - \sum_{k=1}^{K}\left(\frac{|C_k|}{|D|}\right)^2 Gini(D)=1−k=1∑K(∣D∣∣Ck∣)2 -
特征 A A A的基尼指数:
Gini_index ( D , A ) = ∑ i = 1 n ∣ D i ∣ ∣ D ∣ Gini ( D i ) \text{Gini\_index}(D, A) = \sum_{i=1}^{n}\frac{|D_i|}{|D|}\text{Gini}(D_i) Gini_index(D,A)=i=1∑n∣D∣∣Di∣Gini(Di) -
Gini ( D ) \text{Gini}(D) Gini(D):衡量数据集 D D D的不纯度,值越小纯度越高
-
Gini_index ( D , A ) \text{Gini\_index}(D, A) Gini_index(D,A):特征 A A A的基尼指数,用于选择最优分裂特征
-
K K K:类别数, ∣ C k ∣ |C_k| ∣Ck∣:第 k k k类的样本数, ∣ D ∣ |D| ∣D∣:总样本数
-
n n n:特征 A A A的取值个数, ∣ D i ∣ |D_i| ∣Di∣:特征 A A A取第 i i i个值的样本数
4.2 基尼指数的意义
-
基尼指数衡量的是从数据集中随机抽取两个样本时,它们类别不一致的概率。这个指标越小,说明数据集的纯度越高;反之,则表示数据越混杂。
-
在决策树构建过程中,当我们使用某个特征对数据集进行划分后,会分别计算每个子集的基尼指数,并通过加权求和得到整体的基尼指数(即 Gini_index ( D , A ) \text{Gini\_index}(D, A) Gini_index(D,A) )。该值越小,意味着划分后的各子集纯度越高,分类效果越好。
-
可以形象地理解为:假设你有一袋球,颜色各异。如果你随手抓出两个球,颜色不一样的可能性越大,说明这袋球越“混乱”(基尼值高)。我们的目标是找到一种分法(特征),让每次分出来的每一小袋都尽可能接近“同色”,也就是让每袋的基尼指数尽可能小。
4.3 CART决策树案例演示:贷款申请的基尼指数计算
沿用前例,计算特征"有房子"的基尼指数(CART为二叉树,需将多取值特征转化为二叉分裂)。
步骤1:计算数据集D的基尼值
Gini
(
D
)
=
1
−
(
5
15
)
2
−
(
10
15
)
2
=
0.4444
\text{Gini}(D) = 1 - \left(\frac{5}{15}\right)^2 - \left(\frac{10}{15}\right)^2 = 0.4444
Gini(D)=1−(155)2−(1510)2=0.4444
步骤2:计算"有房子"特征的基尼指数
- 有房子:7条样本,6同意,1拒绝
Gini ( 有房子 ) = 1 − ( 6 7 ) 2 − ( 1 7 ) 2 = 0.2449 \text{Gini}(有房子) = 1 - \left(\frac{6}{7}\right)^2 - \left(\frac{1}{7}\right)^2 = 0.2449 Gini(有房子)=1−(76)2−(71)2=0.2449 - 无房子:8条样本,2同意,6拒绝
Gini ( 无房子 ) = 1 − ( 2 8 ) 2 − ( 6 8 ) 2 = 0.375 \text{Gini}(无房子) = 1 - \left(\frac{2}{8}\right)^2 - \left(\frac{6}{8}\right)^2 = 0.375 Gini(无房子)=1−(82)2−(86)2=0.375 - 基尼指数:
Gini_index ( D , 有房子 ) = 7 15 × 0.2449 + 8 15 × 0.375 = 0.3176 \text{Gini\_index}(D, 有房子) = \frac{7}{15} \times 0.2449 + \frac{8}{15} \times 0.375 = 0.3176 Gini_index(D,有房子)=157×0.2449+158×0.375=0.3176
步骤3:计算其他特征的基尼指数
以"年龄"特征为例,需转化为二叉分裂(如"青年"vs"非青年"):
- 青年:5样本,4拒绝,1同意
Gini ( 青年 ) = 1 − ( 4 5 ) 2 − ( 1 5 ) 2 = 0.32 \text{Gini}(青年) = 1 - \left(\frac{4}{5}\right)^2 - \left(\frac{1}{5}\right)^2 = 0.32 Gini(青年)=1−(54)2−(51)2=0.32 - 非青年:10样本,6拒绝,4同意
Gini ( 非青年 ) = 1 − ( 6 10 ) 2 − ( 4 10 ) 2 = 0.48 \text{Gini}(非青年) = 1 - \left(\frac{6}{10}\right)^2 - \left(\frac{4}{10}\right)^2 = 0.48 Gini(非青年)=1−(106)2−(104)2=0.48 - 基尼指数:
Gini_index ( D , 年龄 ) = 5 15 × 0.32 + 10 15 × 0.48 = 0.4267 \text{Gini\_index}(D, 年龄) = \frac{5}{15} \times 0.32 + \frac{10}{15} \times 0.48 = 0.4267 Gini_index(D,年龄)=155×0.32+1510×0.48=0.4267
步骤4:选择基尼指数最小的特征
"有房子"的基尼指数0.3176最小,作为分裂特征,生成二叉树:
- 左子树:有房子,基尼值0.2449(更纯)
- 右子树:无房子,基尼值0.375(较不纯)
步骤5:递归构建子树
在完成当前节点的最佳划分特征选择后(如“有房子”),CART 决策树会基于该特征的不同取值,将数据集划分为两个子集,并对每个子集递归地重复以下过程:
- 判断是否满足终止条件:
- 若当前子集中所有样本属于同一类别(基尼值为 0),则将其设为叶子节点,直接返回类别结果。
- 若达到预设的最大深度(如 max_depth=3)或样本数过少(如小于 min_samples_split),也停止继续分裂。
- 否则,继续进行最优划分特征的选择:
- 对当前子集重新计算各个特征的基尼指数。
- 选择基尼指数最小的特征作为新的划分依据。
- 再次划分数据集并生成新的左右子节点:
- 每个子节点代表一个划分后的子集。
- 对每个子节点递归执行上述步骤,直到满足终止条件为止。
- 最终形成一棵完整的二叉决策树:
绘图代码:
# 导入必要的库
import pandas as pd # 用于构建 DataFrame 数据结构
from sklearn.tree import DecisionTreeClassifier, plot_tree # 构建和可视化决策树
from sklearn.preprocessing import LabelEncoder # 对字符串特征进行编码
import matplotlib.pyplot as plt # 可视化工具
# 设置中文字体显示(解决中文乱码问题)
plt.rcParams['font.sans-serif'] = ['SimHei'] # 将默认字体设置为"SimHei(黑体)",确保中文标签正常显示
plt.rcParams['axes.unicode_minus'] = False # 修复负号显示为方块的问题(确保特殊符号渲染正常)
# =============================================
# 1. 数据准备(含 ID)
# =============================================
# 构建训练数据集,包含 5 个特征和 1 个目标变量
data = {
# 每个样本的唯一编号,容易造成过拟合
"ID": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
# 年龄:青年、中年、老年
"年龄": ["青年", "青年", "青年", "青年", "青年", "中年", "中年", "中年", "中年", "中年",
"老年", "老年", "老年", "老年", "老年"],
# 是否有工作:是、否
"有工作": ["否", "否", "是", "是", "否", "否", "否", "是", "否", "否",
"否", "否", "是", "是", "否"],
# 是否有房子:是、否
"有房子": ["否", "否", "否", "是", "否", "否", "否", "是", "是", "是",
"是", "是", "否", "否", "否"],
# 信贷情况:一般、好、非常好
"信贷情况": ["一般", "好", "好", "一般", "一般", "一般", "好", "好", "非常好", "非常好",
"非常好", "好", "好", "非常好", "一般"],
# 类别:是否贷款成功(同意/拒绝)
"类别": ["拒绝", "拒绝", "同意", "同意", "拒绝", "拒绝", "拒绝", "同意", "同意", "同意",
"同意", "同意", "同意", "同意", "拒绝"]
}
# 将数据转换为 pandas 的 DataFrame 格式
df = pd.DataFrame(data)
# 提取特征和目标变量
X = df.drop("类别", axis=1) # 特征:年龄、有工作、有房子、信贷情况
y = df["类别"] # 目标变量:类别(是否贷款成功)
# =============================================
# 2. 特征编码(将字符串特征转为数字)
# =============================================
# 创建 LabelEncoder 并对每个特征列进行编码
encoders = {} # 存储每个特征的编码器
for col in X.columns:
le = LabelEncoder()
X[col] = le.fit_transform(X[col]) # 转换为数字
encoders[col] = le # 保存编码器供后续反编码用
# 对目标变量进行编码:'拒绝' -> 0, '同意' -> 1
y = LabelEncoder().fit_transform(y)
# =============================================
# 3. 构建 CART 决策树模型
# =============================================
# 创建 CART 分类器,使用默认的 gini 指数作为划分标准
clf = DecisionTreeClassifier(criterion='gini', max_depth=3)
# 训练模型
clf.fit(X, y)
# =============================================
# 4. 使用 matplotlib 可视化决策树结构
# =============================================
# 设置绘图风格
plt.figure(figsize=(10, 6)) # 设置画布大小
# 绘制决策树结构图
plot_tree(
clf, # 决策树模型
feature_names=X.columns, # 特征名称
class_names=["拒绝", "同意"], # 类别名称(中文支持)
filled=True, # 使用颜色填充节点
fontsize=10, # 字体大小
proportion=False, # 显示样本数量而非比例
rounded=True # 圆角矩形显示节点
)
# 设置图像标题并调整布局
plt.title("CART 决策树结构(基于基尼指数)")
plt.tight_layout() # 自动调整子图参数,防止重叠
# 显示图像
plt.show()
4.4 Scikit-learn 中 CART 决策树的 API
- 分类树:DecisionTreeClassifier
用于离散标签分类(如是否贷款),基于基尼指数或信息熵生成二叉树。
- 核心参数:
criterion
控制分裂标准(gini
为默认基尼指数,entropy
为信息熵);
max_depth
限制树深度(避免过拟合,如设为3-10);
min_samples_split
规定节点分裂的最小样本数(默认2);
min_samples_leaf
设定叶子节点最小样本数(默认1)。 - 关键方法:
fit(X, y)
训练模型(X为特征矩阵,y为离散标签);
predict(X)
预测类别;
predict_proba(X)
输出类别概率;
score(X, y)
计算准确率。
- 回归树:DecisionTreeRegressor
用于连续值预测(如房价),基于均方误差或平均绝对误差分裂。
- 核心参数:
criterion
可选mse
(均方误差,默认)或mae
(平均绝对误差);
其他参数(如max_depth
、min_samples_split
)与分类树逻辑一致。 - 关键方法:
fit(X, y)
训练模型(y为连续值);
predict(X)
输出回归值;
score(X, y)
计算R²系数(越接近1拟合越好)。
- 核心共通点
- 分裂策略:
splitter='best'
(全局最优)或'random'
(随机分裂防过拟合); - 特征重要性:训练后通过
feature_importances_
属性获取; - 可视化:用
sklearn.tree.export_graphviz
或plot_tree
展示树结构。
- 分类与回归差异
分类树用基尼/熵衡量纯度,输出类别概率;回归树用MSE/MAE衡量误差,输出连续值预测,评估指标为R²而非准确率。
4.5 小结
CART决策树的核心优势在于:
- 始终生成二叉树,结构更简单
- 基尼指数计算效率高于熵运算
- 同时支持分类和回归任务(回归时使用平方误差最小化)
- 对噪声数据和缺失值的处理更鲁棒
五、三大决策树模型对比总结
模型 | 核心指标 | 节点分裂方式 | 特征处理 | 树结构 | 优缺点 |
---|---|---|---|---|---|
ID3 | 信息增益 | 多叉分裂 | 仅支持离散特征 | 多叉树 | 优点:计算简单;缺点:偏向取值多的特征,易过拟合 |
C4.5 | 信息增益率 | 多叉分裂 | 支持离散/连续特征 | 多叉树 | 优点:修正ID3的偏向问题;缺点:计算复杂,对取值少的特征仍有偏向 |
CART | 基尼指数 | 二叉分裂 | 支持离散/连续特征 | 二叉树 | 优点:计算高效,泛化能力强,支持分类/回归;缺点:对高维数据表现不佳 |
决策树的发展历程体现了机器学习算法的优化思路——从简单有效到复杂精准,再到平衡效率与性能。实际应用中,可根据数据特点和任务需求选择合适的决策树模型,或采用集成学习(如随机森林)进一步提升模型效果。
六、其他重要决策树算法
6.1 CHAID:基于统计检验的决策树
核心思想:使用卡方检验(分类目标)或F检验(连续目标)评估特征与目标的相关性
公式示例:
χ
2
=
∑
(
O
i
−
E
i
)
2
E
i
\chi^2 = \sum \frac{(O_i - E_i)^2}{E_i}
χ2=∑Ei(Oi−Ei)2
其中
O
i
O_i
Oi为观察频数,
E
i
E_i
Ei为期望频数
优势:
- 支持多叉分裂,直观展示特征交互作用
- 自动处理缺失值
- 可生成非二叉树结构
应用场景:市场细分、客户画像分析
6.2 QUEST:快速无偏决策树
核心思想:通过统计检验选择分裂特征,避免CART对连续特征的偏向
算法流程:
- 使用ANOVA F-test选择最佳分裂特征
- 应用二次判别分析确定最优分裂点
- 递归构建二叉树
优势:
- 无偏特征选择
- 计算效率高
- 处理高维数据能力强
应用场景:基因表达数据分析、高维生物信息学
6.3 条件推断树:统计驱动的决策树
核心思想:基于置换检验(Permutation Test)选择分裂变量和分裂点
算法特点:
- 计算特征与目标的统计相关性
- 评估分裂显著性的p值
- 仅当p值小于阈值时才进行分裂
公式核心:
p
-value
=
P
(
∣
T
∣
≥
∣
t
∣
∣
H
0
)
p\text{-value} = P(|T| \geq |t| | H_0)
p-value=P(∣T∣≥∣t∣∣H0)
其中
T
T
T为检验统计量,
t
t
t为观察值,
H
0
H_0
H0为无关联假设
优势:
- 有效控制变量选择偏差
- 适用于混合类型特征(离散+连续)
- 提供统计显著性评估
应用场景:临床试验数据分析、社会科学研究
6.4 斜决策树(Oblique Decision Tree)
核心思想:允许使用特征的线性组合进行分裂(如 0.5 × 年龄 + 0.8 × 收入 > 50 0.5 \times \text{年龄} + 0.8 \times \text{收入} > 50 0.5×年龄+0.8×收入>50)
数学表示:
∑
j
=
1
p
w
j
x
j
>
θ
\sum_{j=1}^{p} w_j x_j > \theta
j=1∑pwjxj>θ
其中
w
j
w_j
wj为特征权重,
θ
\theta
θ为阈值
优势:
- 构建更紧凑的树结构
- 提升模型泛化能力
- 更好处理相关特征
挑战:
- 分裂点优化复杂度高
- 需结合线性规划求解
- 训练时间显著增加
应用场景:图像识别、金融风险评估
6.5 流数据决策树(Hoeffding Tree)
核心思想:基于Hoeffding不等式,在有限内存下处理无限数据流
核心公式:
n
>
R
2
ln
(
1
/
δ
)
2
ϵ
2
n > \frac{R^2 \ln(1/\delta)}{2\epsilon^2}
n>2ϵ2R2ln(1/δ)
其中
n
n
n为所需样本数,
R
R
R为随机变量范围,
δ
\delta
δ为置信度,
ϵ
\epsilon
ϵ为误差界
优势:
- 单次遍历数据
- 实时更新模型
- 有限内存需求
应用场景:实时交易监控、物联网设备数据分析