简介这是一套面向机器学习初学者的股票预测实战项目聚焦决策树、随机森林与时间序列模型在金融数据上的应用。压缩包共7个文件约2.02MB包含两个Jupyter Notebook源代码分类任务与时间序列预测、对应的HTML预览版、股票与公司数据集csv/xlsx以及项目说明文档docx方便直接阅读代码与结果。项目覆盖数据清洗、特征构造、模型训练、交叉验证与误差指标MAE、RMSE、R²分析等完整流程并附有特征重要性解读与投资策略示例帮助学习者将算法理论落地到真实股价预测场景。该资源已有3481人学习下载适合希望提升数据分析与建模技能、了解量化分析基础的人群。1. 机器学习实战资源拆解这个 zip 里究竟有什么在机器学习实战的进阶路上缺的往往不是算法原理而是一套能同时覆盖分类、集成学习和金融时间序列的完整可运行代码。这个“机器学习实战项目——决策树随机森林时间序列预测股价.zip”恰好把三块拼齐了两个 Jupyter NotebookClassification_Decision TreeRandom 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_size0.2, stratifyy, random_state42 ) # max_depth4限制树深避免一开始就长出完全拟合的大树 clf DecisionTreeClassifier( max_depth4, min_samples_split10, min_samples_leaf5, criteriongini, random_state42, ) clf.fit(X_train, y_train) train_acc clf.score(X_train, y_train) test_acc clf.score(X_test, y_test) print(fTrain Acc: {train_acc:.3f}, Test Acc: {test_acc:.3f})第一次跑这份代码的人最容易犯的错是max_depth不设限制直接fit。如果样本量只有几百一棵无限制的决策树甚至可以把每个训练样本都单独装进自己的叶子节点训练准确率 100%测试集却可能掉到 70% 以下。上面把max_depth设为 4min_samples_leaf设为 5目的是强制每个叶子至少有 5 个样本让分裂不那么“钻牛角尖”。跑完打印训练和测试准确率如果两个数字差很多说明过拟合如果都低说明特征或参数还没找对。建议在这里先做几组对比感知一下深度的边界# 同一数据不剪枝 vs 剪枝的效果对比 for depth in [None, 4, 6]: model DecisionTreeClassifier( max_depthdepth, min_samples_leaf5, random_state42, ) model.fit(X_train, y_train) print( fdepth{str(depth):4} | train{model.score(X_train, y_train):.3f} | ftest{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(criteriongini, random_state42), param_grid, cv5, # 5 折交叉验证 scoringaccuracy, 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_depth8和max_depth12的分数只差 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_namesX.columns.tolist(), class_names[Not, Enrolled], filledTrue, roundedTrue, fontsize9, ) plt.savefig(decision_tree.png, dpi120, bbox_inchestight)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_leafn_estimators、max_features、max_depth在实际项目中我的习惯是先跑一棵剪枝后的决策树拿到可解释的规则和基准分数再跑随机森林看上限能到哪。如果随机森林分数远高于单棵决策树说明数据里确实存在非线性组合规律如果两者差不多说明信号本身已经很明确不需要上集成。3.2 随机森林训练与特征重要性分析在同一个 ExtraaLearn 数据集上随机森林训练代码和决策树只差一个类名from sklearn.ensemble import RandomForestClassifier rf RandomForestClassifier( n_estimators200, # 200 棵树 max_featuressqrt, # 分类问题常用 sqrt(特征数) min_samples_leaf2, # 叶子最小样本数比单棵树松弛一些 oob_scoreTrue, # 启用袋外样本评分相当于内置验证集 random_state42, n_jobs-1, ) rf.fit(X_train, y_train) print(fOOB Score: {rf.oob_score_:.3f}) # 约等于交叉验证分数 print(fTest Score: {rf.score(X_test, y_test):.3f})oob_scoreTrue是随机森林很有用的一个参数每棵树只用大约 63% 的样本训练剩下 37% 没有被抽中这些“袋外样本”恰好可以用来做验证省掉一次手动切分。OOB 分数和交叉验证分数通常很接近如果相差超过 5%说明训练和验证集的分布存在差异要回头检查切分逻辑。特征重要性是随机森林另一个招牌输出importance pd.Series( rf.feature_importances_, indexX.columns, ).sort_values(ascendingFalse) print(importance) # 画柱状图更直观 importance.plot.barh(figsize(8, 5), titleFeature 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_estimatorsn, max_featuressqrt, oob_scoreTrue, random_state42, 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(fn_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_estimators100, max_featuresmf, min_samples_leaf3, oob_scoreTrue, random_state42, n_jobs-1, ) model.fit(X_train, y_train) print(fmax_features{mf} | oob{model.oob_score_:.4f})跑完这组对比你会明显看到“多样性”和“树的质量”之间存在一个平衡点max_features1.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_colDate, ) # 按时间排序防止原始文件顺序混乱 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(methodffill)而不是一股脑全删。4.2 滞后特征构造与模型训练时间序列预测股票价格常见做法不是直接上 LSTM而是先把序列问题转成监督学习问题用过去 N 天的价格构造特征预测未来第 T 天的价格。这份资源里偏向用树模型做回归预测这也是一种非常务实的 baseline——LSTM 的输入形状、训练速度、调参复杂度都不是新手友好树模型跑通后哪怕结果一般至少让你先把整个流程走通。构造滞后特征和波动率特征# 滞后特征过去第 1、2、3、5 天的收盘价 for lag in [1, 2, 3, 5]: df_stock[fclose_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_estimators300, max_features0.8, min_samples_leaf3, random_state42, 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_features0.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, squaredFalse) r2 r2_score(test[Close], pred) print(fMAE: {mae:.2f}) print(fRMSE: {rmse:.2f}) print(fR2: {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_estimators200, max_features0.8, random_state42, 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(fRolling 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_estimators200, max_features0.8, random_state42, 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, labelsFalse) group_return valid.groupby(group)[future_return].mean() print(group_return)这段代码做的事情比模型分数更有业务含义把预测价格按从小到大分成五档如果最高档的股票未来 3 天平均收益显著高于最低档说明模型确实捕捉到了一些规律如果五档收益差不多说明模型只是在拟合过去的价格惯性没有交易价值。这是很多量化团队实际在用的因子筛选思路。从那以后我每次做完时间序列模型都会强制走一遍这套流程先按时序切分再看误差指标和 baseline 对比最后用分层的未来收益验证特征有效性。只有这样走完我才敢说这份资源里的建模流程真正被自己消化了而不是“notebook 能跑通、分数还挺高”的表面热闹。希望帮到你。如果你手头正需要一份能同时练分类、集成学习和时序预测的完整案例这个 zip 值得解压出来认真跑一遍——把两套代码的每个参数都试着自己改一遍踩过的坑才是真正学到的东西。本文还有配套的精品资源点击获取
