树模型从单棵树扩展到四类集成方法
基于树的方法通过将预测变量空间分割成若干简单区域来进行预测:
- 决策树:把复杂决策拆成一连串可解释的条件切分
- Bagging:通过自助法平均降低单棵树的高方差
- 随机森林:在 Bagging 基础上用特征子采样削弱树与树之间的同步性
- Boosting:顺序修正前一轮残差,把弱学习器累加成强学习器
- BART:在贝叶斯框架下用多棵小树表达函数并量化不确定性
核心思想:单棵树负责提供结构解释,集成方法负责把这种结构解释转化为更稳健的预测能力。
决策树为何如此受欢迎?
决策树之所以在商业和金融领域广泛应用,有其独特优势:
- 可解释性:决策过程直观透明,可以直接向管理层解释
- 处理非线性:自动捕捉变量间的交互效应和非线性关系
- 无需特征缩放:不像SVM、KNN等方法需要标准化
- 处理混合数据:同时处理数值型和类别型变量
但单棵树也有明显缺陷:预测精度较低、不够稳定
解决方案:集成学习——将多棵”弱”树组合为”强”模型
数据准备:A股上市公司财务数据
import numpy as np # 数值计算基础库
import pandas as pd # 表格数据处理库
import matplotlib.pyplot as plt # 数据可视化库
import os # 跨平台路径处理
plt.rcParams['font.family'] = ['Source Han Serif SC', 'Noto Serif CJK SC', 'SimSun'] # 配置中文字体优先级
plt.rcParams['axes.unicode_minus'] = False # 正确显示负号
# 根据操作系统选择本地数据根目录
DATA_DIR = 'C:/qiufei/data' if os.name == 'nt' else '/home/ubuntu/r2_data_mount/data'
financial_path = os.path.join(DATA_DIR, 'stock/financial_statement.h5') # 财务报表文件路径
financial_data = pd.read_hdf(financial_path) # 读取上市公司财务报表数据
# 基于杜邦分析框架构造核心财务比率
financial_data['Margin'] = financial_data['net_profit'] / financial_data['operating_revenue'] # 销售净利率
financial_data['Turnover'] = financial_data['operating_revenue'] / financial_data['total_assets'] # 资产周转率
financial_data['ROE'] = financial_data['net_profit'] / financial_data['equity_parent_company'] # 净资产收益率
financial_data['Leverage'] = financial_data['total_assets'] / financial_data['equity_parent_company'] # 权益乘数
financial_data['Log_Assets'] = np.log(financial_data['total_assets'] + 1) # 对数化总资产(规模效应)
# 清洗:替换inf为NaN,按分析列去除缺失值
analysis_cols = ['Margin', 'Turnover', 'ROE', 'Leverage', 'Log_Assets'] # 分析所需列
financial_data[analysis_cols] = financial_data[analysis_cols].replace([np.inf, -np.inf], np.nan) # inf替换为NaN
financial_data = financial_data.dropna(subset=analysis_cols) # 按分析列去除缺失值
# 过滤极端值以保证模型稳定性
financial_data = financial_data[
(financial_data['Margin'].between(-0.5, 0.5)) & # 净利率合理范围
(financial_data['Turnover'].between(0, 5)) & # 周转率合理范围
(financial_data['ROE'].between(-0.5, 0.5)) & # ROE合理范围
(financial_data['Leverage'].between(1, 10)) # 杠杆倍数合理范围
]
# 安全采样1000个样本用于后续演示
sampled_data = financial_data.sample(n=min(1000, len(financial_data)), random_state=42) # 随机抽样
print(f'样本量: {len(sampled_data)}, 特征: Margin, Turnover, ROE, Leverage, Log_Assets') # 输出数据概况
样本量: 1000, 特征: Margin, Turnover, ROE, Leverage, Log_Assets
递归分割的数学表述
回归树将特征空间划分为 \(J\) 个不重叠的区域 \(R_1, R_2, \ldots, R_J\):
\[
\hat{f}(x) = \sum_{j=1}^{J} \hat{c}_j \cdot \mathbf{1}(x \in R_j)
\]
其中 \(\hat{c}_j = \text{ave}(y_i \mid x_i \in R_j)\) 是区域 \(R_j\) 内响应变量的均值。
分割准则:在每一步选择变量 \(X_j\) 和分割点 \(s\),使得以下RSS最小:
\[
\sum_{i: x_i \in R_1(j,s)} (y_i - \hat{c}_1)^2 + \sum_{i: x_i \in R_2(j,s)} (y_i - \hat{c}_2)^2
\]
- \(R_1(j,s) = \{X \mid X_j < s\}\):左子节点
- \(R_2(j,s) = \{X \mid X_j \ge s\}\):右子节点
递归二元分割:贪婪的自上而下搜索
由于穷举所有分区在计算上不可行,我们采用递归二元分割:
- 自上而下:从所有数据开始,逐步分割
- 贪婪:每步选择当前最优分割,不向前看
对于特征 \(X_j\) 和切割点 \(s\),定义两个半平面:
\[
R_1(j, s) = \{X | X_j < s\}, \quad R_2(j, s) = \{X | X_j \geq s\}
\]
寻找使下式最小化的 \(j\) 和 \(s\):
\[
\sum_{i: x_i \in R_1} (y_i - \hat{y}_{R_1})^2 + \sum_{i: x_i \in R_2} (y_i - \hat{y}_{R_2})^2
\]
类比:就像玩”20个问题”游戏,每次问最能缩小答案范围的问题。
案例:回归树预测上市公司ROE
Code
from sklearn.tree import DecisionTreeRegressor, plot_tree # 回归树模型与可视化
features_matrix = sampled_data[['Margin', 'Turnover']] # 取两个杜邦因子作为特征
target_roe = sampled_data['ROE'] # 回归目标:净资产收益率
# 拟合深度为2的回归树(便于可视化和解释)
regression_tree = DecisionTreeRegressor(max_depth=2, random_state=42) # 初始化浅层回归树
regression_tree.fit(features_matrix, target_roe) # 训练模型
plt.figure(figsize=(12, 7)) # 设置画布大小
plot_tree(regression_tree, feature_names=['净利率', '周转率'], # 绘制树结构
filled=True, rounded=True, fontsize=10, impurity=False, precision=3) # 填色圆角、隐藏不纯度
plt.title('回归树: 基于杜邦分析预测ROE', fontsize=14) # 图表标题
plt.show() # 显示图表
树剪枝:防止过拟合的关键
完全生长的树容易过拟合训练数据。解决策略:成本复杂度剪枝(最弱链接剪枝)。
对每个调优参数 \(\alpha \geq 0\),寻找子树 \(T \subset T_0\) 使下式最小化:
\[
\sum_{m=1}^{|T|} \sum_{i: x_i \in R_m} (y_i - \hat{y}_{R_m})^2 + \alpha |T|
\]
- \(|T|\):终端节点数量(树的复杂度)
- \(\alpha = 0\):\(T = T_0\)(完全树,仅衡量训练误差)
- \(\alpha\) 增大:惩罚复杂树,倾向更简单的子树
| \(\alpha = 0\) |
不剪枝,等于完全树 |
| \(\alpha\) 适中 |
偏差-方差最佳平衡 |
| \(\alpha\) 很大 |
仅保留根节点 |
代价复杂度剪枝的数学原理
代价复杂度剪枝(Cost Complexity Pruning)的目标函数:
\[
C_\alpha(T) = \sum_{m=1}^{|T|} \sum_{i: x_i \in R_m} (y_i - \hat{c}_m)^2 + \alpha |T|
\]
- \(|T|\):树的叶节点数(复杂度度量)
- \(\alpha \ge 0\):调节参数(类似Lasso中的 \(\lambda\))
\(\alpha\) 的直觉理解:
| \(\alpha = 0\) |
不惩罚复杂度 → 全树 |
完全不正则化 |
| \(\alpha\) 小 |
轻微剪枝 |
弱正则化 |
| \(\alpha\) 大 |
重度剪枝 → 树桩 |
强正则化 |
| \(\alpha \to \infty\) |
只剩根节点 |
截距模型 |
选择最优 \(\alpha\):通过K折交叉验证选择使测试误差最小的 \(\alpha\)
读这个目标函数的顺序:
- 先看前半项:它衡量当前这棵树在训练样本上还剩多少拟合误差
- 再看后半项:\(\alpha |T|\) 表示’每多保留一个叶节点要付出的复杂度税’
- 最后看选择原则:\(\alpha\) 不是靠训练误差最小来定,而是靠交叉验证判断哪棵子树最能泛化
分类树与不纯度度量
分类树用于预测定性响应,关键区别在于分裂准则:
| 分类误差率 |
\(E = 1 - \max_k(\hat{p}_{mk})\) |
直观但不够敏感 |
| 基尼指数 |
\(G = \sum_{k=1}^{K} \hat{p}_{mk}(1-\hat{p}_{mk})\) |
节点纯度度量 |
| 熵 |
\(D = -\sum_{k=1}^{K} \hat{p}_{mk}\log\hat{p}_{mk}\) |
信息不确定性 |
其中 \(\hat{p}_{mk}\) 是第 \(m\) 节点中属于第 \(k\) 类的样本比例。
关键洞见:基尼指数是交叉熵的一阶近似(通过Taylor展开 \(\log p \approx p - 1\)),但计算更高效,因此是sklearn的默认选择。
实务判断顺序:
- 如果你只是汇报最终错分比例,分类错误率最直观
- 如果你正在决定’这一刀要不要切’,优先看基尼指数或交叉熵
- 当节点从 50/50 走向 80/20 时,后两者会更敏感地奖励这种纯度提升
三种不纯度度量的数学定义
设节点 \(m\) 中第 \(k\) 类的比例为 \(\hat{p}_{mk}\),三种不纯度度量:
分类错误率(Classification Error):
\[
E = 1 - \max_k(\hat{p}_{mk})
\]
基尼指数(Gini Index):
\[
G = \sum_{k=1}^{K} \hat{p}_{mk}(1-\hat{p}_{mk})
\]
交叉熵(Cross-Entropy):
\[
D = -\sum_{k=1}^{K} \hat{p}_{mk} \log(\hat{p}_{mk})
\]
为什么基尼指数和交叉熵优于分类错误率?
- 分类错误率对节点纯度不够敏感
- 基尼指数和交叉熵对纯度变化的梯度更大
- 用分类错误率可能错过改善纯度的有效分割
案例:分类树识别高盈利公司
Code
from sklearn.tree import DecisionTreeClassifier, plot_tree # 分类树模型与可视化
# 构造二分类目标:ROE>15%为高盈利
sampled_data_cls = sampled_data.copy() # 复制数据避免修改原始样本
sampled_data_cls['High_ROE'] = (sampled_data_cls['ROE'] > 0.15).astype(int) # 高盈利标记
sampled_data_cls = sampled_data_cls[sampled_data_cls['Leverage'] < 10] # 剔除杠杆极端值
cls_features = sampled_data_cls[['Margin', 'Turnover', 'Leverage']] # 杜邦三因子特征
cls_target = sampled_data_cls['High_ROE'] # 二分类目标
cls_tree = DecisionTreeClassifier(max_depth=3, criterion='gini', random_state=42) # Gini准则分类树
cls_tree.fit(cls_features, cls_target) # 训练分类树
plt.figure(figsize=(14, 9)) # 设置画布大小
plot_tree(cls_tree, feature_names=['净利率', '周转率', '杠杆'], # 绘制分类树
class_names=['普通', '高盈利'], filled=True, rounded=True, fontsize=10) # 类别标签与样式
plt.title('分类树: 识别高盈利公司 (ROE > 15%)', fontsize=14) # 图表标题
plt.show() # 显示图表
树与线性模型的对比
线性模型假设:\(f(X) = \beta_0 + \sum_{j=1}^{p} X_j \beta_j\)
树模型假设:\(f(X) = \sum_{m=1}^{M} c_m \cdot \mathbf{1}(X \in R_m)\)
| 线性关系 |
线性回归 |
能利用线性结构 |
| 非线性交互 |
决策树 |
自然捕捉交互作用 |
| 混合变量类型 |
决策树 |
无需虚拟变量编码 |
| 高维稀疏 |
线性回归 |
树在\(p \gg n\)时易过拟合 |
| 需要外推 |
线性回归 |
树无法外推 |
| 可解释性优先 |
决策树 |
规则直观明了 |
从单棵树到森林:集成学习的核心思想
单棵决策树的局限:高方差、不稳定——数据微小变化可能导致完全不同的树。
集成方法的核心:组合多棵树,取长补短。
集成学习的统计学基础
为什么集成能降低方差?
设 \(Z_1, \ldots, Z_n\) 为独立同分布随机变量,方差为 \(\sigma^2\),则:
\[
\text{Var}\left(\frac{1}{n}\sum_{i=1}^n Z_i\right) = \frac{\sigma^2}{n}
\]
方差随 \(n\) 增大线性下降! 这就是Bagging的理论基础。
但如果变量不独立,相关系数为 \(\rho\):
\[
\text{Var}\left(\frac{1}{n}\sum_{i=1}^n Z_i\right) = \rho\sigma^2 + \frac{1-\rho}{n}\sigma^2
\]
- 第一项 \(\rho\sigma^2\) 无法通过增加 \(n\) 消除
- 随机森林的核心创新:通过随机选特征降低 \(\rho\)!
Bagging:通过自助法平均降低方差
Bootstrap聚合的核心思想:
- 独立观测均值的方差:\(\text{Var}(\bar{Z}) = \sigma^2 / n\)
- 生成 \(B\) 个自助训练集 → 训练 \(B\) 棵深树 → 平均预测
\[
\hat{f}_{\text{bag}}(x) = \frac{1}{B} \sum_{b=1}^{B} \hat{f}^{*b}(x)
\]
关键特性:
- 每棵树不剪枝(高方差、低偏差)
- 平均后降低方差,偏差基本不变
- 袋外误差(OOB):每棵树约 1/3 样本未被使用,可直接估计测试误差
OOB误差:免费的交叉验证
Bagging的一个独特优势:袋外(Out-of-Bag)误差估计
原理:每棵自助法树大约只用到 \(\frac{2}{3}\) 的训练数据
\[
P(\text{样本}\ i\ \text{被选中}) = 1 - \left(1 - \frac{1}{n}\right)^n \approx 1 - e^{-1} \approx 0.632
\]
- 剩余约 \(\frac{1}{3}\) 的样本对这棵树来说是”测试数据”
- 对每个样本,只用没有包含它的那些树来预测
- 汇总这些预测就得到OOB误差
OOB误差 ≈ LOOCV误差,但计算成本低得多!
随机森林:通过去相关进一步降低方差
随机森林在Bagging基础上的关键改进:
每次分裂时,只从 \(m\) 个随机选取的特征中选择最佳分裂点(通常 \(m \approx \sqrt{p}\))。
为什么? 如果存在一个强预测变量,Bagging的所有树都会在顶部使用它:
- 树与树高度相关 → 平均效果有限
- 随机森林强制忽略部分特征 → 去相关 → 平均方差更低
\[
\text{Var}(\bar{f}) = \rho\sigma^2 + \frac{1-\rho}{B}\sigma^2
\]
当 \(B \to \infty\) 时,方差趋于 \(\rho\sigma^2\)。相关性 \(\rho\) 越小,集成效果越好。
随机森林的超参数选择
随机森林的关键超参数及选择指南:
| 树的数量 |
\(B\) |
500 |
越多越好,直到OOB误差稳定 |
| 每次候选特征数 |
\(m\) |
\(\sqrt{p}\)(分类)/ \(p/3\)(回归) |
用OOB误差网格搜索 |
| 最大深度 |
max_depth |
None(不限) |
回归可限制;分类通常不限 |
| 叶节点最小样本数 |
min_samples_leaf |
1 |
增大可防过拟合 |
核心参数 \(m\) 的直觉:
- \(m = p\):退化为Bagging(树高度相关)
- \(m = 1\):完全随机选特征(树不相关但每棵树很弱)
- \(m = \sqrt{p}\):平衡点——足够随机以去相关,又保留足够信息以维持单树质量
Bagging、随机森林、Boosting 的训练顺序要并排看
这三种集成方法都在“用很多棵树”,但真正的运行顺序并不一样:
| Bagging |
先从原始数据抽一个 Bootstrap 样本 |
对每个样本各训练一棵深树 |
回归取平均,分类取投票 |
| 随机森林 |
先抽 Bootstrap 样本 |
每棵树内部的每次分裂都只看一部分特征 |
再把所有树的结果平均或投票 |
| Boosting |
先给一个初始预测 |
每一轮都拟合上一轮剩下的残差,再小步更新 |
把所有浅树按学习率累加起来 |
最适合课堂记忆的口令:
- Bagging:先重抽样,再并行长树,最后做平均
- 随机森林:先重抽样,再随机挑特征长树,最后做平均
- Boosting:先给初值,再逐轮纠错,最后把小改动累加
这页真正要解决的不是公式,而是防止把三种方法混成一句“都是很多树”。Bagging 强调的是并行平均,随机森林强调的是并行 + 去相关,Boosting 强调的是串行纠错。
BART:贝叶斯加性回归树
BART结合了Bagging的随机性和Boosting的残差修正思想:
- 维护 \(K\) 棵回归树,迭代 \(B\) 次
- 每次迭代更新一棵树:计算部分残差 → 对上一轮树进行随机扰动
- 扰动方式:添加/修剪分支、改变叶节点预测值
\[
\hat{f}(x) = \frac{1}{B - L} \sum_{b=L+1}^{B} \hat{f}^b(x)
\]
其中 \(L\) 为预烧期(burn-in),丢弃早期不稳定的迭代结果。
独特优势:自然提供不确定性估计(贝叶斯后验分布)。
BART的独特优势
BART相比其他集成方法的独特之处:
BART = Bayesian Additive Regression Trees
与随机森林和Boosting的关键区别:
| 训练方式 |
并行 |
串行 |
迭代(Gibbs采样) |
| 不确定性量化 |
无 |
无 |
天然给出置信区间 |
| 需调参数 |
少 |
多 |
较少 |
| 过拟合风险 |
低 |
中 |
低 |
BART的核心思想:
- 每棵树都被鼓励做小的贡献(强先验)
- 通过MCMC采样获得后验分布
- 不仅给出点预测,还给出预测的不确定性
案例1:四种树方法预测公司ROE
from sklearn.tree import DecisionTreeRegressor # 回归树
from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor # 随机森林与梯度提升
from sklearn.model_selection import train_test_split # 数据集划分
from sklearn.metrics import mean_squared_error # MSE评估
# 使用更大样本量的数据进行集成学习
modeling_data = financial_data.sample(n=min(2000, len(financial_data)), random_state=42) # 抽取2000样本
X_all = modeling_data[['Margin', 'Turnover', 'Leverage', 'Log_Assets']] # 四维特征矩阵
y_all = modeling_data['ROE'] # ROE目标
# 70%/30%划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X_all, y_all, test_size=0.3, random_state=42)
# 训练四种模型
single_tree = DecisionTreeRegressor(random_state=42) # 单棵回归树(无限制深度)
single_tree.fit(X_train, y_train) # 拟合
bagging = RandomForestRegressor(n_estimators=100, max_features=None, random_state=42) # Bagging(全特征)
bagging.fit(X_train, y_train) # 拟合
rf = RandomForestRegressor(n_estimators=100, max_features='sqrt', random_state=42) # 随机森林(特征子采样)
rf.fit(X_train, y_train) # 拟合
boosting = GradientBoostingRegressor(n_estimators=100, learning_rate=0.1, max_depth=3, random_state=42) # 梯度提升
boosting.fit(X_train, y_train) # 拟合
集成方法显著优于单棵树
Code
# 计算四种方法的测试集MSE
mse_values = [ # MSE列表
mean_squared_error(y_test, single_tree.predict(X_test)), # 单棵树MSE
mean_squared_error(y_test, bagging.predict(X_test)), # Bagging MSE
mean_squared_error(y_test, rf.predict(X_test)), # 随机森林MSE
mean_squared_error(y_test, boosting.predict(X_test)) # Boosting MSE
]
methods = ['单棵树', 'Bagging', '随机森林', 'Boosting'] # 方法标签
plt.figure(figsize=(10, 6)) # 设置画布
bars = plt.bar(methods, mse_values, color=['#E3120B', '#2C3E50', '#008080', '#F0A700']) # 柱状图
for bar, val in zip(bars, mse_values): # 添加数值标签
plt.text(bar.get_x() + bar.get_width()/2., bar.get_height(), f'{val:.4f}',
ha='center', va='bottom', fontsize=11) # 柱顶标注MSE值
plt.title('ROE预测: 不同树方法的测试集MSE比较', fontsize=14) # 图表标题
plt.ylabel('均方误差 (MSE)') # Y轴标签
plt.grid(axis='y', alpha=0.3) # Y轴网格线
plt.tight_layout() # 自动布局
plt.show() # 显示图表
案例2:随机森林与XGBoost预测A股涨跌
from sklearn.ensemble import RandomForestClassifier # 随机森林分类器
from sklearn.metrics import accuracy_score, roc_auc_score # 准确率与AUC评估
import warnings # 警告控制
warnings.filterwarnings('ignore') # 关闭冗余警告
# 加载海康威视前复权股价数据
stock_path = os.path.join(DATA_DIR, 'stock/stock_price_pre_adjusted.h5') # 股价文件路径
stock_data = pd.read_hdf(stock_path) # 读取前复权股价数据
if 'order_book_id' in stock_data.index.names: # 若在索引中则重置
stock_data = stock_data.reset_index()
stock_data = stock_data[stock_data['order_book_id'] == '002415.XSHE'].copy() # 筛选海康威视
stock_data = stock_data.sort_values('date') # 按日期排序
# 构建金融技术特征
stock_data['Ret'] = stock_data['close'].pct_change() # 日收益率
stock_data['Target'] = (stock_data['Ret'].shift(-1) > 0).astype(int) # 明天是否上涨
for i in range(1, 4): # 构造滞后收益率特征
stock_data[f'Lag_{i}'] = stock_data['Ret'].shift(i)
stock_data['Vol_5'] = stock_data['Ret'].rolling(5).std() # 5日波动率
stock_data['Vol_Change'] = stock_data['volume'].pct_change() # 成交量变化率
stock_data = stock_data.replace([np.inf, -np.inf], np.nan).dropna() # 清洗inf和NaN
feat_cols = [f'Lag_{i}' for i in range(1, 4)] + ['Vol_5', 'Vol_Change'] # 特征列
# 时间前向切分(70%训练/30%测试)严格保持时间顺序
split = int(len(stock_data) * 0.7) # 70%分界点
X_tr, X_te = stock_data[feat_cols].values[:split], stock_data[feat_cols].values[split:] # 特征切分
y_tr, y_te = stock_data['Target'].values[:split], stock_data['Target'].values[split:] # 目标切分
金融时间序列的低信噪比挑战
Code
from sklearn.ensemble import GradientBoostingClassifier # 梯度提升分类器
# 训练随机森林
rf_cls = RandomForestClassifier(n_estimators=300, max_depth=5, max_features='sqrt', random_state=42)
rf_cls.fit(X_tr, y_tr) # 拟合
# 训练梯度提升(强正则化应对低信噪比)
gb_cls = GradientBoostingClassifier(n_estimators=300, max_depth=3, learning_rate=0.01, random_state=42)
gb_cls.fit(X_tr, y_tr) # 拟合
models_dict = {'Random Forest': rf_cls, 'Gradient Boosting': gb_cls} # 模型字典
plt.figure(figsize=(14, 5)) # 设置画布
for idx, (name, mdl) in enumerate(models_dict.items()): # 遍历模型
proba = mdl.predict_proba(X_te)[:, 1] # 正类预测概率
auc = roc_auc_score(y_te, proba) # 计算AUC
plt.subplot(1, 2, idx+1) # 子图
imp = mdl.feature_importances_ # 特征重要性
order = np.argsort(imp) # 升序索引
plt.barh(range(len(order)), imp[order], color='#3498DB') # 水平条形图
plt.yticks(range(len(order)), [feat_cols[i] for i in order]) # 特征名称
plt.xlabel('特征重要性') # X轴标签
plt.title(f'{name} (AUC={auc:.3f})') # 子图标题
plt.tight_layout() # 自动布局
plt.show() # 显示图表
实证演示:拟合分类树
from sklearn.tree import DecisionTreeClassifier, export_text # 分类树与规则导出
from sklearn.metrics import classification_report, confusion_matrix # 分类评估
# 准备数据:预测公司是否盈利
lab_data = financial_data.sample(n=min(2000, len(financial_data)), random_state=42) # 抽取子样本
lab_data['Profitable'] = (lab_data['ROE'] > 0).astype(int) # 二分类目标:是否盈利
lab_features = lab_data[['Margin', 'Turnover', 'Leverage', 'Log_Assets']] # 四维特征
lab_target = lab_data['Profitable'] # 目标变量
# 70%/30%随机划分
lab_X_tr, lab_X_te, lab_y_tr, lab_y_te = train_test_split(
lab_features, lab_target, test_size=0.3, random_state=42)
# 拟合分类树(Gini准则,最大深度3层)
lab_cls_tree = DecisionTreeClassifier(criterion='gini', max_depth=3, random_state=42)
lab_cls_tree.fit(lab_X_tr, lab_y_tr) # 训练分类树
lab_pred = lab_cls_tree.predict(lab_X_te) # 测试集预测
print('分类报告:') # 输出标题
print(classification_report(lab_y_te, lab_pred)) # 输出精确率、召回率、F1
print(f'混淆矩阵:\n{confusion_matrix(lab_y_te, lab_pred)}') # 输出混淆矩阵
分类报告:
precision recall f1-score support
0 1.00 1.00 1.00 71
1 1.00 1.00 1.00 529
accuracy 1.00 600
macro avg 1.00 1.00 1.00 600
weighted avg 1.00 1.00 1.00 600
混淆矩阵:
[[ 71 0]
[ 0 529]]
# 导出树规则为文本(便于硬编码进生产系统)
tree_rules = export_text(lab_cls_tree, feature_names=list(lab_features.columns)) # 导出if-else规则
print('树规则:') # 输出标题
print(tree_rules) # 打印规则文本
树规则:
|--- Margin <= -0.00
| |--- class: 0
|--- Margin > -0.00
| |--- class: 1
实证演示:拟合回归树
from sklearn.metrics import r2_score # R²评估
# 回归目标:预测具体ROE数值
lab_reg_target = lab_data['ROE'] # 连续目标变量
lab_reg_X_tr, lab_reg_X_te, lab_reg_y_tr, lab_reg_y_te = train_test_split(
lab_features, lab_reg_target, test_size=0.3, random_state=42) # 数据划分
# 拟合回归树(最大深度4层)
lab_reg_tree = DecisionTreeRegressor(max_depth=4, random_state=42) # 初始化
lab_reg_tree.fit(lab_reg_X_tr, lab_reg_y_tr) # 训练
reg_pred = lab_reg_tree.predict(lab_reg_X_te) # 测试集预测
print(f'回归树 测试集MSE: {mean_squared_error(lab_reg_y_te, reg_pred):.4f}') # 输出MSE
print(f'回归树 测试集R²: {r2_score(lab_reg_y_te, reg_pred):.4f}') # 输出R²
回归树 测试集MSE: 0.0024
回归树 测试集R²: 0.6948
实证演示:随机森林显著提升R²
# 随机森林(100棵树)
lab_rf = RandomForestRegressor(n_estimators=100, random_state=42) # 初始化随机森林
lab_rf.fit(lab_reg_X_tr, lab_reg_y_tr) # 训练
rf_pred = lab_rf.predict(lab_reg_X_te) # 测试集预测
print(f'随机森林 测试集MSE: {mean_squared_error(lab_reg_y_te, rf_pred):.4f}') # MSE
print(f'随机森林 测试集R²: {r2_score(lab_reg_y_te, rf_pred):.4f}') # R²
# 特征重要性排名
importance_df = pd.DataFrame( # 构建重要性DataFrame
{'importance': lab_rf.feature_importances_}, # 特征重要性值
index=lab_features.columns # 特征名称作为索引
).sort_values('importance', ascending=False) # 按重要性降序排列
print('\n特征重要性 (Random Forest):') # 输出标题
print(importance_df) # 打印排名表
随机森林 测试集MSE: 0.0006
随机森林 测试集R²: 0.9210
特征重要性 (Random Forest):
importance
Margin 0.538685
Turnover 0.341995
Leverage 0.097492
Log_Assets 0.021829
实证演示:Boosting回归
# 梯度提升树(100棵浅树,学习率0.1)
lab_gb = GradientBoostingRegressor(n_estimators=100, learning_rate=0.1, max_depth=3, random_state=42)
lab_gb.fit(lab_reg_X_tr, lab_reg_y_tr) # 训练
gb_pred = lab_gb.predict(lab_reg_X_te) # 测试集预测
print(f'Boosting 测试集MSE: {mean_squared_error(lab_reg_y_te, gb_pred):.4f}') # MSE
print(f'Boosting 测试集R²: {r2_score(lab_reg_y_te, gb_pred):.4f}') # R²
# 三种方法横向对比
print('\n--- 三种方法对比 ---') # 输出标题
print(f'单棵树 MSE={mean_squared_error(lab_reg_y_te, reg_pred):.4f}, R²={r2_score(lab_reg_y_te, reg_pred):.4f}')
print(f'随机森林 MSE={mean_squared_error(lab_reg_y_te, rf_pred):.4f}, R²={r2_score(lab_reg_y_te, rf_pred):.4f}')
print(f'Boosting MSE={mean_squared_error(lab_reg_y_te, gb_pred):.4f}, R²={r2_score(lab_reg_y_te, gb_pred):.4f}')
Boosting 测试集MSE: 0.0007
Boosting 测试集R²: 0.9054
--- 三种方法对比 ---
单棵树 MSE=0.0024, R²=0.6948
随机森林 MSE=0.0006, R²=0.9210
Boosting MSE=0.0007, R²=0.9054
五种树方法的横向对比
- 先看结构:单棵树最适合发现阈值、交互和业务规则。
- 再做稳健基线:随机森林通常是结构化数据任务里的默认起点。
- 最后冲精度:确认存在可挖掘的剩余非线性后,再把时间重点投向 Boosting。
- 需要区间时再上 BART:当问题不仅要点预测,还要后验不确定性时,BART 才真正显示优势。
实践建议
何时使用基于树的方法?
- 非线性交互关系:如杜邦分析中的乘法交互
- 混合变量类型:定量+定性变量(无需虚拟变量编码)
- 可解释性优先:单棵树提供完美的”决策流程图”
- 预测精度优先:使用集成方法(随机森林或Boosting)
关键注意事项:
| 树无法外推 |
预测值不超出训练数据范围 |
| 金融数据信噪比低 |
树集成在股票预测中AUC通常仅略高于0.5 |
| 特征重要性可能误导 |
相关特征间重要性会被分摊 |
| 超参数调优关键 |
树深、学习率、特征子集大小需交叉验证 |
推荐顺序:先用单棵树看结构,再用随机森林做稳健基线,最后再考虑 Boosting 冲击更高精度
决策指南:选择哪种树模型?
| 需要可解释性 |
单棵决策树(剪枝) |
可视化决策路径 |
| 结构化数据预测 |
XGBoost/LightGBM |
Kaggle竞赛冠军标配 |
| 小样本、少调参 |
随机森林 |
开箱即用,不易过拟合 |
| 需要不确定性估计 |
BART |
贝叶斯置信区间 |
| 特征重要性分析 |
随机森林 |
置换重要性可靠 |
| 高频交易/实时 |
单棵树/线性模型 |
推理速度快 |
实操经验:
- 第一步:随机森林(基线模型,5分钟搞定)
- 第二步:XGBoost精调(花80%时间调参)
- 第三步:特征工程 > 模型选择(提升幅度更大)
- 如果变量重要性排序与业务常识明显冲突,先检查样本切分、特征共线性与泄露问题