一、决策树基础认知

决策树是一种直观易懂的分类算法,核心是通过对特征的逐步划分,将数据集拆解为纯度越来越高的子集,最终形成可用于预测的树形结构。

  • 核心思想:类似生活中的 “if-else” 判断逻辑,每个内部节点代表一个特征判断,每个分支代表判断结果,每个叶节点代表最终分类。
  • 关键问题:如何选择最优特征进行划分?ID3 和 C4.5 的核心区别就在特征选择准则不同。
  • 实战数据:本文使用贷款申请数据集,包含 16 条样本,特征包括 “年龄段”“有工作”“有自己的房子”“信贷情况”,目标是预测 “是否给贷款”。

二、ID3 与 C4.5 核心原理

2.1 ID3 算法:基于信息增益

  • 核心准则:选择信息增益最大的特征作为当前划分节点。
  • 信息熵:衡量数据集的混乱程度,熵值越小,数据纯度越高。公式为:Ent(D)=−∑k=1K​pk​log2​pk​(pk​是第 k 类样本占比)。
  • 信息增益:划分后数据集熵值的减少量,公式为:Gain(D,a)=Ent(D)−∑v=1V​∣D∣∣Dv​∣​Ent(Dv​)(a 是特征,Dv​是特征 a 取第 v 个值的子集)。
  • 优缺点:简单易实现,但倾向于选择取值多的特征,对噪声敏感。

2.2 C4.5 算法:基于信息增益比

  • 核心准则:解决 ID3 的偏向性问题,选择信息增益比最大的特征。
  • 信息增益比:公式为:Gain_ratio(D,a)=Gain(D,a)/​,其中IV(a)=−∑v=1V(​∣Dv​∣​/∣D∣)log2​(∣Dv​∣​/∣D∣),(IV (a) 是特征 a 的固有值,取值越多则 IV (a) 越大)。
  • 优缺点:平衡了特征取值数量的影响,泛化能力更强,是 ID3 的改进版。

三、Python 实战实现(初学者友好)

3.1 环境准备

无需复杂依赖,仅用 Python 内置库:

import numpy as np
import pandas as pd
from math import log2

3.2 数据预处理

先加载贷款数据集,将文字特征转换为机器可识别的数值(也可直接用文字特征计算):

# 构建数据集(对应文档中数据表)
data = {
    '年龄段': [0,0,0,0,0,1,1,1,1,1,2,2,2,2,2,2],  # 0=青年,1=中年,2=老年
    '有工作': [0,0,1,1,0,0,0,1,0,0,0,0,1,1,0,0],  # 0=否,1=是
    '有自己的房子': [0,0,0,1,0,0,0,1,1,1,1,1,0,0,0,0],  # 0=否,1=是
    '信贷情况': [0,1,1,0,0,0,1,1,2,2,2,1,1,2,0,2],  # 0=一般,1=好,2=非常好
    '是否给贷款': [0,0,1,1,0,0,0,1,1,1,1,1,1,1,0,0]  # 0=否,1=是
}
df = pd.DataFrame(data)
X = df.iloc[:, :-1]  # 特征
y = df.iloc[:, -1]   # 标签

3.3 核心函数实现

(1)计算信息熵
def calc_entropy(y):
    """计算信息熵"""
    count = y.value_counts()  # 统计每个类别出现次数
    entropy = 0.0
    total = len(y)
    for num in count:
        p = num / total
        entropy -= p * log2(p)  # 熵的公式
    return entropy
(2)ID3 特征选择(信息增益)
def select_feature_id3(X, y):
    """ID3:选择信息增益最大的特征"""
    base_entropy = calc_entropy(y)
    max_gain = 0.0
    best_feature = -1
    # 遍历所有特征
    for i in range(X.shape[1]):
        feature_values = X.iloc[:, i].unique()  # 该特征的所有取值
        new_entropy = 0.0
        # 计算该特征每个取值的条件熵
        for val in feature_values:
            subset_y = y[X.iloc[:, i] == val]
            weight = len(subset_y) / len(y)
            new_entropy += weight * calc_entropy(subset_y)
        # 计算信息增益
        gain = base_entropy - new_entropy
        # 更新最大增益和最优特征
        if gain > max_gain:
            max_gain = gain
            best_feature = i
    return best_feature, max_gain
(3)C4.5 特征选择(信息增益比)
def calc_iv(X, feature_idx):
    """计算特征的固有值IV"""
    feature_values = X.iloc[:, feature_idx].unique()
    iv = 0.0
    total = len(X)
    for val in feature_values:
        subset_size = len(X[X.iloc[:, feature_idx] == val])
        p = subset_size / total
        iv -= p * log2(p)
    return iv

def select_feature_c45(X, y):
    """C4.5:选择信息增益比最大的特征"""
    base_entropy = calc_entropy(y)
    max_gain_ratio = 0.0
    best_feature = -1
    for i in range(X.shape[1]):
        # 先计算信息增益
        feature_values = X.iloc[:, i].unique()
        new_entropy = 0.0
        for val in feature_values:
            subset_y = y[X.iloc[:, i] == val]
            weight = len(subset_y) / len(y)
            new_entropy += weight * calc_entropy(subset_y)
        gain = base_entropy - new_entropy
        # 计算信息增益比(避免IV为0的情况)
        iv = calc_iv(X, i)
        gain_ratio = gain / iv if iv != 0 else 0
        # 更新最优特征
        if gain_ratio > max_gain_ratio:
            max_gain_ratio = gain_ratio
            best_feature = i
    return best_feature, max_gain_ratio
(4)构建决策树(递归实现)
def majority_vote(y):
    """当所有特征都用完时,返回出现次数最多的类别(投票机制)"""
    return y.value_counts().idxmax()

def build_tree(X, y, feature_names, method='ID3'):
    """递归构建决策树"""
    # 1. 若所有样本属于同一类别,返回该类别
    if len(y.unique()) == 1:
        return y.iloc[0]
    # 2. 若没有特征可划分,返回投票结果
    if X.empty:
        return majority_vote(y)
    # 3. 选择最优特征
    if method == 'ID3':
        best_idx, _ = select_feature_id3(X, y)
    else:  # C4.5
        best_idx, _ = select_feature_c45(X, y)
    best_feature = feature_names[best_idx]
    # 4. 构建树节点
    tree = {best_feature: {}}
    # 5. 移除已使用的特征
    new_feature_names = feature_names.copy()
    new_feature_names.pop(best_idx)
    # 6. 遍历最优特征的所有取值,递归构建子树
    feature_values = X.iloc[:, best_idx].unique()
    for val in feature_values:
        subset_X = X[X.iloc[:, best_idx] == val].drop(X.columns[best_idx], axis=1)
        subset_y = y[X.iloc[:, best_idx] == val]
        tree[best_feature][val] = build_tree(subset_X, subset_y, new_feature_names, method)
    return tree

3.4 训练与测试

# 特征名称(用于树的可视化)
feature_names = ['年龄段', '有工作', '有自己的房子', '信贷情况']

# 1. 训练ID3决策树
id3_tree = build_tree(X, y, feature_names, method='ID3')
print("ID3决策树:")
print(id3_tree)

# 2. 训练C4.5决策树
c45_tree = build_tree(X, y, feature_names, method='C4.5')
print("\nC4.5决策树:")
print(c45_tree)

# 3. 简单预测函数
def predict(tree, feature_names, sample):
    """预测单个样本"""
    root = list(tree.keys())[0]
    root_idx = feature_names.index(root)
    val = sample[root_idx]
    subtree = tree[root][val]
    if isinstance(subtree, dict):
        return predict(subtree, feature_names, sample)
    else:
        return subtree

# 测试样本(来自testset.txt,转换为[年龄段,有工作,有自己的房子,信贷情况])
test_samples = [
    [0,0,0,1], [0,1,0,1], [1,0,1,2],
    [1,0,0,1], [2,1,0,2], [2,0,0,0], [2,0,0,2]
]

print("\n预测结果:")
for sample in test_samples:
    id3_pred = predict(id3_tree, feature_names, sample)
    c45_pred = predict(c45_tree, feature_names, sample)
    print(f"样本{sample}:ID3预测{id3_pred},C4.5预测{c45_pred}")

四、结果解读与可视化

4.1 输出结果说明

  • 训练后会得到嵌套字典形式的决策树,例如 ID3 可能先以 “有自己的房子” 为根节点(信息增益最大),再依次划分其他特征。
  • 测试集预测结果中,ID3 和 C4.5 可能因特征选择顺序不同出现细微差异,但整体分类效果相近。

五、常见问题与总结

5.1 常见坑点

  1. 特征取值为 0 时的处理:计算信息增益比时需避免 IV=0(本文已处理)。
  2. 递归终止条件:必须判断 “所有样本同类别” 或 “无特征可划分”,否则会无限递归。
  3. 数据格式:确保 X 和 y 是 DataFrame/Series,避免数组索引混乱。

5.2 算法对比总结

算法特征选择准则优点缺点
ID3信息增益简单易实现、计算快偏向取值多的特征
C4.5信息增益比平衡特征取值影响计算量略大

5.3 后续优化方向

  1. 剪枝处理:解决过拟合(预剪枝 / 后剪枝)。
  2. 连续值处理:C4.5 支持连续特征离散化,可扩展实现。
  3. 可视化工具:使用matplotlibgraphviz绘制更专业的树图。
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐