从‘读心术’到决策树:用Pandas和NumPy复现ID3算法,实战筛选最佳特征
从‘读心术’到决策树:用Pandas和NumPy复现ID3算法,实战筛选最佳特征
想象一下你在玩一个猜谜游戏:朋友心中默想一个动物,你只能通过是非题来缩小范围。"是哺乳动物吗?""生活在水中吗?"每个问题的价值其实不同——有些问题能瞬间排除大量选项,有些则收效甚微。这种"问题价值量化"的直觉,正是决策树算法中信息增益的核心思想。本文将带您用Python基础工具包,从零实现经典的ID3决策树算法,体验机器如何像人类一样通过智能提问完成分类任务。
1. 信息论基础:决策树的数学语言
1.1 信息熵:不确定性的度量标准
1948年,香农将热力学中的熵概念引入信息领域,用数学量化了信息的不确定性。在决策树中,熵衡量了数据集的"混乱程度":
import numpy as np def entropy(labels): """计算信息熵""" _, counts = np.unique(labels, return_counts=True) probabilities = counts / len(labels) return -np.sum(probabilities * np.log2(probabilities))示例:假设一个袋子有3红球和1蓝球:
print(entropy(['红','红','红','蓝'])) # 输出0.811当概率分布越均匀(如2红2蓝时熵为1),熵值越大;当全部为同一类别(如4红球)时,熵降为0。
1.2 条件熵与信息增益
条件熵表示已知某个特征取值后的剩余不确定性,而信息增益则是原始熵与条件熵的差值:
| 概念 | 数学表达 | 现实类比 |
|---|---|---|
| 信息熵H(D) | -Σp(x)log₂p(x) | 初始猜测的难度 |
| 条件熵H(D | A) | Σp(a)H(D |
| 信息增益IG | H(D) - H(D | A) |
用Python实现信息增益计算:
def information_gain(data, feature, target): """计算特征的信息增益""" total_entropy = entropy(data[target]) # 计算加权条件熵 values, counts = np.unique(data[feature], return_counts=True) weighted_entropy = 0 for v, c in zip(values, counts): subset = data[data[feature] == v][target] weighted_entropy += (c/len(data)) * entropy(subset) return total_entropy - weighted_entropy2. ID3算法实现:从理论到代码
2.1 算法核心流程
ID3算法的本质是一个贪心搜索过程:
- 计算当前数据集所有特征的信息增益
- 选择增益最大的特征作为划分节点
- 对每个特征值创建分支,递归执行上述过程
递归终止条件:
- 所有样本属于同一类别
- 没有剩余特征可供划分
- 分支样本数低于阈值
2.2 完整Python实现
使用Pandas处理数据,构建可解释的决策树结构:
import pandas as pd class ID3DecisionTree: def __init__(self, max_depth=None): self.tree = {} self.max_depth = max_depth def fit(self, X, y, depth=0): # 终止条件检查 if len(np.unique(y)) == 1: return y.iloc[0] if len(X.columns) == 0 or (self.max_depth and depth >= self.max_depth): return y.mode()[0] # 选择最佳分裂特征 gains = {col: information_gain(pd.concat([X,y], axis=1), col, y.name) for col in X.columns} best_feature = max(gains, key=gains.get) # 构建子树 tree = {best_feature: {}} for value in X[best_feature].unique(): subset_X = X[X[best_feature] == value].drop(best_feature, axis=1) subset_y = y[X[best_feature] == value] tree[best_feature][value] = self.fit(subset_X, subset_y, depth+1) return tree2.3 可视化决策过程
通过DataFrame展示特征选择过程(以鸢尾花数据集为例):
| 特征 | 信息增益 | 分裂优先级 |
|---|---|---|
| 花瓣长度 | 0.918 | 1 |
| 花瓣宽度 | 0.862 | 2 |
| 萼片长度 | 0.311 | 3 |
| 萼片宽度 | 0.156 | 4 |
这个结果直观显示为什么决策树通常会优先使用花瓣特征进行分类。
3. 实战对比:手动实现 vs Scikit-learn
3.1 泰坦尼克号生存预测
加载并预处理数据:
titanic = pd.read_csv('titanic.csv') features = ['Pclass', 'Sex', 'Age', 'SibSp', 'Parch'] X = titanic[features].fillna({'Age': titanic['Age'].median()}) X['Sex'] = X['Sex'].map({'male':0, 'female':1}) y = titanic['Survived']3.2 手动ID3实现结果
manual_tree = ID3DecisionTree(max_depth=3) manual_tree.fit(X, y)典型输出结构:
{ "Sex": { 0: { "Pclass": { 1: {"Age": {...}}, 2: 0.17, # 生存率 3: 0.11 } }, 1: { "Pclass": { 1: 0.96, 2: 0.92, 3: 0.50 } } } }3.3 与Scikit-learn对比
from sklearn.tree import DecisionTreeClassifier sklearn_tree = DecisionTreeClassifier(criterion='entropy', max_depth=3) sklearn_tree.fit(X, y)对比发现:
- 相同点:都优先选择性别作为首要分裂特征
- 差异点:
- Scikit-learn使用CART算法,支持连续值处理
- 手动ID3只能处理离散特征
- Scikit-learn实现了剪枝等优化
4. 进阶讨论:ID3的局限与改进
4.1 算法局限性
- 无法处理连续特征:需要预先离散化
- 偏向多值特征:倾向于选择取值多的特征
- 无剪枝机制:容易过拟合
4.2 改进方案对比
| 算法 | 改进点 | 适用场景 |
|---|---|---|
| C4.5 | 引入信息增益率 | 特征取值差异大的数据 |
| CART | 支持回归任务 | 需要统一框架的场景 |
| 随机森林 | 集成多棵树+特征随机选择 | 高维数据 |
4.3 处理连续特征的技巧
通过二分法寻找最佳分割点:
def find_best_split(series, target): unique_values = np.sort(series.unique()) gains = [] for threshold in unique_values[1:-1]: left = target[series <= threshold] right = target[series > threshold] gain = entropy(target) - (len(left)/len(target)*entropy(left) + len(right)/len(target)*entropy(right)) gains.append(gain) return unique_values[np.argmax(gains) + 1]在真实项目中,手动实现决策树更多是教育意义。但理解这些底层原理,能帮助我们在使用现成库时做出更明智的参数选择,比如:
- 当特征取值很多时,改用
criterion='gini'可能更好 - 对于类别不平衡数据,调整
class_weight参数 - 监控
max_depth防止过拟合
