《深入浅出Python量化交易实战》第八章(上)决策树与随机森林
朝朝暮暮1895
2025年09月15日 22:52
收录于文集
共131篇

本章的主要内容如下:

决策树和随机森林的原理与使用方法。 使用决策树判断因子的重要程度。 使用随机森林模型进行选股。 编写更精细的交易策略并回测。

【生成一个数据集,并且让这个数据集不适用线性模型】

{代码部分}

#导入决策树回归器和其他必要的库

from sklearn.tree import DecisionTreeRegressor

from sklearn.datasets import make_regression

import matplotlib.pyplot as plt

import numpy as np

import pandas as pd

# 生成一个回归任务数据集,样本数量100

X, y = make_regression(n_samples=100,

            # 只有一个特征

            n_features=1,

            # 为了使线性模型不适用

            # 设置噪声参数为60

            noise=60,

            # 设定随机状态,便于复现

            random_state=18)

# 使用散点图将样本进行可视化

plt.scatter(X, y, s=80,

      # 下面几个参数都是为了美观

      c=y, edgecolor='grey',

      alpha=0.6)

# 添加网格并展示图像

plt.grid()

plt.show()

{返回结果}

【决策树示例】

{代码部分}

# 创建一个决策树示例,可以通过调节max_depth参数来防止模型过拟合

# 这里我们不限制决策树的max_depth

reg = DecisionTreeRegressor(max_depth=None)

# 使用模型拟合样本数据

reg.fit(X, y)

# 为了将模型进行可视化,同样生成一个沿横轴分布的数列

X_new = np.linspace(-2.5, 3.5, 100)

# 调用模型对生成数列的目标值做出预测

y_new = reg.predict(X_new.reshape(-1, 1))

# 以下是绘图的部分

plt.scatter(X, y, s=80,

      c=y, edgecolor='grey',

      alpha=0.4)

plt.plot(X_new, y_new, lw=2,

     ls='-', c='grey')

plt.grid()

plt.show()

{返回结果}

【决策树算法逻辑】

决策树是监督学习算法,用树形结构做分类 / 回归。核心逻辑是递归分割数据集,让子集 “纯度”(同类样本占比)逐步提升,直到满足停止条件(如节点样本同类别、树深达上限等 ),过程像 “分而治之”:

  • 节点含义:内部节点是特征 / 属性判断(如 “天气是否晴朗” ),分支是特征取值(“晴 / 雨” ),叶节点是分类结果(“适合 / 不适合户外活动” )。

  • 特征选择:用指标选最优分割特征,常用:

    • 信息增益(ID3 算法):基于信息熵,选 “分割后数据集不确定性降最多” 的特征;

    • 信息增益率(C4.5 算法):修正信息增益,避免偏好多取值特征;

    • 基尼指数(CART 算法):衡量数据集纯度,选 “分割后纯度提升最多” 的特征。

生活场景举例(是否约会)

用决策树模拟 “女生判断是否见相亲对象” 的逻辑: 特征取值决策逻辑(简化)年龄≤30 岁 />30 岁优先用 “年龄是否≤30 岁” 初步筛选颜值帅 / 一般若年龄符合,再看 “颜值是否帅”职业公务员 / 其他最后用 “是否公务员” 决定最终是否见面

简言之,决策树用 “分层判断特征” 的方式,把复杂决策拆解成简单规则,既直观又易解释,是理解机器学习分类逻辑的经典模型~

决策树的工作过程是一个树状的结构——这也是决策树名字的由来。在整个树状结构的顶端,模型先对样本特征进行判断:如果样本特征小于或等于某个值,则向左分枝,否则向右分枝;到第二层左边的节点,如果样本特征小于或等于某个值,则向左分枝,否则向右分枝……模型一直重复这个过程,直到拟合完全部的样本

【随机森林】

{代码部分}

# 导入随机森林回归器

from sklearn.ensemble import RandomForestRegressor

# 创建一个随机森林实例,指定森林中有100棵决策树

reg2 = RandomForestRegressor(n_estimators=100)

# 使用随机森林拟合数据

reg2.fit(X, y)

# 生成对数列进行预测

y_new_2 = reg2.predict(X_new.reshape(-1, 1))

# 下面是绘图部分

plt.scatter(X, y, s=80,

      c=y, edgecolor='grey',

      alpha=0.4)

# 将决策树和随机森林的模型进行可视化

plt.plot(X_new, y_new, lw=1.5,

     ls='-', c='grey',

     label='Tree')

plt.plot(X_new, y_new_2, lw=1.5,

     ls='--', c='r',

     label='Forest')

# 添加图注、网格并展示

plt.legend()

plt.grid()

plt.show()

{返回结果}

随机森林模型“波动”的幅度没有决策树模型大。这说明,随机森林模型要比决策树模型更简单,相对不容易受到噪声的干扰,也就更不容易出现过拟合的现象。

【什么是随机森林?】

### 随机森林算法介绍

随机森林是**集成学习**里基于 **Bagging(自助聚合)** 思想的经典算法,核心是用多棵决策树“集体投票”做决策,以提升模型的泛化能力(抗过拟合)和鲁棒性(稳定性),流程可概括为:

1. **随机抽样构建子数据集**:对原始训练集**有放回抽样**(Bootstrap 抽样),生成多组与原数据集同大小的子数据集(子集会有重复样本,也会遗漏部分样本 )。

2. **随机选特征训练决策树**:每棵决策树训练时,从所有特征里**随机选部分特征**参与分裂(进一步增加树的多样性 ),用 CART 等算法独立训练成完整决策树(不剪枝 )。

3. **多树投票决策**:预测时,新样本输入所有决策树,分类问题选**众数**(回归问题选**均值** )作为最终结果,靠“群体智慧”降低单棵树的偏差与方差。

### 生活场景举例(周末是否郊游)

假设想通过 **“天气、温度、是否周末、同伴是否有空”** 4 个特征,判断周末是否适合郊游(结果:去/不去 ),用随机森林模拟如下:

#### 1. 原始数据集(简化)

| 样本 | 天气(晴/雨) | 温度(高/中/低) | 是否周末(是/否) | 同伴是否有空(是/否) | 是否郊游(结果) |

|------|--------------|------------------|------------------|----------------------|------------------|

| 1 | 晴 | 中 | 是 | 是 | 去 |

| 2 | 雨 | 高 | 否 | 否 | 不去 |

| 3 | 晴 | 低 | 是 | 否 | 去(独自郊游) |

|... |... |... |... |... |... |

#### 2. 随机森林构建过程

- **步骤 1:抽样生成子数据集**

假设构建 3 棵决策树,对原始数据集有放回抽样,得到 3 组子数据:

- 子集 1(树 1):样本 1、1、3、2…(样本 1 重复,样本 4 未被抽中 )

- 子集 2(树 2):样本 3、2、2、1…(样本 2 重复,样本 5 未被抽中 )

- 子集 3(树 3):样本 1、3、2、3…(样本 3 重复 )

- **步骤 2:随机选特征训练决策树**

每棵树随机选部分特征训练(如树 1 选“天气、是否周末”,树 2 选“温度、同伴是否有空” ),各自生成决策规则:

- 树 1 规则(简化):天气=晴 → 再看是否周末 → 是则“去”,否则“不去”

- 树 2 规则(简化):温度=低 → 不管其他特征 → 直接“去”(因训练子集里低温样本都对应“去” )

- 树 3 规则(简化):同伴有空=是 → “去”;否则看天气 → 晴则“去”,雨则“不去”

#### 3. 新样本预测(周末、天气晴、温度中、同伴有空 )

- 树 1 预测:天气=晴+是周末 → **去**

- 树 2 预测:温度=中(不触发“温度=低→去” ),但因训练时“温度”规则简单,可能结合其他特征→ **去**(或依实际训练结果 )

- 树 3 预测:同伴有空=是 → **去**

- **最终投票**:3 棵树都预测“去”→ 随机森林输出结果 **去郊游**

### 核心优势与特点

- **抗过拟合**:多棵树“投票”抵消单棵树的极端预测,适合数据噪声大的场景。

- **易扩展**:支持分类(如上述郊游案例 )、回归(如预测房价 ),还能输出特征重要性(如判断“天气”对郊游决策的影响度 )。

- **高效**:多棵树可并行训练,适合大数据量,比单决策树更稳定。

简单说,随机森林像“一群参谋”,各自从不同样本、特征角度分析问题,最后汇总意见做决策,既保留了决策树的直观,又靠“人多力量大”解决了单棵树的不足~

【看到随机森林中的每一棵树】

{代码部分}

#查看随机森林中前两棵决策树

reg2.estimators_[:2]

{返回结果}

[DecisionTreeRegressor(criterion='mse', max_depth=None, max_features='auto',

max_leaf_nodes=None, min_impurity_decrease=0.0,

min_impurity_split=None, min_samples_leaf=1,

min_samples_split=2, min_weight_fraction_leaf=0.0,

presort=False, random_state=1188818746, splitter='best'),

DecisionTreeRegressor(criterion='mse', max_depth=None, max_features='auto',

max_leaf_nodes=None, min_impurity_decrease=0.0,

min_impurity_split=None, min_samples_leaf=1,

min_samples_split=2, min_weight_fraction_leaf=0.0,

presort=False, random_state=1263748285, splitter='best')]

系统返回了随机森林中前两棵决策树的模型。可以看到,这两棵决策树模型的大部分参数是相同的,只有random_state参数不同。当然,仅仅是random_state的差别也可以让两棵决策树的预测结果有一定的差异。在我们的随机森林中,有100棵不同的决策树,这就使得随机森林的预测结果更倾向于“中立”,从而降低过拟合的风险。

【决策树的“隐藏功能”——判断特征重要性(feature importance)】

{代码部分}

# 这里开始验证

import jqdata

from jqlib.technical_analysis import *

import datetime

# 获取股票池

stocks = get_index_stocks('000040.XSHG')

# 修正查询语句:移除 valuation.code.in_(stocks) 作为返回列,仅作为过滤条件

q = query(

  valuation.code,

  valuation.market_cap,

  balance.total_current_assets - balance.total_current_liability,

  balance.total_liability - balance.total_assets,

  balance.total_liability / balance.equities_parent_company_owners,

  (balance.total_assets - balance.total_current_assets) / balance.total_assets,

  balance.equities_parent_company_owners / balance.total_assets,

  indicator.inc_total_revenue_year_on_year,

  valuation.turnover_ratio,

  valuation.pe_ratio,

  valuation.pb_ratio,

  valuation.ps_ratio,

  indicator.roa # 总资产收益率(假设您需要此字段,根据实际需求调整)

).filter(

  valuation.code.in_(stocks) # 正确的过滤条件用法

)

# 获取数据并命名列(列数必须与 query 返回字段数一致)

df = get_fundamentals(q, date=None)

df.columns = ['code', '市值', '净营运资本', '净债务', '产权比率', 

       '非流动资产比率', '股东权益比率', '营收增长率', 

       '换手率', 'PE', 'PB', 'PS', '总资产收益率']

# 检查数据

df.head(20)

{返回结果}

【获取技术因子】

{代码部分}

# 这里继续验证

# 将股票代码作为数据表的 index

df.index = df.code.values

# 使用 del 也可以删除列

del df['code']

# 下面定义时间变量

today = datetime.datetime.today()

# 设定 3 个时间差,分别是 50 天、1 天和 2 天

delta50 = datetime.timedelta(days=50)

delta1 = datetime.timedelta(days=1)

delta2 = datetime.timedelta(days=2)

# 50 天前作为一个历史节点

history = today - delta50

# 再计算昨天和 2 天前的日期

yesterday = today - delta1

two_days_ago = today - delta2

# 下面获取股票的动量线、成交量、累计能量线、平均差、

# 指数移动平均、移动平均、乖离率等因子

# 时间范围都设为 10 天

df['动量线'] = list(MTM(df.index, two_days_ago,

           timeperiod=10, unit='1d',

           include_now=True,

           fq_ref_date=None).values())

df['成交量'] = list(VOL(df.index, two_days_ago, M1=10,

           unit='1d', include_now=True,

           fq_ref_date=None)[0].values())

df['累计能量线'] = list(OBV(df.index, check_date=two_days_ago,

            timeperiod=10).values())

df['平均差'] = list(DMA(df.index, two_days_ago, N1=10,

           unit='1d', include_now=True,

           fq_ref_date=None)[0].values())

df['指数移动平均'] = list(EMA(df.index, two_days_ago, timeperiod=10,

             unit='1d', include_now=True,

             fq_ref_date=None).values())

df['移动平均'] = list(MA(df.index, two_days_ago, timeperiod=10,

           unit='1d', include_now=True,

           fq_ref_date=None).values())

df['乖离率'] = list(BIAS(df.index, two_days_ago, N1=10,

           unit='1d', include_now=True,

           fq_ref_date=None)[0].values())

# 把数据表中的空值用 0 来代替

df.fillna(0, inplace=True)

# 检查是否成功

df.head(20)

{返回结果}

【设定目标并训练模型】

思路——先找到股票的历史收盘价(如50天前),再用前一天的收盘价除以50天前的收盘价并减1,计算出这50天来股票的收益;然后我们找到那些收益水平大于平均水平的股票,标记为1,其余标记为0,作为模型的分类标签。

{代码部分}

# 获取股票前一日的收盘价

df['close1'] = list(get_price(stocks,

               end_date=yesterday,

               count=1,

               fq='pre',

               panel=False)['close'])

# 获取股票50天前的收盘价

df['close2'] = list(get_price(stocks,

               end_date=history,

               count=1,

               fq='pre',

               panel=False)['close'])

# 计算收益

df['return'] = df['close1'] / df['close2'] - 1

# 如果收益大于平均水平,则标记为1

# 否则标记为0

df['signal'] = np.where(df['return'] < df['return'].mean(), 0, 1)

# 检查是否成功

df.head(20)

{返回结果}

close1、close2、return和signal这几列。它们分别对应的是1天前的收盘价、50天前的收盘价、该时间段内的收益,以及收益是否大于平均值。signal这一列是训练模型用的分类标签。

【开始训练模型】

{代码部分}

# 导入数据集拆分工具

from sklearn.model_selection import train_test_split

# 导入决策树分类器

from sklearn.tree import DecisionTreeClassifier

# 把因子值作为样本的特征,所以要去掉刚刚添加的几个字段(假设 df 是已处理好的 DataFrame 数据)

X = df.drop(['close1', 'close2','return','signal'], axis = 1)  

# 把 signal 作为分类标签

y = df['signal']  

# 将数据拆分为训练集和验证集

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.3)  

# 创建决策树分类器实例,指定 random_state 便于复现

clf = DecisionTreeClassifier(random_state=1000)  

# 拟合训练数据

clf.fit(X_train, y_train)  

# 查看分类器在训练集和验证集中的准确率

print(clf.score(X_train, y_train),

   clf.score(X_test, y_test))  

{返回结果}

1.0 0.8888888888888888

决策树模型的表现还是比较不错的——训练集中的准确率达到了100%,在验证集中的准确率达到了88.88%左右。 【哪些因子重要】 决策树的属性——feature_importances_存储的是模型判断的样本特征的重要程度。为了便于查看,我们把这个属性存储到一个列表中。

{代码部分}

# 为了便于观察,我们创建一个数据表

# 数据表有两个字段,分别是特征名和重要性

# 特征名就是因子的名称

# 重要性就是决策树给出的feature_importances_

factor_weight = pd.DataFrame({'features': list(X.columns),

               'importance': clf.feature_importances_}).sort_values(

  # 这里根据重要程度降序排列,一遍遍找到重要性最高的特征

  by='importance', ascending=False)

# 检查结果

factor_weight

{返回结果}

在所有的因子当中,平均差(DMA)这个因子的重要性竟然是最高的,达到了0.68左右,远远超过了其他因子。平均差因子的含义是,短期均线的数值减去长期均线的数值。这里的DMA是默认的参数,也就是10天均线减去50天均线。 我们也可以看到,次重要因子是——市值因子占0.32左右。