决策树实战:用Python手写ID3算法(附完整代码与数据集)

决策树作为机器学习中最直观的算法之一,其核心思想就像我们日常做决策时的思考过程——通过一系列的判断条件逐步缩小范围,最终得出结论。今天我们将从零开始实现经典的ID3决策树算法,通过代码深入理解信息增益的计算、递归构建树结构等关键环节。

1. 环境准备与数据加载

在开始之前,我们需要准备Python环境和演示数据集。这里我们使用一个经典的"贷款审批"数据集,包含年龄、收入、信用等级等特征,以及最终的审批结果。

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

# 创建演示数据集
data = {
    '年龄': ['青年', '青年', '青年', '青年', '青年', '中年', '中年', '中年', '中年', '中年', '老年', '老年', '老年', '老年', '老年'],
    '有工作': ['否', '否', '是', '是', '否', '否', '否', '是', '否', '否', '否', '否', '是', '是', '否'],
    '有房子': ['否', '否', '否', '是', '否', '否', '否', '是', '是', '是', '是', '是', '否', '否', '否'],
    '信用': ['一般', '好', '好', '一般', '一般', '一般', '好', '好', '非常好', '非常好', '非常好', '好', '好', '非常好', '一般'],
    '类别': ['否', '否', '是', '是', '否', '否', '否', '是', '是', '是', '是', '是', '是', '是', '否']
}

df = pd.DataFrame(data)
print("数据集预览:")
print(df.head())

这个数据集包含了15条记录,每条记录有4个特征和1个类别标签。我们的目标是构建一个决策树模型,能够根据这些特征预测贷款是否会被批准。

2. 核心概念:信息熵与信息增益

ID3算法的核心是信息增益,而要理解信息增益,首先需要掌握信息熵的概念。

信息熵是度量数据集不确定性的指标,计算公式为:

def entropy(data):
    """计算数据集的信息熵"""
    labels = data.iloc[:, -1]
    total = len(labels)
    counts = labels.value_counts()
    ent = 0.0
    for count in counts:
        prob = count / total
        ent -= prob * log2(prob)
    return ent

信息增益则表示使用某个特征划分数据集后,不确定性减少的程度:

def info_gain(data, feature):
    """计算指定特征的信息增益"""
    base_entropy = entropy(data)
    feature_values = data[feature].unique()
    weighted_entropy = 0.0
    
    for value in feature_values:
        subset = data[data[feature] == value]
        weight = len(subset) / len(data)
        weighted_entropy += weight * entropy(subset)
    
    return base_entropy - weighted_entropy

让我们计算各特征的信息增益:

features = df.columns[:-1]
for feature in features:
    print(f"{feature}的信息增益: {info_gain(df, feature):.4f}")

输出结果将显示哪个特征对分类最有帮助,这正是ID3算法选择划分特征的依据。

3. 递归构建决策树

有了信息增益的计算方法,我们就可以递归地构建决策树了。决策树的每个节点将存储以下信息:

  • 如果是叶节点:存储类别标签
  • 如果是内部节点:存储划分特征和子节点
class DecisionNode:
    """决策树节点类"""
    def __init__(self, feature=None, children=None, label=None):
        self.feature = feature   # 划分特征
        self.children = children or {}  # 子节点字典 {特征值: 子节点}
        self.label = label       # 叶节点的类别标签

def build_tree(data, features):
    """递归构建决策树"""
    # 如果所有样本属于同一类别,返回叶节点
    if len(data['类别'].unique()) == 1:
        return DecisionNode(label=data['类别'].iloc[0])
    
    # 如果没有特征可用,返回样本数最多的类别
    if not features:
        majority_label = data['类别'].mode()[0]
        return DecisionNode(label=majority_label)
    
    # 选择最佳划分特征
    best_feature = max(features, key=lambda f: info_gain(data, f))
    
    # 递归构建子树
    children = {}
    for value in data[best_feature].unique():
        subset = data[data[best_feature] == value]
        if len(subset) == 0:
            majority_label = data['类别'].mode()[0]
            children[value] = DecisionNode(label=majority_label)
        else:
            remaining_features = [f for f in features if f != best_feature]
            children[value] = build_tree(subset, remaining_features)
    
    return DecisionNode(feature=best_feature, children=children)

构建决策树并打印结构:

tree = build_tree(df, list(features))

def print_tree(node, indent=""):
    """打印决策树结构"""
    if node.label is not None:
        print(indent + "类别:", node.label)
    else:
        print(indent + "特征:", node.feature)
        for value, child in node.children.items():
            print(indent + f"  {value}:")
            print_tree(child, indent + "    ")

print("决策树结构:")
print_tree(tree)

4. 决策树的可视化与预测

为了更直观地理解决策树,我们可以使用graphviz库进行可视化:

from graphviz import Digraph

def visualize_tree(tree):
    """可视化决策树"""
    dot = Digraph()
    node_id = 0
    
    def add_nodes(node, parent_id=None, edge_label=None):
        nonlocal node_id
        current_id = str(node_id)
        node_id += 1
        
        if node.label is not None:
            dot.node(current_id, label=f"类别: {node.label}", shape='box')
        else:
            dot.node(current_id, label=f"特征: {node.feature}")
            
        if parent_id is not None:
            dot.edge(parent_id, current_id, label=edge_label)
            
        if node.children:
            for value, child in node.children.items():
                add_nodes(child, current_id, str(value))
    
    add_nodes(tree)
    return dot

# 显示决策树
visualize_tree(tree)

有了构建好的决策树,我们就可以用它来进行预测了:

def predict(tree, sample):
    """使用决策树进行预测"""
    if tree.label is not None:
        return tree.label
    
    feature_value = sample[tree.feature]
    if feature_value not in tree.children:
        return None  # 未知特征值
    
    return predict(tree.children[feature_value], sample)

# 测试预测
test_sample = {'年龄': '中年', '有工作': '是', '有房子': '否', '信用': '好'}
print("预测结果:", predict(tree, test_sample))

5. 算法优化与扩展

虽然我们实现了基本的ID3算法,但在实际应用中还需要考虑以下优化:

  1. 连续值处理:ID3只能处理离散特征,可以通过二分法将连续特征离散化
  2. 缺失值处理:可以采用概率分配或最常见值填充等方法
  3. 剪枝策略:预剪枝和后剪枝可以防止过拟合
  4. 其他划分准则:如C4.5的信息增益率、CART的基尼指数

这里我们实现一个简单的预剪枝版本:

def build_tree_with_pruning(data, features, max_depth=3, min_samples=2):
    """带预剪枝的决策树构建"""
    if len(data['类别'].unique()) == 1:
        return DecisionNode(label=data['类别'].iloc[0])
    
    if not features or len(data) < min_samples or max_depth <= 0:
        majority_label = data['类别'].mode()[0]
        return DecisionNode(label=majority_label)
    
    best_feature = max(features, key=lambda f: info_gain(data, f))
    
    children = {}
    for value in data[best_feature].unique():
        subset = data[data[best_feature] == value]
        remaining_features = [f for f in features if f != best_feature]
        children[value] = build_tree_with_pruning(
            subset, remaining_features, max_depth-1, min_samples)
    
    return DecisionNode(feature=best_feature, children=children)

# 构建带预剪枝的决策树
pruned_tree = build_tree_with_pruning(df, list(features), max_depth=2)
print("剪枝后的决策树:")
print_tree(pruned_tree)

决策树虽然直观易懂,但也有其局限性。在实际项目中,我们通常会使用随机森林或梯度提升树等集成方法,它们基于决策树但能提供更好的泛化性能。

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐