简介:这是一套面向机器学习初学者的股票预测实战项目,聚焦决策树、随机森林与时间序列模型在金融数据上的应用。压缩包共7个文件,约2.02MB,包含两个Jupyter Notebook源代码(分类任务与时间序列预测)、对应的HTML预览版、股票与公司数据集(csv/xlsx)以及项目说明文档(docx),方便直接阅读代码与结果。项目覆盖数据清洗、特征构造、模型训练、交叉验证与误差指标(MAE、RMSE、R²)分析等完整流程,并附有特征重要性解读与投资策略示例,帮助学习者将算法理论落地到真实股价预测场景。该资源已有3481人学习下载,适合希望提升数据分析与建模技能、了解量化分析基础的人群。
1. 机器学习实战资源拆解:这个 zip 里究竟有什么
在机器学习实战的进阶路上,缺的往往不是算法原理,而是一套能同时覆盖分类、集成学习和金融时间序列的完整可运行代码。这个“机器学习实战项目——决策树&随机森林&时间序列预测股价.zip”恰好把三块拼齐了:两个 Jupyter Notebook(Classification_Decision Tree&Random Forests.ipynb 和 Time_Series_Stock_Prediction.ipynb),配套的 ExtraaLearn.csv 分类数据集、amazon_stocks_prices.xlsx 股票价格数据,以及 HTML 版本的结果回放和一份项目描述文档。解压后不需要额外找数据,直接在 notebook 里按顺序跑,就能看到从清洗、特征工程到建模、评估的完整链路。
对正在学 scikit-learn 的读者,这份资源最直接的价值,是把决策树、随机森林和时间序列预测放进同一个项目上下文里,一次看到三类问题的典型处理方式。已经能跑通代码的熟手,则适合用它来对照检查自己的预处理顺序和评估方式——后面我会重点拆解几个容易翻车的点。总体来说,它适合刚学完机器学习基础想上手练习的人,也适合准备面试需要完整项目案例的人,以及想用现成代码快速验证股价预测想法的人。
2. 决策树分类实战:从 ExtraaLearn.csv 到可解释规则
2.1 数据集细节与预处理流程
ExtraaLearn.csv 是一个典型的培训机构学员数据,通常包含性别、学历、岗位、课程类型等字段,目标列是“最终是否转化”。这类数据在真实营销场景里非常常见,处理逻辑和金融风控也类似——先读进来看字段类型和缺失分布,再决定怎么编码。
先看第一步的读数和检查代码:
import pandas as pd # 解压 zip 后,CSV 和 Notebook 在同一个目录下,直接读 df = pd.read_csv("ExtraaLearn.csv") print(df.shape) # 先看样本量和特征数 print(df.info()) # 看每列类型和缺失情况 print(df.head()) # 打印前五行,确认数据长什么样这段代码的作用是把原始数据读进内存,并快速了解结构。df.info()会列出每一列的非空数量,如果某列非空数明显少于总行数,说明存在缺失值;df.head()是为了直观感受字段含义,比如gender到底是字符串还是 0/1 编码。多数情况下,原始 CSV 的类别列是文本,需要做标签编码或独热编码。
接下来处理缺失值和类别特征:
# 目标列不能有缺失,先去掉目标列为空的行,避免样本标签不干净 df = df.dropna(subset=["enrolled_or_not"]) # 缺失值用众数或中位数填充:分类字段用 most_frequent,数值字段用 median df["academic_score"] = df["academic_score"].fillna(df["academic_score"].median()) df["job_role"] = df["job_role"].fillna(df["job_role"].mode()[0]) # 把字符串类别转成整数,模型才能吃 from sklearn.preprocessing import LabelEncoder le = LabelEncoder() for col in ["gender", "education_level", "job_role", "course"]: df[col] = le.fit_transform(df[col].astype(str))这里的操作顺序很关键:先删目标列缺失的样本,再做特征填充。如果先填充再删除,某些本应被舍弃的脏样本可能参与填充统计,产生不必要的偏差。LabelEncoder适合树模型,因为树模型不关心特征之间的数值比例关系,只关心分裂点;如果是逻辑回归或 SVM,则应该用OneHotEncoder,避免给类别强加大小顺序。
一个容易忽略的点:数值特征要不要标准化。在决策树和随机森林里,归一化不会改变结果,因为树是按阈值切分的。所以我一般对树模型跳过 StandardScaler,这套代码里也省掉了这一步,节省一次fit_transform的调用。如果后面你想在同一份数据上跑 XGBoost 或线性模型,再按需添加标准化即可。
2.2 决策树构建:scikit-learn 参数与可视化
预处理完成后,进入建模环节。树模型有一个非常直观的特性:它通过把特征空间切分成矩形区域,用分段常数去逼近真实曲线。所以训练出来的规则是“年龄小于 30 且岗位是销售,则转化概率超 70%”这种能直接念出来的形式,这也是它在金融、营销场景里仍被广泛使用的核心原因——规则可解释,业务人员能直接用。
构建一棵可控制的决策树:
from sklearn.model_selection import train_test_split from sklearn.tree import DecisionTreeClassifier X = df.drop(columns=["enrolled_or_not"]) y = df["enrolled_or_not"] # 保持随机状态,保证结果可复现;分层抽样保证正负样本比例一致 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, stratify=y, random_state=42 ) # max_depth=4:限制树深,避免一开始就长出完全拟合的大树 clf = DecisionTreeClassifier( max_depth=4, min_samples_split=10, min_samples_leaf=5, criterion="gini", random_state=42, ) clf.fit(X_train, y_train) train_acc = clf.score(X_train, y_train) test_acc = clf.score(X_test, y_test) print(f"Train Acc: {train_acc:.3f}, Test Acc: {test_acc:.3f}")第一次跑这份代码的人最容易犯的错,是max_depth不设限制,直接fit。如果样本量只有几百,一棵无限制的决策树甚至可以把每个训练样本都单独装进自己的叶子节点,训练准确率 100%,测试集却可能掉到 70% 以下。上面把max_depth设为 4,min_samples_leaf设为 5,目的是强制每个叶子至少有 5 个样本,让分裂不那么“钻牛角尖”。
跑完打印训练和测试准确率,如果两个数字差很多,说明过拟合;如果都低,说明特征或参数还没找对。建议在这里先做几组对比,感知一下深度的边界:
# 同一数据,不剪枝 vs 剪枝的效果对比 for depth in [None, 4, 6]: model = DecisionTreeClassifier( max_depth=depth, min_samples_leaf=5, random_state=42, ) model.fit(X_train, y_train) print( f"depth={str(depth):>4} | train={model.score(X_train, y_train):.3f} | " f"test={model.score(X_test, y_test):.3f}" )这段代码用循环串了三档深度,能很直观看到训练准确率一路走高、测试准确率先升后降的过程。此时你已经找到了“过拟合开始”的信号:深度从 4 加到 6,训练准确率涨了,测试准确率反而跌或不动,那 4 就是当前数据下的合理深度。这不是玄学,是树模型对噪声的响应方式——深度越深,单一样本的影响范围越大,训练集里那些极端组合会被当成规律记住。
2.3 决策树调参:剪枝策略与过拟合边界
手调参数只能摸个大概方向,要系统性地找最优组合,用网格搜索。决策树可调的核心参数其实就三个:max_depth、min_samples_split、min_samples_leaf,分别控制树的整体深度、节点继续分裂所需的最小样本数、叶子节点保留的最小样本数。
from sklearn.model_selection import GridSearchCV param_grid = { "max_depth": [3, 5, 8, 12], "min_samples_split": [5, 10, 20], "min_samples_leaf": [2, 5, 10], } grid = GridSearchCV( DecisionTreeClassifier(criterion="gini", random_state=42), param_grid, cv=5, # 5 折交叉验证 scoring="accuracy", n_jobs=-1, # 用满本机 CPU,树模型训练很快 ) grid.fit(X_train, y_train) print(grid.best_params_) print(grid.best_score_)GridSearchCV会把 4 × 3 × 3 = 36 组参数各跑 5 折,一共 180 次拟合。数据量不大时几秒到几十秒出结果。拿到best_params_之后,我建议不要直接拿它重新训练,而是再看一眼cv_results_,确认最优参数附近的分数是不是平稳,避免选了偶然浮动的最佳点。比如max_depth=8和max_depth=12的分数只差 0.3%,选 8 通常更稳。
调完参数后,把树画出来是很有价值的交付物,尤其是面向业务汇报时:
from sklearn.tree import plot_tree import matplotlib.pyplot as plt best_tree = grid.best_estimator_ plt.figure(figsize=(16, 8)) plot_tree( best_tree, feature_names=X.columns.tolist(), class_names=["Not", "Enrolled"], filled=True, rounded=True, fontsize=9, ) plt.savefig("decision_tree.png", dpi=120, bbox_inches="tight")plot_tree会把每个节点的分裂条件、gini 系数、样本数和类别分布画出来。这张图可以直接放进项目报告里,比纯文字更有说服力。注意feature_names一定要用X.columns.tolist(),否则画出来的分裂条件全是x[3] < 0.5,好看但没有业务含义。如果画出来树太深太密,说明参数仍然偏松,先调小深度再画。
3. 随机森林集成:为什么它比单棵决策树更稳
3.1 从决策树到随机森林:Bagging 与特征抽样
单棵决策树的短板很明显:对训练数据敏感,换一批样本,树的结构可能完全变样;深度一大就过拟合。随机森林的思路是用 Bagging 把“不稳定的弱学习器”变成“稳定的强学习器”——对训练集做有放回抽样(bootstrap),每次抽出一份子样本训练一棵树,最后把几十棵、几百棵树的预测结果投票或取平均。
随机森林在 Bagging 基础上又加了一层扰动:每棵树分裂时,不是从全部特征里选最优分裂点,而是先随机抽一个特征子集(max_features),再从这个子集里找最优切分。这样做的好处是让每棵树长得更“不一样”——如果所有树都用同一个强特征去分裂,那它们本质上还是同一棵树,集成之后等于没集成。
决策树和随机森林的差别,用一张表就能说清:
| 对比维度 | 单棵决策树 | 随机森林 |
|---|---|---|
| 结构复杂度 | 单棵树,深度一旦加大极易过拟合 | 多棵树平均,对单棵树过拟合不敏感 |
| 可解释性 | 高,一棵树可以直接画出来 | 低一些,需要借助特征重要性 |
| 训练时间 | 快 | 线性增加,但可用 n_jobs 并行 |
| 抗噪声能力 | 弱,个别极端值会改变分裂点 | 强,多数投票会抵消部分噪声 |
| 主要调参项 | max_depth、min_samples_leaf | n_estimators、max_features、max_depth |
在实际项目中,我的习惯是先跑一棵剪枝后的决策树拿到可解释的规则和基准分数,再跑随机森林看上限能到哪。如果随机森林分数远高于单棵决策树,说明数据里确实存在非线性组合规律;如果两者差不多,说明信号本身已经很明确,不需要上集成。
3.2 随机森林训练与特征重要性分析
在同一个 ExtraaLearn 数据集上,随机森林训练代码和决策树只差一个类名:
from sklearn.ensemble import RandomForestClassifier rf = RandomForestClassifier( n_estimators=200, # 200 棵树 max_features="sqrt", # 分类问题常用 sqrt(特征数) min_samples_leaf=2, # 叶子最小样本数,比单棵树松弛一些 oob_score=True, # 启用袋外样本评分,相当于内置验证集 random_state=42, n_jobs=-1, ) rf.fit(X_train, y_train) print(f"OOB Score: {rf.oob_score_:.3f}") # 约等于交叉验证分数 print(f"Test Score: {rf.score(X_test, y_test):.3f}")oob_score=True是随机森林很有用的一个参数:每棵树只用大约 63% 的样本训练,剩下 37% 没有被抽中,这些“袋外样本”恰好可以用来做验证,省掉一次手动切分。OOB 分数和交叉验证分数通常很接近,如果相差超过 5%,说明训练和验证集的分布存在差异,要回头检查切分逻辑。
特征重要性是随机森林另一个招牌输出:
importance = pd.Series( rf.feature_importances_, index=X.columns, ).sort_values(ascending=False) print(importance) # 画柱状图更直观 importance.plot.barh(figsize=(8, 5), title="Feature Importance")feature_importances_输出每一列对模型预测的贡献占比,所有值加起来等于 1。注意它衡量的是“在树分裂中被使用的频率和带来的纯度提升”,不代表因果方向,更不代表“把不重要的特征删掉模型一定变好”。我在项目里见过有人看到某个特征重要性为 0 就直接删列,结果测试分数反而下降——因为两个强相关特征共享了重要性,删掉一个等于损失一半信息。正确做法是先看相关性矩阵,再决定是否删除冗余特征。
3.3 超参数调节经验:n_estimators 与 max_features
随机森林的调参逻辑和单决策树不太一样。n_estimators不是越大越好,树的数量超过一定阈值后,OOB 分数基本走平,只会白白增加训练时间。我一般从 100 开始,观察 OOB 曲线:
import numpy as np from sklearn.metrics import accuracy_score # 测试不同树数量下的 OOB 分数变化 oob_scores = [] n_list = [10, 50, 100, 200, 300] for n in n_list: m = RandomForestClassifier( n_estimators=n, max_features="sqrt", oob_score=True, random_state=42, n_jobs=-1, ) m.fit(X_train, y_train) oob_scores.append(m.oob_score_) for n, s in zip(n_list, oob_scores): print(f"n_estimators={n} | oob={s:.4f}")如果 100 棵和 300 棵的 OOB 分数差不到 0.5%,就停在 100 棵,把时间留给max_features和min_samples_leaf。max_features是随机森林里影响“随机性”的关键参数:分类默认sqrt,回归默认1.0。如果数据特征少,比如只有 8 列,sqrt(8)约等于 2.8,每棵树只在 3 个特征里选分裂点,树与树的多样性就会很高;如果发现 OOB 分数偏低,可以把max_features调到 0.5 或 0.8,让每棵树看到的特征更多。
# 对比不同 max_features 的效果 for mf in ["sqrt", 0.3, 0.6, 1.0]: model = RandomForestClassifier( n_estimators=100, max_features=mf, min_samples_leaf=3, oob_score=True, random_state=42, n_jobs=-1, ) model.fit(X_train, y_train) print(f"max_features={mf} | oob={model.oob_score_:.4f}")跑完这组对比,你会明显看到“多样性”和“树的质量”之间存在一个平衡点:max_features=1.0时每棵树都从所有特征里选最优分裂,多样性下降;太小则每棵树都像盲人摸象。这个平衡点在不同数据集上位置不同,但用上面这段代码 1 分钟就能找到,比拍脑袋设参数靠谱得多。
4. 时间序列预测股价:针对亚马逊股票数据的完整流程
4.1 股票时序数据的读取与预处理
zip 里的另一个 Notebook 用的是amazon_stocks_prices.xlsx,典型的日线数据:Open、High、Low、Close、Volume,加上日期索引。时间序列数据和普通表格数据最大的区别是顺序不能乱——昨天的样本和今天的样本之间有强依赖关系,任何打乱顺序的操作都会让模型“作弊”,学了不该学的未来信息。
先读取 Excel 并做基础清理:
import pandas as pd # parse_dates 把 Date 列解析成时间类型,index_col 设为日期列 df_stock = pd.read_excel( "amazon_stocks_prices.xlsx", parse_dates=["Date"], index_col="Date", ) # 按时间排序,防止原始文件顺序混乱 df_stock = df_stock.sort_index() # 只保留核心量价字段;全为空的行直接删掉 df_stock = df_stock[["Open", "High", "Low", "Close", "Volume"]] df_stock = df_stock.dropna() print(df_stock.head()) print(df_stock.index.min(), "->", df_stock.index.max())parse_dates=["Date"]会把日期列转成datetime64类型,这样后面做滞后操作或按时间切片时不会出错。sort_index()是容易忽略的一步——有些数据源导出的 Excel 行顺序是乱的,不排序直接做滞后特征,等于把时间线打散了。
股票数据最常遇到的坑是停牌日或节假日导致的缺失行。这份亚马逊数据整体比较干净,多数日期都有交易记录。如果换用自己的数据源,注意dropna()之前先看一下缺失行数占总行数比例,超过 5% 就要考虑是补上前一天价格、还是用前向填充fillna(method="ffill"),而不是一股脑全删。
4.2 滞后特征构造与模型训练
时间序列预测股票价格,常见做法不是直接上 LSTM,而是先把序列问题转成监督学习问题:用过去 N 天的价格构造特征,预测未来第 T 天的价格。这份资源里偏向用树模型做回归预测,这也是一种非常务实的 baseline——LSTM 的输入形状、训练速度、调参复杂度都不是新手友好,树模型跑通后哪怕结果一般,至少让你先把整个流程走通。
构造滞后特征和波动率特征:
# 滞后特征:过去第 1、2、3、5 天的收盘价 for lag in [1, 2, 3, 5]: df_stock[f"close_lag_{lag}"] = df_stock["Close"].shift(lag) # 5 日均线:平滑短期波动 df_stock["ma_5"] = df_stock["Close"].rolling(5).mean() # 波动率:近 5 日收益率标准差 df_stock["volatility_5"] = df_stock["Close"].pct_change().rolling(5).std() # 滞后和滚动窗口产生 NaN,全部去掉后才能进模型 df_stock = df_stock.dropna() # 随便抽一天看特征是否已经生成 print(df_stock.tail())这段代码里shift(lag)是时间序列特征工程的核心操作:把收盘价整体向后平移 lag 行,让第 t 行的close_lag_1等于第 t-1 天的收盘价。这样模型的每个样本都是“已知过去、预测未来”的结构。pct_change()计算的是日收益率,rolling(5).std()得到近 5 天的收益率波动程度,这个特征在股价预测里往往比单纯的价格水平更有信息量。
模型训练时最关键的注意点:切分必须按时间顺序。
from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score # 按时间顺序切分,前 80% 训练,后 20% 测试 cut = int(len(df_stock) * 0.8) train = df_stock.iloc[:cut] test = df_stock.iloc[cut:] # 特征列:排除原始价量列,只保留构造出来的滞后和波动特征 feature_cols = [ "close_lag_1", "close_lag_2", "close_lag_3", "close_lag_5", "ma_5", "volatility_5", ] model = RandomForestRegressor( n_estimators=300, max_features=0.8, min_samples_leaf=3, random_state=42, n_jobs=-1, ) model.fit(train[feature_cols], train["Close"]) pred = model.predict(test[feature_cols])这里用iloc[:cut]和iloc[cut:]而不是train_test_split,是因为train_test_split默认是随机抽样,会把时间顺序完全打乱。如果打了乱,模型会在“用未来数据预测过去”的状态下训练,测出来的分数虚高,换到实盘立刻原形毕露。第一次做时序预测的人 90% 会在这里翻车,后面避坑章节会再展开。
max_features=0.8是我在回归任务里比较常用的设置,比默认的1.0稍微增加一点随机性,让每棵树关注的特征组合更多样,对抑制方差有帮助。
4.3 评估指标:MAE、RMSE 与滚动预测验证
在股票预测里,只用 R² 或者 accuracy 会骗人——尤其当预测目标是价格水平时,价格有很强的自相关性,哪怕你的模型只是“昨天的价格就是今天的价格”,R² 也可能高达 0.98。所以必须同时看误差指标:
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score mae = mean_absolute_error(test["Close"], pred) rmse = mean_squared_error(test["Close"], pred, squared=False) r2 = r2_score(test["Close"], pred) print(f"MAE: {mae:.2f}") print(f"RMSE: {rmse:.2f}") print(f"R2: {r2:.4f}")这三个指标各有偏重:MAE 是绝对误差的平均值,单位是美元,直接反映“平均每次预测差多少钱”;RMSE 给大误差更大的惩罚,如果某个极端日期的预测偏差特别大,RMSE 会明显高于 MAE,两者的差距就是“离群误差惩罚”的体现;R² 在这里只能作为参考——一个预测值始终等于前一天收盘价的傻瓜模型,R² 可能也有 0.9 以上,所以单看它没有任何意义。
更贴近实战的验证方式是滚动预测:
import numpy as np # 手动做多步滚动预测:每次预测 5 天,然后把真实值并入训练窗口 window = 60 horizon = 5 pred_list = [] actual_list = [] for start in range(0, len(df_stock) - window - horizon, horizon): train_part = df_stock.iloc[start:start + window] test_part = df_stock.iloc[start + window:start + window + horizon] model = RandomForestRegressor( n_estimators=200, max_features=0.8, random_state=42, n_jobs=-1, ) model.fit(train_part[feature_cols], train_part["Close"]) pred_part = model.predict(test_part[feature_cols]) pred_list.extend(pred_part) actual_list.extend(test_part["Close"].values) print(f"Rolling MAE: {mean_absolute_error(actual_list, pred_list):.2f}")这段代码模拟的是现实中的交易节奏:每 5 个交易日重新训练一次模型,用过去 60 天的数据预测未来 5 天。每次窗口滑动 5 天,既控制了训练频率,又让模型不断吸收最新行情。这种验证方式对股票这类可能发生结构性变化的数据尤其重要,你能看出模型在不同行情阶段的误差分布,而不是只看一个整体平均值。
5. 实战避坑:金融数据建模中常见的五个坑
5.1 随机切分测试集导致未来数据泄漏
现象:用train_test_split切分股票数据,R² 高达 0.97,换成滚动窗口验证却只有 0.6,相差极大。
原因:train_test_split默认随机抽样,把时间序列完全打乱。模型训练时见过测试集附近的数据,等于提前偷看了答案,测试分数自然虚高。
解决:时间序列数据一律用按顺序切分,train.iloc[:cut]和train.iloc[cut:],或者在交叉验证时使用TimeSeriesSplit。
5.2 全量数据上做缩放和填充导致泄漏
现象:在构造完滞后特征后,对整个 DataFrame 做StandardScaler或fillna(median),训练指标正常,但上实盘后预测结果完全不稳定。
原因:缩放和填充的统计量(均值、中位数)是在包含测试集在内的全量数据上计算的,测试集的信息已经渗入训练过程。这在普通表格数据里可能只是轻微影响,在时序数据里却可能让模型学到未来分布。
解决:先切分训练集和测试集,再在训练集上fit缩放器,然后transform测试集。我用一个习惯保证不犯错:所有预处理步骤里,凡是涉及统计量的操作,都写在切分之后。
5.3 决策树不限制深度,训练集准确率 100%
现象:决策树直接fit不设参数,训练集准确率 100%,测试集准确率 60% 出头,分类报告显示特定类别召回率极低。
原因:决策树分裂到底会把每个训练样本都“记住”,噪声和极端组合也被当成规律。样本量越少,这个问题越严重。
解决:至少设置max_depth和min_samples_leaf两个参数,然后用网格搜索确认边界。判断过拟合的标准很简单:训练准确率显著高于测试准确率,差值超过 5% 就要加大剪枝力度。
5.4 用 R² 评估非平稳的股价预测
现象:模型在预测收盘价时 R² 高达 0.97,看起来非常完美,但画图发现预测曲线只是把真实曲线整体平移了一天。
原因:股票价格是强非平稳序列,今天的价格和昨天高度相关。预测“价格”本身时,一个只会复制最近价格的模型也能拿到很高的 R²,因为它学到的其实是“价格惯性”而不是市场规律。
解决:改用收益率或对数收益率作为预测目标,然后评估MAE和RMSE。如果要保留价格预测,至少同时报告这两个误差指标,并且和“昨天的价格作为预测”这个 baseline 对比。如果模型没有明显优于 baseline,说明没有学到额外信息。
5.5 把特征重要性当成因果结论
现象:随机森林显示volume的特征重要性接近 0.5,业务方据此得出结论“成交量是股价涨跌的原因”,随后按成交量信号入场交易,亏损严重。
原因:特征重要性衡量的是特征在树分裂中的贡献度,和“因果影响”是两回事。股价里所有量价指标互相纠缠,重要性高不代表你能拿它做单因子交易。
解决:把特征重要性当作特征筛选的参考,不要直接做投资决策。要验证特征的真实预测力,应该做单因子回测:用该特征排序分层,观察未来收益在哪些区间显著分化。这一点在后一章会给出具体思路。
6. 进阶技巧:用窗口回测验模型,而不是只看分数
最后一个想分享的技巧,是把训练好的随机森林拿去验证“滞后特征是否真的有用”。具体做法是做一个简单的单因子回测:用close_lag_1或volatility_5做打分因子,看预测值和实际未来收益之间的相关性,而不是只看一贯的 R²。
# 构造未来 3 天的收益率标签 df_stock["future_return"] = df_stock["Close"].shift(-3) / df_stock["Close"] - 1 # 对齐特征 valid = df_stock.dropna() # 计算预测值(先用训练好的模型) from sklearn.ensemble import RandomForestRegressor feature_cols = ["close_lag_1", "close_lag_2", "close_lag_3", "close_lag_5", "ma_5", "volatility_5"] model = RandomForestRegressor(n_estimators=200, max_features=0.8, random_state=42, n_jobs=-1) model.fit(valid[feature_cols], valid["Close"]) valid["pred"] = model.predict(valid[feature_cols]) # 按预测值从低到高分成 5 组,看各组未来收益均值是否单调递增 valid["group"] = pd.qcut(valid["pred"], 5, labels=False) group_return = valid.groupby("group")["future_return"].mean() print(group_return)这段代码做的事情比模型分数更有业务含义:把预测价格按从小到大分成五档,如果最高档的股票未来 3 天平均收益显著高于最低档,说明模型确实捕捉到了一些规律;如果五档收益差不多,说明模型只是在拟合过去的价格惯性,没有交易价值。这是很多量化团队实际在用的因子筛选思路。
从那以后,我每次做完时间序列模型,都会强制走一遍这套流程:先按时序切分,再看误差指标和 baseline 对比,最后用分层的未来收益验证特征有效性。只有这样走完,我才敢说这份资源里的建模流程真正被自己消化了,而不是“notebook 能跑通、分数还挺高”的表面热闹。希望帮到你。
如果你手头正需要一份能同时练分类、集成学习和时序预测的完整案例,这个 zip 值得解压出来认真跑一遍——把两套代码的每个参数都试着自己改一遍,踩过的坑才是真正学到的东西。
本文还有配套的精品资源,点击获取