决策树(Decision Tree)- CART算法

决策树(Decision Tree)- CART算法

陈拓 2026/8/1-2026/8/8

1. 前言

CART(Classification and Regression Tree,分类与回归树)是决策树算法中的一种重要框架,由 Breiman 等人在 1984 年提出。它是目前机器学习领域中‌唯一同时支持分类与回归任务‌的主流决策树算法,也是 sklearn、XGBoost 等现代框架的基石 。‌‌

1.1 核心特点

CART 框架与其他决策树算法(如 ID3、C4.5)相比,具有以下显著特征:

  • ‌严格二叉树结构‌:每个非叶子节点仅有两个子节点(“是”或“否”),即使特征有多个取值,也通过二分法划分为两部分 。
  • 统一的分裂标准‌:‌分类任务‌:使用‌基尼系数(Gini Impurity)‌衡量不纯度,选择使基尼指数最小的划分点。回归任务‌:使用‌最小平方误差(MSE)‌,目标是最小化预测值与真实值的平方误差 。
  • ‌代价复杂度剪枝(CCP)‌:采用后剪枝策略,通过引入正则化参数 𝛼 生成子树序列,并利用交叉验证选择泛化能力最强的模型,有效防止过拟合 。
  • 天然支持连续特征‌:无需预先离散化,可直接寻找最优切分点 。‌‌

1.2 教学为什么常用 ID3?

大多数教材和课程讲决策树,会先讲 ID3(用「信息熵 / 信息增益」选特征)。原因很朴素:熵 =「混乱度」的直觉最直白,而且 ID3 是 Quinlan 最早提出的、最经典的决策树算法,配套的「是否打网球」例子几乎是教科书标配,零基础读者最容易跟上。

1.3 本文为什么用纯 CART?

因为本文所有代码都跑在 scikit-learn 上,而 sklearn 的 DecisionTreeClassifier分类器(模型)从底层就只实现了 CART 这一种树(没有 ID3,也没有 C4.5)。CART 默认用「基尼指数」给特征打分,且只能二叉分裂、自上而下贪心生长。与其在 CART 框架里把评分公式硬换成 ID3 的熵,不如全程使用 sklearn 默认的纯 CART + 基尼指数——既和工具行为完全一致,也不会出现「混用两种算法」的迷惑。

2. 决策树到底是什么?

决策树是一种“像人一样做判断”的机器学习模型。它把一堆 if…else 的问题串成一棵树:从最上面的一个问题开始,根据答案往下走,走到叶子节点就给出结论。

举个生活里的例子:周末到了,你想知道“今天要不要去打网球”,你可能会这样想:“先看天气:如果是阴天,直接去;如果是晴天,再看湿度大不大;如果是雨天,再看风大不大……”这一连串判断,就是一棵决策树。

和“硬规则 if-else”相比,决策树的好处是:这些问题(先问哪个特征、在哪里切分)是由算法对着数据自动学出来的。本文根据表1使用 CART 算法来“学”这棵树。

表1  训练数据(14 天)

Outlook

Temperature

Humidity

Wind

Play

sunny

hot

high

weak

no

sunny

hot

high

strong

no

overcast

hot

high

weak

yes

rain

mild

high

weak

yes

rain

cool

normal

weak

yes

rain

cool

normal

strong

no

overcast

cool

normal

strong

yes

sunny

mild

high

weak

no

sunny

cool

normal

weak

yes

rain

mild

normal

weak

yes

sunny

mild

normal

strong

yes

overcast

mild

high

strong

yes

overcast

hot

normal

weak

yes

rain

mild

high

strong

no

有4个特征:天气(Outlook)、温度(Temperature)、湿度(Humidity)、风(Wind);标签是“是否打球(Play)”。

3. 一棵树的“零件”:三种节点

图1  决策树的三种节点(根、内部、叶)

  • 根节点:最顶端的节点,代表“最重要的第一个问题”,对应数据里区分度最高的特征。
  • 内部节点:中间的节点,代表“继续追问的下一个问题”,树的生长就是不断在这些节点上做切分。
  • 叶节点:最底端的节点,代表“最终答案/预测结果”,不再往下分。

根节点→内部节点(不断提问)→叶节点(给出答案)

4. 核心问题:先问哪个特征?(基尼指数与基尼增益)

CART 算法采用贪心策略(是一种经典的算法设计思路,核心是在每一步决策时都选择当前状态下的局部最优解,期望通过这些局部最优的累积,最终得到全局最优解。):每一步都选那个“最能把数据分干净”的特征来提问。衡量“干净程度”的指标,CART 默认用“基尼指数(Gini index)”和“基尼增益(Gini gain)”。在 scikit-learn 里通过 criterion="gini" 指定CART 算法作为选特征的评分公式。

基尼指数与基尼增益的核心定义与区别:

  • ‌基尼指数 (Gini Index)‌:通常指划分后的‌加权基尼不纯度‌。它衡量的是在某个特征条件下,数据集整体的“混乱程度”。‌基尼指数越低‌,说明划分后的子集纯度越高,划分效果越好。
  • ‌基尼增益 (Gini Gain)‌:指划分前后基尼不纯度的‌减少量‌。它直接量化了某个特征对提升数据纯度的贡献。‌基尼增益越大‌,说明该特征的分类能力越强,是决策树选择划分依据的首选标准。

经济学起源

基尼指数原本是衡量社会财富/收入分配不平等程度的统计量,取值[0,1]。

  • G=0:绝对平均,所有人财富一样;
  • G=1:极端不平等,财富集中在极少数人手里。

CART决策树借用同一数学思想,用基尼看样本类别混乱,基尼增益用来评价划分效果。

4.1 基尼指数:衡量样本不纯度

CART 算法定义树的生长方式为“二叉分裂”,基尼指数负责为每一次“分裂”打分评估优劣。

基尼指数量化集合内类别混杂程度,通用公式:

对于二分类:

Gini(D) = 1 – p₁² - p₂²

设一类占比为p,另一类占比就必定是(1−p):

Σ pᵢ² = p² + (1−p)²

代入通用公式展开: Gini = 1 − [ p² + (1 − p)² ]

     = 1 − [ p² + (1 − 2p + p²) ]      // (1−p)² 完全平方展开

     = 1 − [ 1 − 2p + 2p² ]            // 合并两个 p²

     = 1 − 1 + 2p − 2p²                // 去括号

     = 2p − 2p²

     = 2p(1 − p)                       // 提取公因式 2p

 即 Gini = 2·p·(1−p)。

  • 如果样本全部属于同一类别占100%:Gini=0,数据集最“纯”。
  • 如果两类样本各占 50%:Gini=0.5,数据集最“不纯”。

规律:基尼指数越大,样本越不纯、越不确定;越小,越纯、越确定。CART 希望每切一刀后,各分支的基尼指数都尽量小,切分到最后到达叶节点Gini=0。

二分类基尼曲线:样本分布越接近 50/50,基尼指数越大、不纯度越高(最大值为 0.5

4.2 基尼增益:切一刀能“降多少不纯度”

4.2.1 基尼增益的计算

基尼增益越大,说明用这个特征提问,“降不纯度”的效果越好。CART 就选基尼增益最大的特征作为当前节点的切分依据。

计算公式:

对某个特征 a 做切分后,基尼增益 = 切分前的总基尼 − 切分后各分支基尼的加权平均值:

公式的大白话是:增益 = 父节点不纯度 − 各子分支不纯度的加权平均

(权重 = 该分支样本数 ÷ 父节点样本数)

其中:

  • D = 当前这个节点上、还没被切分的样本集合(也叫“父节点样本”)。在根节点时 D 就是全部14天;在更深的节点时,D 就只是落到那个节点的那部分样本。
  • a = 正在考虑用来切分的那个特征(天气/温度/湿度/风)。
  • Dᵥ = 用特征 a 把 D 切开后,第 v 个分支(子集)里的样本。下标 v 就是给各个分支编号。
  • CART(二叉):每个特征只产生 2 个 Dᵥ(左/右),Σ 只加 2 项:

Gain(D, a) = Gini(D) − |D₁|/|D|· Gini(D₁) - |D₂|/|D|· Gini(D₂)

根节点计算:

注意:Outlook ≤ 0.5阈值的计算见后面4.3节,这里我们先使用它。

对于根节点:

• D = 根节点全部14天,切分后得到 D₁ = 4 天(阴天),D₂ = 10 天(雨+晴)

• a = Outlook(阈值的计算见下一小节)

• Dᵥ 在 CART 二叉下只有 2 个分支:

  • D₁ = 左分支Outlook ≤ 0.5→ 4 天(阴天)
  • D₂ = 右分支Outlook > 0.5 → 10 天(雨+晴)

• |Dᵥ|/|D| 就是分支权重:|D₁|/|D|=4/14、|D₂|/|D|=10/14

• 代入:

Gain(14, Outlook) = Gini(14) − [4/14·Gini(4) + 10/14·Gini(10)]

  • 计算基尼指数Gini(14) Gini(14天)、Gini(D₁)、Gini(D₂)

算Gini(14天) —— 整个根节点的不纯度

先数根节点 14 天的标签:不打(no) = 5天,打(yes) = 9天

Gini(14天) = 1 − (5/14)² − (9/14)²

     = 1 − 25/196 − 81/196

     = 1 − 106/196

     = 90/196

     = 0.4592 ≈ 0.46

算Gini(D₁) —— 左分支(Outlook ≤ 0.5 = 阴天,4 天)

左分支 4 天全是 overcast,逐行核对标签:第 3/7/12/13 行全是 yes,不打(no) = 0 天,打(yes) = 4 天

Gini(D₁) = 1 − (0/4)² − (4/4)²

     = 1 − 0 – 1

     = 0.000

因为 4 天全是同一类,已经"纯"了,不纯度自然为 0。

算Gini(D₂) —— 右分支(Outlook > 0.5 = 雨+晴,10 天)

右分支是剩下 10 天,逐行数标签:

  • 不打(no):第 1, 2, 6, 8, 14 行 → 5 天
  • 打(yes):第 4, 5, 9, 10, 11 行 → 5 天

Gini(D₂) = 1 − (5/10)² − (5/10)²

     = 1 − 0.25 − 0.25

     = 0.500

这是二分类里最不纯的情况(两类各占一半,基尼上界就是 0.5)。

回代到增益公式

Gain = 0.4592 − [ 4/14 × 0.000 + 10/14 × 0.500 ]

   = 0.4592 − [ 0 + 0.3571 ]

   = 0.1021 ≈ 0.102

一句话总结三者的算法步骤:

  1. 数子集里 no / yes 各多少条(n_no, n_yes);
  2. 算占比 p_no = n_no/n,p_yes = n_yes/n;
  3. 代入公式 Gini = 1 − p_no² − p_yes²。

唯一的差别就是“代入哪堆样本”——根节点用全部14天,D₁用4天阴天,D₂用10天雨+晴。公式一模一样,只是分母和分子变了。

4.2.2 左分支与右分支

问题:

为什么是 左分支(Outlook ≤ 0.5 = 阴天,4 天)而不是 右分支(Outlook > 0.5 = rain+sunny(雨+晴),10 天)

这是 sklearn 把决策树结构“写死”的一个约定,不是我们随便给分支起名。关键在于:

CART 二叉切分时,条件的方向和左右分支是绑定的:

  • feature ≤ threshold → 永远进入 左子节点(Left
  • feature > threshold → 永远进入 右子节点(Right

sklearn 的DecisionTreeClassifier模型在内存里每行节点存了两个指针:children_left(左孩子)和children_right(右孩子)。在本例中落在≤ 0.5的样本被路由到children_left,落在> 0.5的被路由到children_right。

所以根节点Outlook ≤ 0.5这一刀,天然分出:

  • 左分支 = 条件成立的样本 = Outlook ≤ 0.5 = overcast(阴天)4天
  • 右分支 = 条件不成立的样本 = Outlook > 0.5 = rain+sunny(雨+晴)10天

一句话:左/右不是我们挑的,是 sklearn 的结构约定决定的:“≤阈值”必走左,“大于阈值”必走右。

4.3 阈值从何而来:Outlook ≤ 0.5 如何计算出?

理解了如何挑选特征后,进一步思考:选定特征后,划分阈值怎么确定?

4.3.1 计算方法

约定1sklearn的DecisionTreeClassifier模型仅支持数值输入

文本类别特征需要通过编码转为整数;本案例使用 LabelEncoder(按字母顺序编码),映射关系如下:

表2  特征 – 编码映射表

特征

编码(整数)

Outlook

overcast=0, rain=1, sunny=2

Temperature

cool=0, hot=1, mild=2

Humidity

high=0, normal=1

Wind

strong=0, weak=1

表3  标签 – 编码映射表

标签

编码(整数)

play

no=0, yes=1

对表1编码后得到表4:

表4  编码后的训练数据(14 天)

Outlook

Temperature

Humidity

Wind

Play

sunny=2

hot=1

high=0

weak=1

no=0

sunny=2

hot=1

high=0

strong=0

no=0

overcast=0

hot=1

high=0

weak=1

yes=1

rain=1

mild=2

high=0

weak=1

yes=1

rain=1

cool=0

normal=1

weak=1

yes=1

rain=1

cool=0

normal=1

strong=0

no=0

overcast=0

cool=0

normal=1

strong=0

yes=1

sunny=2

mild=2

high=0

weak=1

no=0

sunny=2

cool=0

normal=1

weak=1

yes=1

rain=1

mild=2

normal=1

weak=1

yes=1

sunny=2

mild=2

normal=1

strong=0

yes=1

overcast=0

mild=2

high=0

strong=0

yes=1

overcast=0

hot=1

normal=1

weak=1

yes=1

rain=1

mild=2

high=0

strong=0

no=0

表中“特征=整数”即该特征取值按表2的整数编码(例如:sunny=2、hot=1、high=0、weak=1),算法实际只认这些整数。同样,“标签=整数”即该标签取值按表3的整数编码(no=0、yes=1)。

约定 2:CART 只能进行二叉分裂

对每个数值特征,候选切分点 = 当前子集里相邻两个不同取值的中点:

vᵢ 和 vᵢ₊₁ 是当前子集里,某个特征“相邻两个不同取值”的编码值(按从小到大排好序后,第 i 个和第 i+1 个)。

先说 v 是什么:

这里的 v 指的是特征被编码后的整数取值。以 Outlook 为例,表2里 overcast=0, rain=1, sunny=2,那 Outlook 在当前节点出现过的不同取值就是集合 {0, 1, 2}。把它从小到大排成一列:

v₁ = 0(overcast)

v₂ = 1(rain)

v₃ = 2(sunny)

vᵢ 就是这一列里第 i 个数,vᵢ₊₁ 是紧跟在它后面的那个数(第i+1个)。

4.3.2 计算候选阈值

把相邻两个取中点,每对相邻取值产出一个候选阈值:

表5  候选阈值表(以 Outlook 为例)

相邻对 (v, v₊₁)

候选阈值 = (v + v₊₁)/2

这一刀把谁隔开

(0, 1)

(0+1)/2 = 0.5

overcast(0) ←/→ rain+sunny

(1, 2)

(1+2)/2 = 1.5

overcast+rain ←/→ sunny

所以特征 Outlook 在根节点有 2 个候选阈值:0.5 和 1.5

其他特征同理:

 • Humidity {high=0, normal=1} → 候选阈值 (0,1) → 0.5

 • Wind {strong=0, weak=1} → 候选阈值 (0,1) → 0.5

 • Temperature {cool=0, hot=1, mild=2} → 候选阈值 (0,1)→0.5,(1,2)→1.5

一句话:vᵢ 和 vᵢ₊₁ 是某特征在子集里排序后的相邻两个不同编码值,二者取中点当作候选切分阈值;候选阈值的个数 = 特征编码个数 – 1。

4.3.3 从候选阈值中选择阈值

以根节点为例,对每个特征的每个候选阈值都算一遍基尼增益,选增益最大的特征阈值作为本节点的切分。

对每个候选阈值,计算“基尼增益“:

表6  基尼增益

候选切分

左子集(n, Gini)

右子集(n, Gini)

增益Gain

Outlook ≤ 0.5

4, 0.000(全是打球)

10, 0.500

0.1020 ✅

Outlook ≤ 1.5

9, 0.346

5, 0.480

0.0655

Humidity ≤ 0.5

7, 0.490

7, 0.245

0.0918

Wind ≤ 0.5

6, 0.500

8, 0.375

0.0306

Temperature ≤ 0.5

4, 0.375

10, 0.480

0.0092

Temperature ≤ 1.5

8, 0.469

6, 0.444

0.0009

注:n是样本数,Gini是基尼指数。

取增益最大的那个:

最大增益 = Outlook ≤ 0.5(增益 0.1020 → 所以它被算法选作根节点的切分。

5. 手把手算一遍:是否打网球

我们用 14 天的历史数据来演示。特征有 4 个:天气(Outlook)、温度(Temperature)、湿度(Humidity)、风(Wind);标签是“是否打球(Play)”。

5.1 第一步:算总基尼指数

14天里,打球(yes)=9天,不打(no)=5天。

Gini(14) = 1 − (9/14)² − (5/14)² = 0.459 ≈ 0.46

5.2 第二步:算每个特征的基尼增益

分别按天气、温度、湿度、风做切分,再算加权基尼与增益:表6

比较下来,Outlook 的增益最大,所以根节点先问“Outlook”这个特征。

(数值由本文后面6.3 小节代码精确计算;可见天气 Outlook 把数据分得最干净,这也是下面树图的根节点。)

5.3 第三步:逐层计算表:每个切分节点都重新算一遍

上一层的样本被切走后,下一层只能在剩下的子集上重新评估所有特征。

5.3.1 节点编号:sklearn 如何给节点编号

建树结束后,sklearn 会把每个节点存进一个一维数组,并给每个节点一个整数下标(id)。这个下标不是按“我们手算的先后”连续编的,而是由树的 深度优先前序(DFS pre-order)遍历自动分配的:根节点永远是0,先把整条左子树编完,再编右子树。因此叶子节点会“插空”占掉一些编号,内部切分节点的编号就会出现间隔(例如0之后直接是2,因为1是左子树里的叶子)。

以本文这棵决策树为例(6.3 小节代码里的 clf.tree_ 就能直接看到):整棵树共有13个节点(node_count = 13),其中6个是内部切分节点,下标为 0、2、3、4、8、9;其余7个是叶子节点,下标为 1、5、6、7、10、11、12。下面这张表给出每个节点的左/右孩子(−1 表示叶子):

表7  节点拓扑表(共 13 个节点,−1 = 叶子)

节点

样本数 n

左孩子

右孩子

说明(切分 / 叶子)

0

14

1

2

内部:Outlook≤0.5

1

4

−1

−1

叶子

2

10

3

8

内部:Humidity≤0.5

3

5

4

7(叶子)

内部:Outlook≤1.5

4

2

5(叶子)

6(叶子)

内部:Wind≤0.5

5

1

−1

−1

叶子

6

1

−1

−1

叶子

7

3

−1

−1

叶子

8

5

9

12(叶子)

内部:Wind≤0.5

9

2

10(叶子)

11(叶子)

内部:Temperature≤1

10

1

−1

−1

叶子

11

1

−1

−1

叶子

12

3

−1

−1

叶子

可以看到,下面的表8中6个切分节点的“真实编号”正是 0、2、3、4、8、9,中间空掉的编号全是叶子。这也解释了6.3小节代码为什么打印“节点 0、2、3、4、8、9”而不是连续的编号,代码里用 sklearn 的叶子标记(feature == −2,即 TREE_UNDEFINED)把叶子跳过,剩下的正好是这几个有间隔的内部节点下标。所以表8每行末尾的序号应当写 sklearn 的真实节点下标,下面表8中的节点编号已按 0/2/3/4/8/9 列出,与 6.3 代码一一对应。

5.3.2 逐层增益计算确定选定特征

表8把整棵树6个切分节点的候选基尼增益都列出来,看“增益最大的那个”被选走的过程。表8中以**标记的就是该节点最终选中的特征及其增益。

读法:每一行代表决策树里的一个切分节点;O/T/H/W 四列分别是该子集上 Outlook / Temperature / Humidity / Wind 的基尼增益;最后一列是实际选中的特征和切分阈值。

表8  逐层增益计算确定选定特征

切分节点(路径)

子集 n

候选 GiniGainO/T/H/W**=选中)

选中特征(阈值,节点编号)

根节点 [全量14条数据]

14

**0.102 / 0.009 / 0.092 / 0.031

Outlook ≤ 0.5 (节点 0)

→ Outlook > 0.5rain/sunny   [10 ]

10

0.020 / 0.024 / **0.180 / 0.083

Humidity ≤ 0.5 (节点 2

根 → Outlook > 0.5(rain/sunny) → Humidity ≤ 0.5(high)   [5 条]

5

**0.120 / 0.053 / 0.000 / 0.053

Outlook ≤ 1.5 (节点 3)

→ Outlook > 0.5rain/sunny → Humidity ≤ 0.5high → Outlook ≤ 1.5rain   [2 ]

2

0.000 / 0.000 / 0.000 / **0.500

Wind ≤ 0.5 (节点 4

根 → Outlook > 0.5(rain/sunny) → Humidity > 0.5(normal)   [5 条]

5

0.053 / 0.053 / 0.000 / **0.120

Wind ≤ 0.5 (节点 8)

→ Outlook > 0.5rain/sunny → Humidity > 0.5normal → Wind ≤ 0.5strong   [2 ]

2

0.500 / **0.500 / 0.000 / 0.000

Temperature ≤ 1(节点 9

一句话记住:阈值 = 当前子集里相邻取值的中点;具体取哪个阈值、取哪个特征,由“在该子集上基尼增益最大”决定。子集变了,阈值就可能变。

看表要点:

  • 每一行都重新算一次 4 个特征的 GiniGain——这正是“逐层重算”的具体体现。
  • 同一特征可以在树里被反复使用:比如 Outlook 出现在根节点和“高湿度”子节点两处。
  • 切到子集仅剩1-2条数据时(最后两行),GiniGain直接拉到 0.5,意味着该节点问哪个问题都能立刻把数据分纯。

5.4 第四步:算法长出的树

5.4.1 由逐层计算表构造出决策树

图3  由 sklearn 训练得到的决策树(criterion="gini")

决策树图说明:

图中,一个框就是一个决策树节点(tree node)。框里那一列字段,在scikit-learn库中叫做这个节点的标注信息(node annotation),也可以理解为节点摘要/节点属性——它描述的是“落到这个节点的那批样本”的统计情况。

  • 内部节点(还能继续切分的,如根节点):标注里既有分裂条件,也有后面那几项gini/samples/value/class。
  • 叶子节点(不再切分):标注里没有分裂条件,只剩gini/samples/value/class。

关键观察:

天气 Outlook 这个特征在树里被用了两次——第一次把“阴天”切走,第二次把“雨 / 晴”分开;湿度 Humidity 也类似。这正是 CART 用“二叉分裂”处理“多值类别”特征的典型手法:每次只能把取值切成两段,要彻底分干净就可能反复使用同一特征。

5.4.2 标注信息说明

以根节点0为例:

  • 分裂条件Outlook <= 0.5 —— 这个节点“问的问题”和切分阈值。
  • 基尼指数gini = 0.46 —— 当前子集的整体不纯度(基尼指数)。
  • 样本数samples = 14 —— 落到这个节点的样本条数。
  • 类别统计value = [5, 9] —— 各类别的样本计数,顺序 = 标签的编码顺序:编码 0 = 不打(no)、编码 1 = 打球(yes),所以 [5, 9] 表示“5 天不打、9 天打”。
  • 多数类class = 打球(yes) —— 该节点的多数类预测:9 > 5,预测为打球。
5.4.3 sklearn怎样通过类别统计value决定多数类class
  • 标签Play按字母顺序编码:
    • no(不打球)= 0
    • yes(打球)= 1
  • 非平局确定class

value=[5, 9]是根节点上14条训练样本中各类别的数量统计。按照CART文档的编码规则, value=[5, 9] 数组的顺序按“标签编码”顺序(no=0在前、yes=1在后,由 LabelEncoder 按字母序决定)。即:

    • value[0] = 5(条数据不打球no)
    • value[1] = 9(条数据打球yes)

sklearn对节点确定class的定义是:class = classes_[np.argmax(value)],也就是“value数组中计数最大的那个类的标签”。对于value=[5, 9]:

np.argmax([5, 9]) = 1

所以:class = classes_[1] = yes(打球)

  • 平局确定class

sklearn 在平局时按“下标最小”确定class:当value中的元素相等时,例如节点2,value=[5,5],value的两个元素都是最大值,这时,np.argmax(value)返回数组第一个最大值value[0]的下标0:

np.argmax([5, 5]) = 0

对应的class = classes_[0] = no(不打球)

5.5 第五步:特征重要性

5.5.1 基尼增益和重要性的关系

基尼增益和重要性不是两个算法,是同一个量(基尼增益)在两个尺度的用法。

  • 基尼增益 = 局部、单次分裂的决策依据(用来“建树”)
  • 每个节点上,我们比较“用哪个特征、哪个阈值切,能最大程度降低不纯度”。选出来那个最大的增益,决定这一刀怎么切。它是“点对点”的——只看当前节点,不管整棵树。
  • 特征重要性 = 全局、按特征的汇总(用来“看懂这棵树”)
  • 树建好后,有人会自然追问:“总体而言,哪个天气因素对'打不打球'影响最大?”单看某一刀的增益答不了这个问题。必须把所有用到该特征的分裂的增益,按样本量加权后加起来,再归一化(让四个特征的重要性之和=1)。
  • 这正是 sklearn 的 feature_importances_——它底层就是"各节点基尼增益 × 该节点样本占比,按特征累加,归一化"。

所以“重要性”不是新概念,是新视角:

 • 数学上:重要性 = 基尼增益的跨节点累加归一化;

 • 命名上:sklearn 把这个"汇总值"叫做 importance,第 6 章代码打印的也正是 feature_importances_,必须把理论和这个输出对应起来。

一句话区分:基尼增益是“建树时每个节点的局部判据”(只管当前这一刀怎么切);特征重要性是“整棵树建完后,对全树的全局汇总”(把所有用到该特征的分裂的增益加总)。两者数学上完全一致,只是看问题的尺度不同——单点 vs 全局。

思路只有两步:

  • 每一次分裂都会“降一点不纯度”,降的量正好就是表8里的基尼增益 Gain。每次降的量,按“经过该分裂的样本占全部样本的比例”加权——样本越多的分裂,贡献越大。
  • 把同一特征在所有分裂里的贡献加起来,再让四个特征的总贡献归一化成 1,就得到每个特征的重要性。
5.5.2 计算

计算公式:

重要性(特征 a) = (1 / 总样本数) × Σ[ 该节点样本数 × 该节点基尼增益 ](只累加“用特征 a 切分”的那些节点),最后让四个特征之和 = 1。

下面把本数据集真实决策树里 6 个内部节点的贡献逐行列出(总样本 14 天,所以加权 = 节点样本数 ÷ 14 × 本次增益):

表9  各内部节点贡献明细(加权 = 节点样本数 ÷ 14 × 本次增益)

分裂发生在哪里

用哪个特征切

节点样本 n

本次基尼增益

加权贡献 (n/14 × 增益)

根节点(全部 14 天)

Outlook

14

0.102

0.1020

右子树·高湿度子集

Humidity

10

0.180

0.1286

高湿度·雨天那支

Outlook

5

0.120

0.0429

高湿度·雨天·强风那支

Wind

2

0.500

0.0714

正常湿度子集

Wind

5

0.120

0.0429

正常湿度·强风那支

Temperature

2

0.500

0.0714

把“加权贡献”按特征加总,再除以总和 0.4592(恰好等于根节点的 Gini),就得到第 6 章代码打印的那组数:

表10  特征重要性归一化结果

特征

累计加权贡献

归一化重要性

Outlook(天气)

0.1020 + 0.0429 = 0.1449

0.1449 / 0.4592 = 0.316

Humidity(湿度)

0.1286

0.1286 / 0.4592 = 0.280

Wind(风)

0.0714 + 0.0429 = 0.1143

0.1143 / 0.4592 = 0.249

Temperature(温度)

0.0714

0.0714 / 0.4592 = 0.156

所以“特征重要性”和前面讲的基尼增益是同一套逻辑:天气 Outlook 在根节点就砍掉一大块不纯度(增益 0.102),又在高湿度子集再砍一次(0.0429),累计最高 → 重要性 0.316 居首;温度 Temperature 只在最后“强风+正常湿度”那一支露过一次脸,贡献最小 → 0.156 垫底。第 6 章代码里的 clf.feature_importances_ 打印出来的,正是这一节手算的结果。

6. Python 实战:5 分钟跑通一棵决策树

下面用最流行的机器学习库 scikit-learn 复现上面的过程。代码可直接复制运行。

6.1 准备数据并训练

6.1.1 代码

data_training.py

import pandas as pd
from sklearn.tree import DecisionTreeClassifier, plot_tree
from sklearn.preprocessing import LabelEncoder
import matplotlib

# 自适应后端:有 tkinter(完整 Python)则弹窗,否则(精简 venv)保存为图片
try:
    import tkinter  # noqa: F401
    matplotlib.use("TkAgg")
    _SHOW = True
except ImportError:
    matplotlib.use("Agg")
    _SHOW = False
import matplotlib.pyplot as plt
# 解决中文乱码:指定中文字体 + 正常显示负号
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'Arial Unicode MS']
plt.rcParams['axes.unicode_minus'] = False

# 1) 准备数据(类别特征先编码成数字)
columns = ["Outlook", "Temperature", "Humidity", "Wind", "Play"]
rows = [
    ("sunny","hot","high","weak","no"), ("sunny","hot","high","strong","no"),
    ("overcast","hot","high","weak","yes"), ("rain","mild","high","weak","yes"),
    ("rain","cool","normal","weak","yes"), ("rain","cool","normal","strong","no"),
    ("overcast","cool","normal","strong","yes"), ("sunny","mild","high","weak","no"),
    ("sunny","cool","normal","weak","yes"), ("rain","mild","normal","weak","yes"),
    ("sunny","mild","normal","strong","yes"), ("overcast","mild","high","strong","yes"),
    ("overcast","hot","normal","weak","yes"), ("rain","mild","high","strong","no"),
]
df = pd.DataFrame(rows, columns=columns)

X, y = df[columns[:-1]], df["Play"]
enc = {c: LabelEncoder().fit(X[c]) for c in X.columns}
X_enc = X.apply(lambda col: enc[col.name].fit_transform(col))
y_enc = LabelEncoder().fit_transform(y)

# 2) 训练 CART 决策树
#    算法 = CART:二叉分裂、自上而下贪心生长(sklearn 的 DecisionTreeClassifier 就是 CART 实现)。
#    criterion="gini" 是 CART 默认的评分公式——用“基尼指数”给每个节点选最佳切分特征。
#    本文全文统一使用基尼,不再切换其他准则。
clf = DecisionTreeClassifier(criterion="gini", random_state=42)
clf.fit(X_enc, y_enc)
print("训练完成!")

# 3) 可视化决策树
plt.figure(figsize=(11, 6))
plot_tree(clf,
          feature_names=list(X.columns),
          class_names=["不打(no)", "打(yes)"],
          filled=True, rounded=True)
# 有 tkinter 弹窗显示,否则保存为图片文件
if _SHOW:
    plt.show()
else:
    plt.savefig("decision_tree.png", dpi=150, bbox_inches="tight")
    print("已保存 decision_tree.png")
6.1.2 运行结果

图4  由 6.1 节代码训练得到的决策树(运行结果)

6.2 做预测 & 看特征重要性

6.2.1 特征重要性
  • 代码

将下面的代码添加到data_training.py后面

# 4) 预测:晴天、凉爽、湿度正常、风弱  → 应该打球
sample = [[enc["Outlook"].transform(["sunny"])[0],
           enc["Temperature"].transform(["cool"])[0],
           enc["Humidity"].transform(["normal"])[0],
           enc["Wind"].transform(["weak"])[0]]]
print("预测是否打球:", "yes" if clf.predict(sample)[0] == 1 else "no")

# 各特征的重要程度(CART 按“不纯度下降量”累计)
for name, imp in zip(X.columns, clf.feature_importances_):
    print(f"{name:12s} 重要性 = {imp:.3f}")
  • 运行结果

# 各特征的重要程度

预测是否打球: yes

Outlook      重要性 = 0.316

Temperature  重要性 = 0.156

Humidity     重要性 = 0.280

Wind         重要性 = 0.249

6.2.2 图示特征重要性

数字看不直观?画张图。让 matplotlib 按"重要性从大到小"排个序,柱长 = 该特征的不纯度下降量累计。看一眼就明白:Outlook 最能区分打不打球,Temperature 最弱。

  • 代码

将下面的代码添加到data_training.py后面

# 5) 可视化特征重要程度
import matplotlib.pyplot as plt
import numpy as np

imp   = clf.feature_importances_            # 与 6.2.1 print 出来的同一组数
names = list(X.columns)                     # 特征中文名
order = np.argsort(imp)                     # 从小到大 → 画图时倒序即"大→小"

# (不要在这里重置 font.sans-serif,前面已经设好 SimHei)

fig, ax = plt.subplots(figsize=(7.5, 4.2))
bars = ax.barh([names[i] for i in order], [imp[i] for i in order], color='#2A7A8C')
for b, i in zip(bars, order):
    ax.text(b.get_width() + 0.01, b.get_y() + b.get_height()/2,
            f'{imp[i]:.2f}', va='center', fontsize=9)
ax.set_xlabel('特征重要性(不纯度下降量)')
ax.set_title('决策树认为哪些特征最重要(CART / 基尼)', fontsize=13, color='#2A7A8C', weight='bold')
ax.set_xlim(0, max(imp) + 0.08)
fig.tight_layout()
plt.savefig('importance_cart.png', dpi=150, bbox_inches='tight')
plt.show()
  • 运行结果

运行后即可得到下面这张特征重要性条形图(与 6.2.1 打印的数字一一对应):

图5  决策树认为哪些特征最重要(CART / 基尼)

6.3 自己实现:基尼指数与基尼增益(加深理解)

下面的代码能算出和文档里一致的增益数值:

Gini_Index_Gain.py

import numpy as np
import pandas as pd
from collections import Counter
from sklearn.preprocessing import LabelEncoder
from sklearn.tree import DecisionTreeClassifier

# ============ 0. 数据(14 天打网球) ============
columns = ["Outlook", "Temperature", "Humidity", "Wind", "Play"]
rows = [
    ("sunny",    "hot",   "high",   "weak",   "no"),
    ("sunny",    "hot",   "high",   "strong", "no"),
    ("overcast", "hot",   "high",   "weak",   "yes"),
    ("rain",    "mild",  "high",   "weak",   "yes"),
    ("rain",    "cool",  "normal", "weak",   "yes"),
    ("rain",    "cool",  "normal", "strong", "no"),
    ("overcast", "cool",  "normal", "strong", "yes"),
    ("sunny",    "mild",  "high",   "weak",   "no"),
    ("sunny",    "cool",  "normal", "weak",   "yes"),
    ("rain",    "mild",  "normal", "weak",   "yes"),
    ("sunny",    "mild",  "normal", "strong", "yes"),
    ("overcast", "mild",  "high",   "strong", "yes"),
    ("overcast", "hot",   "normal", "weak",   "yes"),
    ("rain",    "mild",  "high",   "strong", "no"),
]
df = pd.DataFrame(rows, columns=columns)
features = ["Outlook", "Temperature", "Humidity", "Wind"]

# 编码(LabelEncoder 字母序,与文档一致:no=0/yes=1,各特征也按字母序编码)
encoders = {c: LabelEncoder().fit(df[c]) for c in features}
X_enc = pd.DataFrame({c: encoders[c].transform(df[c]) for c in features})
y_enc = pd.Series(LabelEncoder().fit_transform(df["Play"]), name="Play")  # no=0, yes=1

# ============ 1. 自己实现:基尼指数 ============
def gini(labels):
    """Gini(D) = 1 - Σ (p_i)^2,其中 p_i = 第 i 类样本数 / 总样本数。"""
    n = len(labels)
    if n == 0:
        return 0.0
    return 1.0 - sum((c / n) ** 2 for c in Counter(labels).values())

# ============ 2. 自己实现:基尼增益(CART 二叉阈值) ============
def best_gini_gain(X_sub, y_sub, feature):
    """
    对“当前子集”上的某个特征:
    枚举该子集实际出现的相邻编码值的中点作候选阈值,
    对每一个候选阈值算 加权基尼 = Σ |D_v|/|D|·Gini(D_v),
    增益 Gain = Gini(D) - 加权基尼,返回其中最大的那个增益。
    (只看当前子集里的取值,与特征全集无关 —— 这正是文档 4.3 的写法)
    """
    total = len(y_sub)
    G0 = gini(y_sub.tolist())
    vals = sorted(X_sub[feature].unique())
    best = 0.0
    for i in range(len(vals) - 1):
        thr = (vals[i] + vals[i + 1]) / 2.0          # 候选阈值 = 相邻编码值中点
        lm = X_sub[feature] <= thr
        w = (len(y_sub[lm]) / total) * gini(y_sub[lm].tolist()) \
          + (len(y_sub[~lm]) / total) * gini(y_sub[~lm].tolist())
        g = G0 - w
        if g > best:
            best = g
    return best

# ============ 3. 与“前面计算”对比 ============
print("=" * 60)
print("【5.1】总基尼指数 Gini(D)")
G_D = gini(y_enc.tolist())
print(f"  Gini(D) = 1 - (9/14)^2 - (5/14)^2 = {G_D:.3f}    文档 5.1:≈ 0.459")

print("\n【表8 根节点行】根节点各特征基尼增益(二叉阈值,取最大)")
root_gains = {}
for f in X_enc.columns:
    g = best_gini_gain(X_enc, y_enc, f)
    root_gains[f] = g
    print(f"  {f:12s} Gain = {g:.3f}")
best_feat = max(root_gains, key=root_gains.get)
print(f"  -> 最大是 {best_feat}(增益 {root_gains[best_feat]:.3f}),根节点先问它。文档 表8 根节点行:Outlook 0.102 ✅")

# ============ 4. 逐节点候选增益(与文档 5.3 表(表8)一一对应) ============
# 用 sklearn 还原文档里那棵真实树,仅取“节点子集划分”;增益全部由上面函数算。
clf = DecisionTreeClassifier(random_state=42, criterion="gini")
clf.fit(X_enc, y_enc)
tree_ = clf.tree_
feat_ = tree_.feature
thr_ = tree_.threshold
left = tree_.children_left
right = tree_.children_right
dp = clf.decision_path(X_enc).toarray()
feat_names = list(X_enc.columns)

print("\n【5.3】逐节点候选 GiniGain(O/T/H/W),**加粗** = 该节点实际选中的特征")
chosen_gains = []   # 收集每个切分节点的“实际选中增益”,用于一致性校验
for u in range(tree_.node_count):
    if feat_[u] == -2:          # 叶子,跳过
        continue
    mask = dp[:, u] > 0
    subX = X_enc[mask]
    suby = y_enc[mask]
    gs = {fn: best_gini_gain(subX, suby, fn) for fn in feat_names}
    chosen = feat_names[feat_[u]]
    t = float(thr_[u])
    chosen_gains.append(round(gs[chosen], 3))
    o, tp, h, w = gs["Outlook"], gs["Temperature"], gs["Humidity"], gs["Wind"]

    def fmt(v, name):
        return f"**{v:.3f}**" if name == chosen else f"{v:.3f}"
    print(f"  节点{u:>2d}  n={int(mask.sum()):>2d} | "
          f"O {fmt(o,'Outlook')}  T {fmt(tp,'Temperature')}  "
          f"H {fmt(h,'Humidity')}  W {fmt(w,'Wind')}   -> 选 {chosen} ≤ {t:g}")

# ============ 5. 一致性校验 ============
print("\n【校验】把所有切分节点的“实际选中增益”收集起来,应等于文档 5.5.2 手算值:")
expected = [0.102, 0.180, 0.120, 0.500, 0.120, 0.500]   # 6 个切分节点(含重复),见文档 5.5.2 手算值
print(f"  期望(文档 5.5.2 手算值,按节点顺序=表9) = {[f'{v:.3f}' for v in expected]}")
print(f"  实际(代码逐节点收集,按节点顺序=表9) = {[f'{v:.3f}' for v in chosen_gains]}")
ok = (sorted(round(v, 3) for v in chosen_gains) == sorted(round(v, 3) for v in expected))
print("  结果:", "✅ 与文档前面计算完全一致" if ok else "❌ 不一致,需检查")
6.3.1 运行结果

============================================================

【5.1】总基尼指数 Gini(D)

  Gini(D) = 1 - (9/14)^2 - (5/14)^2 = 0.459    文档 5.1:≈ 0.459

【表8 根节点行】根节点各特征基尼增益(二叉阈值,取最大)

  Outlook      Gain = 0.102

  Temperature  Gain = 0.009

  Humidity     Gain = 0.092

  Wind         Gain = 0.031

  -> 最大是 Outlook(增益 0.102),根节点先问它。文档 表8 根节点行:Outlook 0.102 ✅

【5.3】逐节点候选 GiniGain(O/T/H/W),**加粗** = 该节点实际选中的特征

  节点 0  n=14 | O **0.102**  T 0.009  H 0.092  W 0.031   -> 选 Outlook ≤ 0.5

  节点 2  n=10 | O 0.020  T 0.024  H **0.180**  W 0.083   -> 选 Humidity ≤ 0.5

  节点 3  n= 5 | O **0.120**  T 0.053  H 0.000  W 0.053   -> 选 Outlook ≤ 1.5

  节点 4  n= 2 | O 0.000  T 0.000  H 0.000  W **0.500**   -> 选 Wind ≤ 0.5

  节点 8  n= 5 | O 0.053  T 0.053  H 0.000  W **0.120**   -> 选 Wind ≤ 0.5

  节点 9  n= 2 | O 0.500  T **0.500**  H 0.000  W 0.000   -> 选 Temperature ≤ 1

【校验】把所有切分节点的“实际选中增益”收集起来,应等于文档 5.5.2 手算值:

  期望(文档 5.5.2 手算值,按节点顺序=表9) = ['0.102', '0.180', '0.120', '0.500', '0.120', '0.500']

  实际(代码逐节点收集,按节点顺序=表9) = ['0.102', '0.180', '0.120', '0.500', '0.120', '0.500']

  结果: ✅ 与文档前面计算完全一致

7. 别忘了:防止“死记硬背”——剪枝

如果让树无限生长,它会把训练数据记到 100% 正确,但遇到新数据就傻眼——这叫“过拟合”。CART 常用的解决办法叫“剪枝”,主要有两种:

7.1 预剪枝

训练时提前设限,比如 max_depth(最大深度)、min_samples_split(最少样本数)、min_samples_leaf(叶最少样本)。简单好用,是默认首选。

预剪枝示例:

# 限制最大深度为 3、叶子至少 3 个样本
clf_pre = DecisionTreeClassifier(criterion="gini",
                             max_depth=3,
                             min_samples_leaf=3,
                             random_state=42)
clf_pre.fit(X_enc, y_enc)

7.2后剪枝

先让树长到底,再自底向上把“用处不大”的分支剪掉(CART 的成本复杂度剪枝 CCP)。效果更好但更慢。

后剪枝(代价复杂度剪枝)示例:

# 后剪枝:基于上面 6.1 节训练好的完整树 clf,用 cost_complexity_pruning_path 找到一系列 alpha,挑一个剪枝
path = clf.cost_complexity_pruning_path(X_enc, y_enc)
ccp_alphas = path.ccp_alphas
# 用交叉验证选最佳 alpha,再训练一棵剪过枝的树
from sklearn.model_selection import GridSearchCV
clf_pruned = GridSearchCV(
    DecisionTreeClassifier(criterion="gini", random_state=42),
    {"ccp_alpha": ccp_alphas}, cv=5
).fit(X_enc, y_enc)
print("最佳 ccp_alpha =", clf_pruned.best_params_["ccp_alpha"])

7.3 完整示例

7.3.1 代码

pruning.py

import pandas as pd
from sklearn.tree import DecisionTreeClassifier
from sklearn.preprocessing import LabelEncoder

# ===== 1) 准备数据(类别特征先编码成数字)=====
columns = ["Outlook", "Temperature", "Humidity", "Wind", "Play"]
rows = [
    ("sunny","hot","high","weak","no"), ("sunny","hot","high","strong","no"),
    ("overcast","hot","high","weak","yes"), ("rain","mild","high","weak","yes"),
    ("rain","cool","normal","weak","yes"), ("rain","cool","normal","strong","no"),
    ("overcast","cool","normal","strong","yes"), ("sunny","mild","high","weak","no"),
    ("sunny","cool","normal","weak","yes"), ("rain","mild","normal","weak","yes"),
    ("sunny","mild","normal","strong","yes"), ("overcast","mild","high","strong","yes"),
    ("overcast","hot","normal","weak","yes"), ("rain","mild","high","strong","no"),
]
df = pd.DataFrame(rows, columns=columns)
X, y = df[columns[:-1]], df["Play"]
enc = {c: LabelEncoder().fit(X[c]) for c in X.columns}
X_enc = X.apply(lambda col: enc[col.name].fit_transform(col))
y_enc = LabelEncoder().fit_transform(y)

# ===== 0) 先训练一个完整树(默认参数,不加限制)——文档 6.1 节的 clf =====
clf = DecisionTreeClassifier(criterion="gini", random_state=42)
clf.fit(X_enc, y_enc)
print("【完整树(默认参数)】深度=%d, 叶子数=%d" % (clf.get_depth(), clf.get_n_leaves()))

# ===== 第7章 预剪枝示例(原文)=====
clf_pre = DecisionTreeClassifier(criterion="gini",
                                 max_depth=3,
                                 min_samples_leaf=3,
                                 random_state=42)
clf_pre.fit(X_enc, y_enc)
print("\n【预剪枝】max_depth=3, min_samples_leaf=3")
print("  深度=%d, 叶子数=%d" % (clf_pre.get_depth(), clf_pre.get_n_leaves()))

# ===== 第7章 后剪枝(代价复杂度剪枝)示例(原文,基于完整树 clf)=====
path = clf.cost_complexity_pruning_path(X_enc, y_enc)
ccp_alphas = path.ccp_alphas
from sklearn.model_selection import GridSearchCV
clf_pruned = GridSearchCV(
    DecisionTreeClassifier(criterion="gini", random_state=42),
    {"ccp_alpha": ccp_alphas}, cv=5
).fit(X_enc, y_enc)
print("\n【后剪枝】ccp_alphas 数量=%d,前5个= %s" % (len(ccp_alphas), [round(a,4) for a in ccp_alphas[:5]]))
print("  最佳 ccp_alpha =", clf_pruned.best_params_["ccp_alpha"])
best = clf_pruned.best_estimator_
print("  剪枝后树:深度=%d, 叶子数=%d" % (best.get_depth(), best.get_n_leaves()))
7.3.2 运行结果

【完整树(默认参数)】深度=4, 叶子数=7

【预剪枝】max_depth=3, min_samples_leaf=3

  深度=2, 叶子数=3          ← 被限制住了

【后剪枝】ccp_alphas 数量=4,前5个= [np.float64(0.0), np.float64(0.0571), np.float64(0.0571), np.float64(0.1153)]

  最佳 ccp_alpha = 0.0

  剪枝后树:深度=4, 叶子数=7  ← 和完整树一样

7.3.3 结果解读
  • 预剪枝生效明显:默认树深 4、7 个叶子,加了 max_depth=3, min_samples_leaf=3 后被压到深 2、3 个叶子。
  • 后剪枝这里最佳 ccp_alpha=0.0,也就是"不剪反而最好"。原因是这个训练集只有 14 条样本、本身就很干净,交叉验证下完整树已经泛化良好,没有需要剪掉的分支。这是个很好的教学点——后剪枝不保证一定会剪掉东西,它只是“该剪才剪”。

8. 决策树(CART)的优缺点(速记)

8.1 优点

  • 可解释性强,树一画出来人就能看懂,适合给业务方“讲道理”。
  • 几乎不用做数据预处理(不强制归一化、能直接吃类别特征)。
  • 训练速度快,能天然处理非线性关系。

8.2 缺点

  • 容易过拟合,对小波动敏感(数据一变树就变)。
  • 单独一棵树的预测稳定性一般。

常用“集成”来弥补:把很多棵树组合起来——随机森林(Random Forest)、梯度提升树(XGBoost/LightGBM) 都是决策树的升级版,也是竞赛和工业界的主力。

9. 一句话小结

CART 决策树 = 用“基尼指数”衡量不纯度,贪心地选“最能降不纯度”的特征不断二叉切分,直到给出答案;它好懂、好用,但要靠剪枝和集成(随机森林等)来提升稳健性。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

晨之清风

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值