决策树模型-数学建模优秀论文算法

决策树模型:从原理到实践

一、背景溯源:决策树的发展脉络

决策树的思想源于人类对复杂问题的分步决策逻辑,其发展经历了从理论萌芽到工程化落地的三次关键突破:

1.1 早期萌芽:概念学习系统(CLS)

1966年,心理学家Hunt等人提出CLS(Concept Learning System),首次将"特征-决策"的分步过程抽象为树结构------根节点是初始问题,内部节点是特征判断,叶节点是决策结果。CLS是决策树的雏形,但未解决"如何选择最优特征"的核心问题。

1.2 经典演化:ID3与C4.5的诞生

1979年,Quinlan提出ID3算法 (Iterative Dichotomiser 3),首次引入信息增益 作为特征选择准则,解决了"选哪个特征分裂节点"的问题。ID3仅支持离散特征和分类任务,无法处理连续特征或缺失值。1993年,Quinlan改进ID3得到C4.5算法

  • 支持连续特征(通过离散化分割点);
  • 信息增益比修正信息增益对"取值多的特征"的偏向;
  • 提出缺失值处理方法(概率加权分配样本)。

1.3 现代延伸:CART树的通用化

1984年,Breiman等人提出CART树(Classification and Regression Tree) ,将决策树扩展到分类+回归双任务:

  • 分类任务用基尼指数(Gini Index)替代熵,计算更高效;
  • 回归任务用平方误差最小化作为分裂准则;
  • 生成二叉树(每个节点仅分裂为两个子节点),简化树结构;
  • 系统提出剪枝策略(预剪枝+后剪枝),解决过拟合问题。

至此,决策树的核心框架(特征选择、树生长、剪枝)完全成型,成为机器学习中"可解释性"与"实用性"平衡最好的基础模型之一。

二、核心思想:模拟人类决策的树状逻辑

2.1 决策树的本质:特征空间的递归分割

决策树的核心是将高维特征空间划分为互不重叠的子区域,每个子区域对应一个决策结果(叶节点)。例如,判断"水果是否为苹果"的决策树:

  • 根节点:"颜色是否为红色?"(特征1);
  • 左分支(红色):"形状是否为圆形?"(特征2);
  • 右分支(非红色):直接判定"非苹果"(叶节点);
  • 左分支的子节点(圆形):判定"苹果"(叶节点)。

这个过程本质是用特征逐步缩小样本的"不确定性",直到每个子区域的样本足够"纯"(即类别一致或输出稳定)。

2.2 树结构的基本要素

决策树由三类节点组成:

  1. 根节点:树的起点,包含全部样本;
  2. 内部节点:表示一个特征判断(如"颜色=红色?");
  3. 叶节点:表示最终决策结果(分类任务为"多数类",回归任务为"均值");
  4. 分支:内部节点的输出,对应特征的一个取值(如"是/否""≤50/>50")。

三、核心理论:不确定性度量与特征选择准则

3.1 基础概念:熵、条件熵与不确定性度量

3.1.1 香农熵(Shannon Entropy):不确定性的量化

香农熵是信息论中衡量随机变量不确定性 的指标。对于分类数据集( D ),假设包含( K )个类别,第( k )类的样本占比为( p_k = \frac{|C_k|}{|D|} )(( |C_k| )为第( k )类的样本数,( |D| )为总样本数),则数据集( D )的熵定义为:H(D)=−∑k=1Kpklog⁡2pk H(D) = -\sum_{k=1}^K p_k \log_2 p_k H(D)=−k=1∑Kpklog2pk

熵的性质

  • 当所有样本属于同一类别(( p_k=1 )),熵( H(D)=0 )(无不确定性);
  • 当样本均匀分布在所有类别(( p_1=p_2=...=p_K )),熵达到最大值(最大不确定性)。
3.1.2 条件熵(Conditional Entropy):给定特征后的不确定性

条件熵是已知某特征( A )的取值后,数据集( D )的剩余不确定性 。假设特征( A )有( m )个取值( {a_1, a_2, ..., a_m} ),对应样本子集( D_1, D_2, ..., D_m )(( D_i \subseteq D )),则条件熵定义为:H(D∣A)=∑i=1m∣Di∣∣D∣H(Di) H(D|A) = \sum_{i=1}^m \frac{|D_i|}{|D|} H(D_i) H(D∣A)=i=1∑m∣D∣∣Di∣H(Di)

其中,( \frac{|D_i|}{|D|} )是子集( D_i )的样本占比,( H(D_i) )是子集( D_i )的熵。

3.2 分类树的特征选择准则

特征选择的目标是找到使"不确定性下降最多"的特征,以下是三类经典准则:

3.2.1 信息增益(Information Gain):不确定性的减少量

信息增益是原熵与条件熵的差值 ,表示"使用特征( A )分裂后,不确定性降低的程度":g(D,A)=H(D)−H(D∣A) g(D,A) = H(D) - H(D|A) g(D,A)=H(D)−H(D∣A)

决策规则:选择信息增益最大的特征作为当前节点的分裂特征。

例子:假设数据集( D )有10个样本(6正类( P ),4负类( N )),特征( A )将( D )分为( D_1 )(3P,2N)和( D_2 )(3P,2N):

  • 原熵( H(D) = -(0.6\log_20.6 + 0.4\log_20.4) ≈ 0.971 );
  • 子集熵( H(D_1) = -(0.6\log_20.6 + 0.4\log_20.4) ≈ 0.971 ),( H(D_2) ≈ 0.971 );
  • 条件熵( H(D|A) = 0.5×0.971 + 0.5×0.971 = 0.971 );
  • 信息增益( g(D,A) = 0.971 - 0.971 = 0 )(特征( A )无作用)。

若特征( B )将( D )分为( D_1 )(6P,0N)和( D_2 )(0P,4N):

  • 条件熵( H(D|B) = 0.6×0 + 0.4×0 = 0 );
  • 信息增益( g(D,B) = 0.971 - 0 = 0.971 )(特征( B )完美分裂)。
3.2.2 信息增益比(Information Gain Ratio):修正取值偏向

信息增益的缺陷:偏向取值多的特征(如"身份证号"这类唯一取值的特征,信息增益必然最大,但无实际意义)。

信息增益比通过归一化信息增益 解决这一问题,定义为:gR(D,A)=g(D,A)HA(D) g_R(D,A) = \frac{g(D,A)}{H_A(D)} gR(D,A)=HA(D)g(D,A)

其中,( H_A(D) )是特征( A )的"固有熵"(衡量特征取值的分散程度):HA(D)=−∑i=1m∣Di∣∣D∣log⁡2∣Di∣∣D∣ H_A(D) = -\sum_{i=1}^m \frac{|D_i|}{|D|} \log_2 \frac{|D_i|}{|D|} HA(D)=−i=1∑m∣D∣∣Di∣log2∣D∣∣Di∣

决策规则:选择信息增益比最大的特征。

例子:特征( C )有3个取值(样本占比0.2,0.2,0.6),信息增益( g(D,C)=0.02 ),固有熵( H_A(D)=1.37 ),则信息增益比( g_R(D,C)=0.0146 );特征( D )有10个取值(每个占比0.1),信息增益( g(D,D)=0.971 ),固有熵( H_A(D)=3.322 ),则信息增益比( g_R(D,D)=0.292 )。尽管( D )的信息增益更大,但信息增益比惩罚了其"取值多"的特点。

3.2.3 基尼指数(Gini Index):概率视角的纯度度量

基尼指数是CART分类树 的特征选择准则,衡量"随机抽取两个样本,类别不同的概率"。对于数据集( D ),基尼值定义为:Gini(D)=1−∑k=1Kpk2 \text{Gini}(D) = 1 - \sum_{k=1}^K p_k^2 Gini(D)=1−k=1∑Kpk2

其中,( p_k )是第( k )类的样本占比。

基尼值的性质

  • 当样本全同(( p_k=1 )),基尼值=0(最纯);
  • 当样本均匀分布,基尼值最大(最不纯)。

条件基尼指数是给定特征( A )后,子节点基尼值的加权平均 :Gini(D∣A)=∑i=1m∣Di∣∣D∣Gini(Di) \text{Gini}(D|A) = \sum_{i=1}^m \frac{|D_i|}{|D|} \text{Gini}(D_i) Gini(D∣A)=i=1∑m∣D∣∣Di∣Gini(Di)

决策规则:选择条件基尼指数最小的特征(即分裂后样本最纯)。

例子:数据集( D )(6P,4N)的基尼值( \text{Gini}(D) = 1 - (0.6^2 + 0.4^2) = 0.48 );特征( B )分裂后,( \text{Gini}(D|B) = 0.6×0 + 0.4×0 = 0 )(最优);特征( A )分裂后,( \text{Gini}(D|A) = 0.5×0.48 + 0.5×0.48 = 0.48 )(无作用)。

3.3 回归树的分裂准则:平方误差最小化

CART回归树的目标是最小化样本的平方误差 ,分裂准则为平方误差减少量最大

对于回归数据集( D )(输出为连续值( y_i )),样本的均值为( \bar{y} = \frac{1}{|D|}\sum_{i=1}^{|D|} y_i ),总平方误差(TSE)为:TSE(D)=∑i=1∣D∣(yi−yˉ)2 \text{TSE}(D) = \sum_{i=1}^{|D|} (y_i - \bar{y})^2 TSE(D)=i=1∑∣D∣(yi−yˉ)2

若特征( A )将( D )分为( D_1 )和( D_2 ),则分裂后的总平方误差为:TSE(D∣A)=TSE(D1)+TSE(D2) \text{TSE}(D|A) = \text{TSE}(D_1) + \text{TSE}(D_2) TSE(D∣A)=TSE(D1)+TSE(D2)

平方误差减少量 定义为:ΔTSE=TSE(D)−TSE(D∣A) \Delta \text{TSE} = \text{TSE}(D) - \text{TSE}(D|A) ΔTSE=TSE(D)−TSE(D∣A)

决策规则:选择( \Delta \text{TSE} )最大的特征(即分裂后误差下降最多)。

例子:房价数据集( D )(面积:50,60,70,80,90,100;房价:100,120,140,160,180,200):

  • 总平方误差( \text{TSE}(D) = (100-150)^2 + ... + (200-150)^2 = 7000 );
  • 特征"面积>75"分裂为( D_1 )(50,60,70,均值120)和( D_2 )(80,90,100,均值180);
  • 分裂后总误差( \text{TSE}(D|A) = 800 + 800 = 1600 );
  • 平方误差减少量( \Delta \text{TSE} = 7000 - 1600 = 5400 )(最优)。

3.4 树的停止条件:避免过拟合的提前终止

决策树若无限生长,会过拟合训练数据(即每个叶节点仅包含1个样本,完全记忆训练集)。因此需设置停止条件:

  1. 样本纯度条件:当前节点的样本全同(分类树)或输出值相同(回归树);
  2. 特征耗尽条件:无更多特征可用于分裂;
  3. 样本数量条件:节点样本数小于预设最小值(如5);
  4. 树深度条件:树的深度达到预设最大值(如10);
  5. 性能阈值条件:分裂后的信息增益/基尼指数变化小于预设阈值(如0.01)。

四、公式推导:从理论到计算的详细演绎

4.1 香农熵与条件熵的推导

香农熵的本质是信息的期望 。假设信息( I(x) )定义为"消除( x )的不确定性所需的比特数",则( I(x) = -\log_2 p(x) )(概率越小,信息越大)。对于随机变量( X ),熵是信息的期望:H(X)=E[I(X)]=−∑ip(xi)log⁡2p(xi) H(X) = \mathbb{E}[I(X)] = -\sum_{i} p(x_i) \log_2 p(x_i) H(X)=E[I(X)]=−i∑p(xi)log2p(xi)

条件熵( H(Y|X) )是"给定( X=x )时,( Y )的熵的期望",即:H(Y∣X)=∑xP(X=x)H(Y∣X=x)=∑xP(x)(−∑yP(y∣x)log⁡2P(y∣x)) H(Y|X) = \sum_{x} P(X=x) H(Y|X=x) = \sum_{x} P(x) \left(-\sum_{y} P(y|x) \log_2 P(y|x)\right) H(Y∣X)=x∑P(X=x)H(Y∣X=x)=x∑P(x)(−y∑P(y∣x)log2P(y∣x))

对于数据集( D ),( P(X=x) = \frac{|D_x|}{|D|} ),( P(y|x) = \frac{|D_{x,y}|}{|D_x|} ),因此条件熵可写为:H(D∣A)=∑i=1m∣Di∣∣D∣(−∑k=1K∣Ci,k∣∣Di∣log⁡2∣Ci,k∣∣Di∣)=−∑i=1m∑k=1K∣Ci,k∣∣D∣log⁡2∣Ci,k∣∣Di∣ H(D|A) = \sum_{i=1}^m \frac{|D_i|}{|D|} \left(-\sum_{k=1}^K \frac{|C_{i,k}|}{|D_i|} \log_2 \frac{|C_{i,k}|}{|D_i|}\right) = -\sum_{i=1}^m \sum_{k=1}^K \frac{|C_{i,k}|}{|D|} \log_2 \frac{|C_{i,k}|}{|D_i|} H(D∣A)=i=1∑m∣D∣∣Di∣(−k=1∑K∣Di∣∣Ci,k∣log2∣Di∣∣Ci,k∣)=−i=1∑mk=1∑K∣D∣∣Ci,k∣log2∣Di∣∣Ci,k∣

其中,( C_{i,k} )是子集( D_i )中第( k )类的样本数。

4.2 信息增益与信息增益比的推导

信息增益是"原熵减去条件熵",即:g(D,A)=H(D)−H(D∣A)=−∑k=1K∣Ck∣∣D∣log⁡2∣Ck∣∣D∣+∑i=1m∑k=1K∣Ci,k∣∣D∣log⁡2∣Ci,k∣∣Di∣ g(D,A) = H(D) - H(D|A) = -\sum_{k=1}^K \frac{|C_k|}{|D|} \log_2 \frac{|C_k|}{|D|} + \sum_{i=1}^m \sum_{k=1}^K \frac{|C_{i,k}|}{|D|} \log_2 \frac{|C_{i,k}|}{|D_i|} g(D,A)=H(D)−H(D∣A)=−k=1∑K∣D∣∣Ck∣log2∣D∣∣Ck∣+i=1∑mk=1∑K∣D∣∣Ci,k∣log2∣Di∣∣Ci,k∣

信息增益比是信息增益除以特征( A )的固有熵,固有熵衡量"特征( A )的取值分散程度":HA(D)=−∑i=1m∣Di∣∣D∣log⁡2∣Di∣∣D∣ H_A(D) = -\sum_{i=1}^m \frac{|D_i|}{|D|} \log_2 \frac{|D_i|}{|D|} HA(D)=−i=1∑m∣D∣∣Di∣log2∣D∣∣Di∣

当特征( A )的取值越多,( H_A(D) )越大,信息增益比越小,从而惩罚取值多的特征。

4.3 基尼指数的推导

基尼指数的本质是二分类错误概率的期望。假设随机抽取两个样本( s_1, s_2 ),则:

  • ( s_1 )属于第( k )类的概率为( p_k );
  • ( s_2 )不属于第( k )类的概率为( 1 - p_k );
  • 两个样本类别不同的总概率为( \sum_{k=1}^K p_k(1 - p_k) = 1 - \sum_{k=1}^K p_k^2 )。

这正是基尼值的定义:Gini(D)=1−∑k=1Kpk2 \text{Gini}(D) = 1 - \sum_{k=1}^K p_k^2 Gini(D)=1−k=1∑Kpk2

条件基尼指数是"每个子集基尼值的加权平均",权重为子集的样本占比:Gini(D∣A)=∑i=1m∣Di∣∣D∣Gini(Di) \text{Gini}(D|A) = \sum_{i=1}^m \frac{|D_i|}{|D|} \text{Gini}(D_i) Gini(D∣A)=i=1∑m∣D∣∣Di∣Gini(Di)

4.4 回归树平方误差准则的推导

回归树的目标是最小化预测值与真实值的平方误差。对于节点( D ),最优预测值是样本的均值( \bar{y} = \frac{1}{|D|}\sum_{i=1}^{|D|} y_i )(证明:对( \text{TSE} = \sum (y_i - \hat{y})^2 )求导,令导数为0,得( \hat{y} = \bar{y} ))。

分裂后的总平方误差是各子集平方误差之和,平方误差减少量为:ΔTSE=∑i=1∣D∣(yi−yˉ)2−(∑i=1∣D1∣(yi−yˉ∗1)2+∑∗i=1∣D2∣(yi−yˉ2)2) \Delta \text{TSE} = \sum_{i=1}^{|D|} (y_i - \bar{y})^2 - \left( \sum_{i=1}^{|D_1|} (y_i - \bar{y}*1)^2 + \sum*{i=1}^{|D_2|} (y_i - \bar{y}_2)^2 \right) ΔTSE=i=1∑∣D∣(yi−yˉ)2− i=1∑∣D1∣(yi−yˉ∗1)2+∑∗i=1∣D2∣(yi−yˉ2)2

其中,( \bar{y}_1 )和( \bar{y}_2 )是子集( D_1 )和( D_2 )的均值。

五、完整模型求解步骤:从数据到决策树的落地流程

5.1 步骤1:数据准备与预处理

  1. 数据清洗:处理缺失值(如删除、填充或用算法处理,如C4.5的概率分配)、异常值(如截断或转换);
  2. 特征编码:离散特征保持原值,连续特征无需标准化(决策树对尺度不敏感);
  3. 数据划分:将数据集分为训练集(70%)、验证集(20%)、测试集(10%)(验证集用于剪枝)。

5.2 步骤2:基于准则选择最优特征

对当前节点的所有特征,计算其信息增益/信息增益比/基尼指数/平方误差减少量,选择最优特征作为分裂特征。

注意:连续特征需离散化------将特征值排序,选择所有可能的分割点(如相邻值的中点),计算每个分割点的准则值,选最优分割点。

5.3 步骤3:递归构建决策树

  1. 分裂节点:用最优特征将当前节点分裂为子节点(离散特征:每个取值对应一个子节点;连续特征:分为"≤分割点"和">分割点"两个子节点);
  2. 递归处理:对每个子节点重复步骤2-3,直到满足停止条件(如步骤3.4所述);
  3. 生成叶节点:对子节点标记决策结果(分类树为多数类,回归树为均值)。

5.4 步骤4:剪枝优化:从过拟合到泛化

剪枝是减少树的复杂度,提高泛化性能的关键步骤,分为两类:

  1. 预剪枝(Pre-pruning):在树生长过程中,提前停止分裂(如分裂后验证集 accuracy 不提升,则停止);
  2. 后剪枝(Post-pruning):先生成完整树,再从叶节点向上剪枝(将子树替换为叶节点,若验证集性能提升,则保留剪枝后的结构)。

后剪枝的具体步骤:a. 从最底层的子树开始,计算剪枝前(子树)和剪枝后(叶节点)的验证集误差;b. 若剪枝后误差更小,保留剪枝结果;否则,保留原结构;c. 重复a-b,直到根节点。

5.5 步骤5:模型预测与结果解释

预测流程:对新样本,从根节点开始,按特征取值沿分支向下遍历,直到叶节点,输出叶节点的决策结果。

结果解释:决策树的可解释性强,可通过"决策路径"解释预测结果。例如,贷款拒绝的决策路径:"收入≤5000→负债≥30%→信用评分≤600→拒绝贷款"。

六、适用边界与场景匹配:决策树的"能"与"不能"

6.1 优势场景:为什么选择决策树?

  1. 高可解释性:决策路径清晰,可用于需要"透明决策"的场景(如金融、医疗);
  2. 低特征工程成本:无需标准化、归一化或非线性转换,对缺失值和异常值有一定鲁棒性;
  3. 非线性与交互作用:能自然捕捉特征间的交互作用(如"收入高且负债低→贷款批准");
  4. 快速原型开发:训练速度快,适合探索性数据分析(如快速发现特征重要性)。

6.2 局限场景:决策树的"能力边界"

  1. 高维稀疏数据:如文本分类(特征是词频,维度高达10^4),决策树容易过拟合;
  2. 样本不平衡:如癌症检测(阳性样本占1%),叶节点可能偏向多数类(阴性);
  3. 低稳定性:小的训练数据变化可能导致树结构巨变(如删除一个样本,树的分裂特征完全改变);
  4. 低精度要求:单棵决策树的泛化性能不如集成学习(如随机森林、XGBoost)。

6.3 与其他算法的互补性:从决策树到集成学习

决策树是集成学习的基础,通过组合多棵决策树,可解决单棵树的局限:

  • 随机森林:用Bagging集成多棵决策树,降低过拟合和不稳定性;
  • GBDT/XGBoost:用梯度提升集成,逐步修正前一棵树的误差,提高精度;
  • LightGBM/CatBoost:优化决策树的训练速度和内存使用,适合大规模数据。

七、总结

决策树是机器学习中最"接地气"的算法之一------它模拟人类决策逻辑,可解释性强,对数据预处理要求低,适合快速原型开发和可解释性场景。但其"容易过拟合、不稳定"的缺陷,需通过剪枝或集成学习解决。

在实际应用中,决策树很少单独使用,但却是理解集成学习(如XGBoost)的关键基础。掌握决策树的原理,能帮助你更深入地理解复杂算法的本质。

案例介绍

模拟贷款申请数据集,包含4个特征(年龄:青年/中年/老年,收入:高/中/低,工作:稳定/不稳定,负债:高/低)和1个标签(贷款批准:是/否),用于训练CART分类决策树并预测新样本。

完整Python代码

python 复制代码
import numpy as np
# import matplotlib.pyplot as plt # 不再需要 matplotlib
from graphviz import Digraph
import os

# 定义数据集:特征为[年龄, 收入, 工作, 负债],标签为[贷款批准]
# 年龄:0=青年, 1=中年, 2=老年;收入:0=低, 1=中, 2=高;工作:0=不稳定, 1=稳定;负债:0=低, 1=高;贷款批准:0=否, 1=是
train_data = np.array([
    [0, 2, 0, 1, 0],
    [0, 2, 0, 0, 0],
    [0, 1, 1, 1, 1],
    [0, 0, 1, 0, 1],
    [0, 2, 1, 1, 1],
    [1, 2, 0, 1, 0],
    [1, 2, 0, 0, 0],
    [1, 1, 1, 0, 1],
    [1, 0, 1, 0, 1],
    [1, 0, 0, 1, 0],
    [2, 0, 0, 1, 0],
    [2, 0, 0, 0, 1],
    [2, 1, 1, 0, 1],
    [2, 2, 1, 1, 1],
    [2, 2, 0, 1, 0]
])


def calculate_gini(dataset):
    """
    计算基尼指数
    :param dataset: 数据集,最后一列为标签
    :return: 基尼指数值
    """
    # 获取数据集样本数量
    n_samples = dataset.shape[0]
    if n_samples == 0:
        return 0.0
    # 获取标签列并计算各类别样本数
    labels = dataset[:, -1]
    unique_labels, counts = np.unique(labels, return_counts=True)
    # 计算基尼指数
    gini = 1.0
    for count in counts:
        prob = count / n_samples
        gini -= prob ** 2
    return gini


def split_dataset(dataset, feature_idx, threshold):
    """
    根据特征索引和阈值分割数据集
    :param dataset: 输入数据集
    :param feature_idx: 用于分割的特征索引
    :param threshold: 分割阈值
    :return: 左子数据集(特征值≤阈值)和右子数据集(特征值>阈值)
    """
    left_data = dataset[dataset[:, feature_idx] <= threshold]
    right_data = dataset[dataset[:, feature_idx] > threshold]
    return left_data, right_data


def select_best_feature(dataset):
    """
    选择最优分裂特征和阈值
    :param dataset: 输入数据集
    :return: 最优特征索引、最优阈值、最小基尼指数
    """
    # 特征数量(最后一列为标签)
    n_features = dataset.shape[1] - 1
    best_gini = float('inf')
    best_feature = -1
    best_threshold = None

    # 遍历所有特征
    for feature_idx in range(n_features):
        # 获取当前特征的所有唯一值
        feature_values = np.unique(dataset[:, feature_idx])
        # 遍历所有可能的分割阈值(相邻值的中点)
        for i in range(len(feature_values) - 1):
            threshold = (feature_values[i] + feature_values[i + 1]) / 2
            # 分割数据集
            left_data, right_data = split_dataset(dataset, feature_idx, threshold)
            # 计算条件基尼指数
            gini_left = calculate_gini(left_data)
            gini_right = calculate_gini(right_data)
            # 加权平均条件基尼指数
            weighted_gini = (left_data.shape[0] / dataset.shape[0]) * gini_left + (
                        right_data.shape[0] / dataset.shape[0]) * gini_right
            # 更新最优特征和阈值
            if weighted_gini < best_gini:
                best_gini = weighted_gini
                best_feature = feature_idx
                best_threshold = threshold
    return best_feature, best_threshold, best_gini


def majority_class(dataset):
    """
    计算数据集的多数类
    :param dataset: 输入数据集
    :return: 多数类标签
    """
    labels = dataset[:, -1]
    unique_labels, counts = np.unique(labels, return_counts=True)
    # 返回出现次数最多的标签
    return unique_labels[np.argmax(counts)]


def build_decision_tree(dataset, depth=0, max_depth=3, min_samples=2):
    """
    递归构建CART分类决策树
    :param dataset: 输入数据集
    :param depth: 当前树深度
    :param max_depth: 树的最大深度(预剪枝参数)
    :param min_samples: 叶节点最小样本数(预剪枝参数)
    :return: 决策树字典结构
    """
    # 获取所有标签
    labels = dataset[:, -1]
    # 停止条件1:所有样本标签相同
    if len(np.unique(labels)) == 1:
        return {"leaf": True, "label": labels[0]}
    # 停止条件2:达到最大深度
    if depth >= max_depth:
        return {"leaf": True, "label": majority_class(dataset)}
    # 停止条件3:样本数小于最小样本数
    if dataset.shape[0] < min_samples:
        return {"leaf": True, "label": majority_class(dataset)}

    # 选择最优分裂特征和阈值
    best_feature, best_threshold, best_gini = select_best_feature(dataset)
    # 若无法分裂(所有特征值相同),则返回多数类
    if best_feature == -1:
        return {"leaf": True, "label": majority_class(dataset)}

    # 分裂数据集
    left_data, right_data = split_dataset(dataset, best_feature, best_threshold)
    # 递归构建左右子树
    left_tree = build_decision_tree(left_data, depth + 1, max_depth, min_samples)
    right_tree = build_decision_tree(right_data, depth + 1, max_depth, min_samples)

    # 返回树结构:非叶节点包含特征、阈值、左右子树
    return {
        "leaf": False,
        "feature": best_feature,
        "threshold": best_threshold,
        "left": left_tree,
        "right": right_tree
    }


def predict_sample(tree, sample):
    """
    使用决策树预测单个样本
    :param tree: 决策树结构
    :param sample: 输入样本(无标签)
    :return: 预测标签
    """
    # 若为叶节点,返回标签
    if tree["leaf"]:
        return tree["label"]
    # 否则根据特征和阈值递归预测
    if sample[tree["feature"]] <= tree["threshold"]:
        return predict_sample(tree["left"], sample)
    else:
        return predict_sample(tree["right"], sample)


# 可视化相关函数
def plot_tree_graphviz(tree, feature_names):
    """
    使用 Graphviz 绘制决策树
    :param tree: 决策树字典
    :param feature_names: 特征名称列表
    """
    dot = Digraph(name="Decision Tree", comment="Decision Tree Visualization")
    # 设置图形属性:dpi控制分辨率
    dot.attr(dpi='300')
    # 设置节点样式
    dot.attr('node', shape='box', style='filled', fontname='SimHei', fillcolor='lightblue')
    dot.attr('edge', fontname='SimHei')

    # 使用计数器生成唯一节点ID
    node_counter = 0

    def add_nodes_edges(sub_tree, parent_id=None, edge_label=""):
        nonlocal node_counter
        current_id = str(node_counter)
        node_counter += 1

        if sub_tree['leaf']:
            # 叶节点
            label_name = "批准" if sub_tree['label'] == 1 else "拒绝"
            # 根据类别设置颜色
            color = 'lightgreen' if sub_tree['label'] == 1 else 'lightcoral'
            dot.node(current_id, label=f"结果: {label_name}", fillcolor=color, shape='ellipse')
        else:
            # 决策节点
            feature_name = feature_names[sub_tree['feature']]
            threshold = sub_tree['threshold']
            label = f"{feature_name} <= {threshold}?"
            dot.node(current_id, label=label, fillcolor='lightyellow')

            # 递归处理子节点
            add_nodes_edges(sub_tree['left'], current_id, "是")
            add_nodes_edges(sub_tree['right'], current_id, "否")

        # 添加边(如果不是根节点)
        if parent_id is not None:
            dot.edge(parent_id, current_id, label=edge_label)

    # 开始递归
    add_nodes_edges(tree)
    
    # 渲染并显示
    try:
        output_path = 'decision_tree_graph'
        dot.render(output_path, view=True, format='png', cleanup=True)
        print(f"决策树可视化已保存至 {os.path.abspath(output_path)}.png 并尝试打开。")
    except Exception as e:
        print(f"Graphviz 绘图失败: {e}")
        print("请确保已安装 Graphviz 软件并配置了环境变量。")


# 主程序:构建决策树并预测新样本
if __name__ == "__main__":
    # 构建决策树
    decision_tree = build_decision_tree(train_data)
    # 新样本:年龄=青年(0),收入=低(0),工作=不稳定(0),负债=低(0)
    new_sample = np.array([0, 0, 0, 0])
    # 预测结果
    prediction = predict_sample(decision_tree, new_sample)
    # 输出预测结果(0=拒绝贷款,1=批准贷款)
    print(f"新样本贷款批准预测结果:{'是' if prediction == 1 else '否'}")

    # 可视化决策树
    feature_names = ['年龄', '收入', '工作', '负债']
    try:
        plot_tree_graphviz(decision_tree, feature_names)
    except Exception as e:
        print(f"可视化出错: {e}")

代码整体结构与数学背景

该代码实现了CART分类决策树 (Classification and Regression Tree)的完整流程:从数据集定义、基尼指数计算、最优特征选择,到递归构建树、样本预测及可视化。CART树是二叉树,分类时使用基尼指数作为不纯度指标,回归时使用均方误差,代码中仅实现分类功能。


1. 数据与库准备

1.1 导入依赖库
python 复制代码
import numpy as np
from graphviz import Digraph
import os
  • numpy:用于高效处理数组形式的数据集。
  • graphviz:用于将决策树可视化(需单独安装并配置环境变量)。
  • os:用于文件路径处理。
1.2 定义数据集
python 复制代码
train_data = np.array([
    [0, 2, 0, 1, 0], [0, 2, 0, 0, 0], [0, 1, 1, 1, 1], [0, 0, 1, 0, 1], [0, 2, 1, 1, 1],
    [1, 2, 0, 1, 0], [1, 2, 0, 0, 0], [1, 1, 1, 0, 1], [1, 0, 1, 0, 1], [1, 0, 0, 1, 0],
    [2, 0, 0, 1, 0], [2, 0, 0, 0, 1], [2, 1, 1, 0, 1], [2, 2, 1, 1, 1], [2, 2, 0, 1, 0]
])

数据集包含4个特征1个标签,编码规则(关键!):

  • 年龄:0=青年1=中年2=老年
  • 收入:0=低1=中2=高
  • 工作:0=不稳定1=稳定
  • 负债:0=低1=高
  • 贷款批准:0=拒绝1=批准

2. 核心函数解析

2.1 基尼指数计算(不纯度指标)
python 复制代码
def calculate_gini(dataset):
    n_samples = dataset.shape[0]  # 样本总数
    if n_samples == 0:
        return 0.0
    labels = dataset[:, -1]  # 提取标签列
    unique_labels, counts = np.unique(labels, return_counts=True)  # 统计各类别样本数
    gini = 1.0
    for count in counts:
        prob = count / n_samples  # 类别概率
        gini -= prob ** 2  # 基尼指数公式:1 - Σ(概率²)
    return gini

数学原理 :基尼指数衡量数据集的不纯度,值越小,数据越"纯"(样本趋于同一类别)。公式为:
Gini(D)=1−∑k=1K(∣Ck∣∣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

其中 DDD 是数据集,CkC_kCk 是 DDD 中属于第 kkk 类的样本子集。

2.2 数据集二分裂
python 复制代码
def split_dataset(dataset, feature_idx, threshold):
    left_data = dataset[dataset[:, feature_idx] <= threshold]  # 特征值≤阈值的样本
    right_data = dataset[dataset[:, feature_idx] > threshold]  # 特征值>阈值的样本
    return left_data, right_data

CART树特性 :始终将数据集分裂为左(≤阈值)、右(>阈值)两个子集,这是CART树为二叉树的原因。

2.3 最优特征与阈值选择
python 复制代码
def select_best_feature(dataset):
    n_features = dataset.shape[1] - 1  # 特征数量(最后一列为标签)
    best_gini = float('inf')  # 初始化最小基尼指数为无穷大
    best_feature = -1  # 初始化最优特征索引
    best_threshold = None  # 初始化最优阈值

    for feature_idx in range(n_features):
        feature_values = np.unique(dataset[:, feature_idx])  # 当前特征的所有唯一值
        # 遍历相邻唯一值的中点作为候选阈值(减少计算量)
        for i in range(len(feature_values) - 1):
            threshold = (feature_values[i] + feature_values[i+1]) / 2
            # 分裂数据集
            left_data, right_data = split_dataset(dataset, feature_idx, threshold)
            # 计算条件基尼指数(加权平均子树基尼)
            weighted_gini = (len(left_data)/len(dataset)) * calculate_gini(left_data) + \
                           (len(right_data)/len(dataset)) * calculate_gini(right_data)
            # 更新最优分裂点
            if weighted_gini < best_gini:
                best_gini = weighted_gini
                best_feature = feature_idx
                best_threshold = threshold
    return best_feature, best_threshold, best_gini

关键逻辑

  • 对每个特征,取其相邻唯一值的中点作为候选阈值(如特征值为0,1,2,则候选阈值为0.5、1.5),避免冗余计算。
  • 条件基尼指数:分裂后左右子树的基尼指数按样本比例加权平均,该值越小,分裂效果越好。
2.4 多数类计算(叶子节点决策)
python 复制代码
def majority_class(dataset):
    labels = dataset[:, -1]
    unique_labels, counts = np.unique(labels, return_counts=True)
    return unique_labels[np.argmax(counts)]  # 返回出现次数最多的标签

当节点无法继续分裂时(如达到最大深度、样本数过少等),以多数类作为叶子节点的输出。

2.5 递归构建决策树
python 复制代码
def build_decision_tree(dataset, depth=0, max_depth=3, min_samples=2):
    labels = dataset[:, -1]
    # 停止条件1:所有样本标签相同
    if len(np.unique(labels)) == 1:
        return {"leaf": True, "label": labels[0]}
    # 停止条件2:达到最大深度(预剪枝,防止过拟合)
    if depth >= max_depth:
        return {"leaf": True, "label": majority_class(dataset)}
    # 停止条件3:样本数小于最小样本数(预剪枝)
    if dataset.shape[0] < min_samples:
        return {"leaf": True, "label": majority_class(dataset)}

    # 选择最优分裂特征和阈值
    best_feature, best_threshold, best_gini = select_best_feature(dataset)
    # 无法分裂(所有特征值相同)
    if best_feature == -1:
        return {"leaf": True, "label": majority_class(dataset)}

    # 递归分裂左右子树
    left_data, right_data = split_dataset(dataset, best_feature, best_threshold)
    left_tree = build_decision_tree(left_data, depth+1, max_depth, min_samples)
    right_tree = build_decision_tree(right_data, depth+1, max_depth, min_samples)

    # 返回非叶子节点结构
    return {
        "leaf": False, "feature": best_feature, "threshold": best_threshold,
        "left": left_tree, "right": right_tree
    }

树结构(字典表示)

  • 叶子节点:{"leaf": True, "label": 类别}
  • 非叶子节点:{"leaf": False, "feature": 特征索引, "threshold": 阈值, "left": 左子树, "right": 右子树}

预剪枝策略

  • max_depth:限制树的最大深度,防止树过深导致过拟合。
  • min_samples:限制叶子节点的最小样本数,避免噪声数据影响。
2.6 样本预测
python 复制代码
def predict_sample(tree, sample):
    if tree["leaf"]:  # 叶子节点直接返回标签
        return tree["label"]
    # 根据特征值与阈值的比较递归预测
    if sample[tree["feature"]] <= tree["threshold"]:
        return predict_sample(tree["left"], sample)
    else:
        return predict_sample(tree["right"], sample)

预测流程:从根节点开始,根据当前节点的特征和阈值判断进入左/右子树,直到到达叶子节点,返回叶子节点的标签。


3. 决策树可视化

python 复制代码
def plot_tree_graphviz(tree, feature_names):
    dot = Digraph(name="Decision Tree", comment="Decision Tree Visualization")
    dot.attr(dpi='300')  # 分辨率
    dot.attr('node', shape='box', style='filled', fontname='SimHei', fillcolor='lightblue')  # 节点样式
    dot.attr('edge', fontname='SimHei')  # 边样式

    node_counter = 0
    def add_nodes_edges(sub_tree, parent_id=None, edge_label=""):
        nonlocal node_counter
        current_id = str(node_counter)
        node_counter += 1

        if sub_tree['leaf']:
            # 叶子节点:形状为椭圆,颜色区分类别
            label_name = "批准" if sub_tree['label'] == 1 else "拒绝"
            color = 'lightgreen' if sub_tree['label'] == 1 else 'lightcoral'
            dot.node(current_id, label=f"结果: {label_name}", fillcolor=color, shape='ellipse')
        else:
            # 决策节点:显示特征与阈值
            feature_name = feature_names[sub_tree['feature']]
            dot.node(current_id, label=f"{feature_name} <= {sub_tree['threshold']}?", fillcolor='lightyellow')
            # 递归处理左右子树
            add_nodes_edges(sub_tree['left'], current_id, "是")
            add_nodes_edges(sub_tree['right'], current_id, "否")

        # 添加边(非根节点)
        if parent_id is not None:
            dot.edge(parent_id, current_id, label=edge_label)

    add_nodes_edges(tree)  # 开始递归绘制
    # 渲染为PNG文件
    try:
        output_path = 'decision_tree_graph'
        dot.render(output_path, view=True, format='png', cleanup=True)
        print(f"决策树已保存至 {os.path.abspath(output_path)}.png")
    except Exception as e:
        print(f"可视化失败: {e},请确保安装Graphviz并配置环境变量")

可视化逻辑 :使用graphviz.Digraph递归绘制节点和边,通过颜色和形状区分决策节点与叶子节点(批准为绿,拒绝为红)。


4. 主程序运行

python 复制代码
if __name__ == "__main__":
    # 构建决策树
    decision_tree = build_decision_tree(train_data)
    # 新样本:年龄=青年(0),收入=低(0),工作=不稳定(0),负债=低(0)
    new_sample = np.array([0, 0, 0, 0])
    # 预测
    prediction = predict_sample(decision_tree, new_sample)
    print(f"新样本贷款批准预测结果:{'是' if prediction == 1 else '否'}")

    # 可视化
    feature_names = ['年龄', '收入', '工作', '负债']
    try:
        plot_tree_graphviz(decision_tree, feature_names)
    except Exception as e:
        print(f"可视化出错: {e}")

运行结果

  • 预测结果:根据样本特征[青年, 低收入, 不稳定工作, 低负债],模型会输出"是"或"否"。
  • 可视化:生成决策树PNG文件,直观展示分裂规则。

代码优缺点与改进方向

优点:
  1. 纯numpy实现,无复杂依赖,便于理解CART树底层逻辑。
  2. 支持预剪枝,避免过拟合。
  3. 可视化清晰,便于结果展示。
改进方向:
  1. 增加后剪枝功能,进一步优化树结构。
  2. 支持连续特征的自动离散化(当前已通过阈值中点实现,但可优化)。
  3. 增加模型评估指标(如准确率、混淆矩阵)。

该代码完整覆盖了CART分类树的核心流程,适合用于数学建模比赛中的分类任务,可直接修改数据集和参数应用到其他场景。

相关推荐
算法如诗3 小时前
豆渣发酵工艺优化 - 基于响应面法结合遗传算法
数学建模
88号技师6 小时前
2026年1月一区SCI-波动光学优化算法Wave Optics Optimizer-附Matlab免费代码
开发语言·算法·数学建模·matlab·优化算法
AI科技星7 小时前
张祥前统一场论电荷定义方程分析报告
开发语言·经验分享·线性代数·算法·数学建模
老歌老听老掉牙11 小时前
基于参数化模型的砂轮轮廓建模与可视化
python·数学建模
郝学胜-神的一滴11 小时前
李航《机器学习方法》全面解析与高效学习指南
人工智能·python·算法·机器学习·数学建模·scikit-learn
Sunsets_Red1 天前
待修改莫队与普通莫队优化
java·c++·python·学习·算法·数学建模·c#
智算菩萨2 天前
【Python机器学习】决策树与随机森林:解释性与鲁棒性的平衡
python·决策树·机器学习
BB学长2 天前
Icepak|01功能介绍
算法·数学建模·能源·微信公众平台
Cathy Bryant2 天前
傅里叶变换(二):旋转楼梯
笔记·算法·数学建模·信息与通信·傅里叶分析