简介:本资源是一套完整的股票价格预测机器学习实战项目,面向计算机、人工智能、电子信息等专业的本科生及初学者,特别适合作为毕业设计、课程设计或期末大作业参考。项目基于人工神经网络、逻辑回归、随机森林、SVM等多种主流算法实现端到端建模与预测,代码经完整测试并成功通过高分答辩(评审96.5分),具备强实践性与教学示范价值。压缩包共20个文件,含6个核心Python脚本(如ANN.py、RandomForest.py、Logistic.py)、5个CSV/XLSX格式数据集(含清洗后训练集、预测结果及原始小组作业数据)、2个批处理脚本(clean.bat、graph.bat)用于环境准备与可视化,以及DOT图谱、PNG模型结构图和README说明文档,整体仅3.66MB,轻量易部署。目前已有328人下载学习,配套数据预处理、特征工程、多模型对比评估及结果可视化全流程均已实现,可直接运行复现,亦支持在现有结构上拓展新算法或适配其他时序预测任务。
1. 股票价格预测不是玄学:一份跑通了的多模型Python实战包,专治毕设焦虑和课设 deadline
你是不是也经历过——导师甩来一句“用机器学习做点股票预测”,然后打开百度搜“python 股票预测 代码”,结果全是调用 yfinance 下个收盘价、用 sklearn.fit 一跑就完事的“Hello World”级 demo?数据没清洗、特征没构造、模型没对比、结果不回测,答辩时被问一句“你这个 MAPE 是怎么算的?训练集泄露了吗?”,当场哑火。这份源码包不是那种玩具级 demo,它来自某高校计算机专业大四学生的高分毕业设计(评审分96.5),完整覆盖从原始 Excel 数据加载、缺失值/异常值清洗(clean.bat)、标准化(normalized_train_test.csv)、到 ANN / Logistic / RF / SVM 四类主流算法并行建模、再到可视化决策树(RF.dot → RF.png)、最终输出可验证的 predict_result.csv 全流程。它不教你“什么是过拟合”,而是让你亲手看到:为什么随机森林在波动期比逻辑回归稳,为什么 ANN 需要归一化而 RF 不怕量纲,为什么 train_test.csv 和 normalized_predict.csv 必须严格分离。适合正在赶毕设、课程设计或期末大作业的你——不是拿来即用的黑匣子,而是能拆、能改、能讲清楚每一步为什么这么做的脚手架。
2. 从 Excel 到标准化数据:清洗与预处理是预测准确率的真正分水岭
2.1 原始数据结构解析:小组作业数据-2021.xlsx 的真实字段含义
项目提供的小组作业数据-2021.xlsx是整个流程的起点,但它的结构并不像 Pandas 教程里那样规整。打开后你会发现三张 sheet:原始数据、技术指标、标签。这不是随意命名——原始数据包含日期、开盘价、最高价、最低价、收盘价、成交量(单位:手);技术指标是人工计算好的 MACD、RSI、布林带上下轨等衍生特征;标签则是目标变量:next_close_change_pct(下一日收盘价相对当日的涨跌幅百分比)。关键点在于:所有模型预测的都不是“明天收盘价是多少”,而是“明天涨跌幅度百分比”。这直接决定了损失函数的选择(MSE 可用,但 MAE 更鲁棒)和评估逻辑(不能直接比预测值和真实价格,必须比变化率)。我第一次跑Logistic.py时发现 AUC 只有 0.53,后来查了半小时才发现——逻辑回归被误用来做回归任务(预测连续涨跌幅),而代码里却用sklearn.linear_model.LogisticRegression做分类(把涨跌强行二值化),导致结果失真。正确做法是:若坚持用逻辑回归,应先对next_close_change_pct设阈值(如 >0.5% 为 1,<-0.5% 为 -1,其余为 0),再走多分类;否则就该换LinearRegression或Ridge。
# 在 Logistic.py 开头添加字段检查(强烈建议你加) import pandas as pd raw_data = pd.read_excel("小组作业数据-2021.xlsx", sheet_name="标签") print("标签列统计:") print(raw_data['next_close_change_pct'].describe()) print("\n分布直方图提示:") print(raw_data['next_close_change_pct'].hist(bins=50))提示:运行这段代码后,你会看到
next_close_change_pct的均值接近 0,标准差约 1.8,且存在长尾。这意味着简单二值化(>0 为涨)会制造大量边界模糊样本,直接拉低分类器性能。这也是为什么后续result.py中 RF 和 ANN 的表现明显优于逻辑回归——它们天然适配回归任务。
2.2 clean.bat:Windows 批处理脚本背后的清洗逻辑与可移植改造
clean.bat看似简单,双击就能运行,但它封装了三个关键清洗动作:
- 删除
原始数据中日期为空或价格为 0 的整行; - 对
技术指标表中 RSI > 100 或 <0 的异常值,用前后 5 日均值插补; - 将
标签表中next_close_change_pct绝对值 >15% 的样本标记为outlier并移至单独 sheet。
但问题来了:如果你用的是 macOS 或 Linux,.bat文件根本执行不了。别急,clean.bat的核心逻辑其实就对应 Python 里的几行 Pandas 操作。我把它的等效 Python 版本抽出来,放在utils/cleaner.py(你可自行创建):
# utils/cleaner.py import pandas as pd import numpy as np def clean_stock_data(raw_path="小组作业数据-2021.xlsx"): # 读取三张表 raw = pd.read_excel(raw_path, sheet_name="原始数据") tech = pd.read_excel(raw_path, sheet_name="技术指标") label = pd.read_excel(raw_path, sheet_name="标签") # 步骤1:删除原始数据中的空日期和零价格 raw = raw.dropna(subset=['日期']) raw = raw[(raw['开盘价'] != 0) & (raw['收盘价'] != 0)] # 步骤2:RSI 异常值插补(tech 表中 'RSI' 列) tech['RSI'] = tech['RSI'].apply(lambda x: np.nan if x > 100 or x < 0 else x) tech['RSI'] = tech['RSI'].interpolate(method='nearest', limit_direction='both', limit=5) # 步骤3:label 表中极端涨跌幅标记 label['is_outlier'] = np.abs(label['next_close_change_pct']) > 15 outliers = label[label['is_outlier']].copy() label = label[~label['is_outlier']].copy() # 保存清洗后数据 with pd.ExcelWriter("cleaned_data.xlsx") as writer: raw.to_excel(writer, sheet_name="cleaned_raw", index=False) tech.to_excel(writer, sheet_name="cleaned_tech", index=False) label.to_excel(writer, sheet_name="cleaned_label", index=False) outliers.to_excel(writer, sheet_name="outliers", index=False) print("✅ 清洗完成:cleaned_data.xlsx 已生成") if __name__ == "__main__": clean_stock_data()参数说明:
limit=5表示最多用前后 5 行数据插补,避免跨周期污染;limit_direction='both'确保首尾也能插补。这个脚本比clean.bat更透明——你知道每一行在做什么,也方便你根据实际数据调整阈值(比如把>15改成>10)。
2.3 归一化陷阱:为什么 normalized_train_test.csv 和 normalized_predict.csv 必须分开生成?
项目里有两个归一化文件:normalized_train_test.csv(用于模型训练/验证)和normalized_predict.csv(用于最终预测)。初学者常犯的错误是:用MinMaxScaler分别对两个文件独立归一化。这是致命错误!因为预测时模型看到的“归一化后特征”必须和训练时的尺度完全一致。正确做法是:只用训练集数据拟合 scaler,再用同一个 scaler 转换测试集和预测集。train_test.csv是原始未归一化的训练+验证数据(按时间切分,前 70% 训练,后 30% 验证),而normalized_train_test.csv是它被统一 scaler 处理后的结果。你可以用以下代码验证 scaler 是否一致:
from sklearn.preprocessing import MinMaxScaler import pandas as pd # 加载原始 train_test.csv(注意:不是 normalized_ 开头的!) df_train_test = pd.read_csv("train_test.csv") df_predict = pd.read_csv("predict.csv") # 注意:predict.csv 是原始未归一化预测数据 # 只用训练部分(前 70%)拟合 scaler split_idx = int(len(df_train_test) * 0.7) scaler = MinMaxScaler() scaler.fit(df_train_test.iloc[:split_idx, :-1]) # 排除最后一列标签 # 转换全部 train_test 和 predict norm_train_test = scaler.transform(df_train_test.iloc[:, :-1]) norm_predict = scaler.transform(df_predict) # 保存(这才是真正的 normalized_train_test.csv 和 normalized_predict.csv) pd.DataFrame(norm_train_test).to_csv("normalized_train_test_verified.csv", index=False) pd.DataFrame(norm_predict).to_csv("normalized_predict_verified.csv", index=False)关键参数:
scaler.fit()必须只传入训练子集(iloc[:split_idx, :-1]),且transform()时传入的必须是全部特征列(:-1排除标签列)。如果漏掉.fit()这一步,transform()会报错;如果用全量df_train_test拟合,就造成未来信息泄露。
3. 四大模型逐个击破:从逻辑回归到随机森林,参数设置与效果差异实测
3.1 Logistic.py:当逻辑回归被强行用于回归任务时,我们到底在优化什么?
Logistic.py这个文件名极具迷惑性。它实际调用的是sklearn.linear_model.LogisticRegression,但目标变量next_close_change_pct是连续值。代码里做了个隐式转换:y_binary = (y > 0).astype(int),把涨跌转成 0/1 分类。问题在于——股票价格变化本身是强连续过程,硬切成二分类丢失了幅度信息。我在本地复现时,把Logistic.py的评估指标从accuracy_score改成f1_score(macro),发现 F1 只有 0.48,远低于 RF 的 0.72。更合理的做法是:要么改用LinearRegression(见下文),要么保留分类但增加第三类“横盘”(abs(y) < 0.3)。以下是修正版Logistic.py的核心段落:
# 替换原 Logistic.py 中的 y_binary 构造逻辑 y = df['next_close_change_pct'].values # 三分类:-1=跌,0=横盘,1=涨 y_triple = np.zeros_like(y, dtype=int) y_triple[y > 0.3] = 1 # 涨幅 >0.3% y_triple[y < -0.3] = -1 # 跌幅 <-0.3% # y_triple[y between -0.3 and 0.3] remains 0 from sklearn.linear_model import LogisticRegression model = LogisticRegression(multi_class='ovr', max_iter=1000) # ovr: one-vs-rest model.fit(X_train, y_triple) y_pred = model.predict(X_test)参数说明:
multi_class='ovr'是多分类必备参数;max_iter=1000防止收敛失败(原代码默认 100,常报 ConvergenceWarning)。这样改后,F1 macro 提升到 0.59,虽仍不如 RF,但至少逻辑自洽。
3.2 random forest.py:为什么随机森林在金融时序中抗噪性强?看特征重要性排序
random forest.py是整个包里最稳健的模型。它没用任何归一化,直接吃原始特征(包括价格、成交量、RSI 等不同量纲变量),却依然跑出最优 MAE(平均绝对误差 0.82%)。原因在于:RF 的基学习器是决策树,而树模型天然对特征尺度不敏感,且通过 bagging 降低方差。更重要的是,它能输出feature_importances_,告诉你哪些因子真正在驱动预测。我在运行后打印了前 5 重要特征:
| 特征名 | 重要性得分 |
|---|---|
| RSI_14 | 0.182 |
| 成交量_5日均值 | 0.156 |
| 布林带上轨 | 0.124 |
| 当日涨跌幅 | 0.098 |
| MACD_diff | 0.087 |
注意:
RSI_14(14日相对强弱指数)排第一,印证了技术分析在短期预测中的有效性;而原始价格(开盘、收盘)未进前五,说明单纯看价格线性外推价值有限。这个排序可以直接指导你删减特征——比如去掉重要性<0.02 的换手率列,模型速度提升 30%,MAE 仅上升 0.03%。
3.3 ANN.py:三层全连接网络的结构选择与早停策略
ANN.py用的是 Keras,结构为Dense(64, relu) → Dense(32, relu) → Dense(1)。但原始代码没加 Dropout 和早停(EarlyStopping),导致在验证集上第 42 轮开始过拟合(val_loss 上升)。我增加了以下两处关键修改:
from tensorflow.keras.callbacks import EarlyStopping from tensorflow.keras.layers import Dropout model = Sequential([ Dense(64, activation='relu', input_shape=(X_train.shape[1],)), Dropout(0.3), # 防止第一层过拟合 Dense(32, activation='relu'), Dropout(0.2), # 防止第二层过拟合 Dense(1) ]) # 早停:监控 val_loss,连续 15 轮不下降则停止 early_stopping = EarlyStopping( monitor='val_loss', patience=15, restore_best_weights=True # 自动加载最优权重 ) history = model.fit( X_train, y_train, validation_data=(X_val, y_val), epochs=200, batch_size=32, callbacks=[early_stopping], verbose=0 )参数说明:
patience=15是经验值,太小(如 5)易早停,太大(如 30)浪费算力;restore_best_weights=True是关键,否则模型保存的是最后 epoch 的权重,可能已过拟合。加了这两项后,训练轮次从固定 200 降到平均 127,val_loss 稳定在 0.41,比原版低 12%。
3.4 SVM.py:核函数选择与 C/gamma 参数的暴力搜索实践
SVM.py默认用SVR(kernel='rbf'),但没调参。我用GridSearchCV对C(惩罚系数)和gamma(RBF 核宽度)做了网格搜索:
from sklearn.svm import SVR from sklearn.model_selection import GridSearchCV param_grid = { 'C': [0.1, 1, 10, 100], 'gamma': ['scale', 'auto', 0.001, 0.01, 0.1, 1] } svr = SVR(kernel='rbf') grid_search = GridSearchCV( svr, param_grid, cv=5, # 5折交叉验证 scoring='neg_mean_absolute_error', n_jobs=-1 ) grid_search.fit(X_train, y_train) print("✅ 最优参数:", grid_search.best_params_) print("✅ 最优 CV MAE:", -grid_search.best_score_)结果:C=10, gamma=0.01组合最优,CV MAE 为 0.87%,比默认参数(C=1, gamma='scale')的 1.02% 显著提升。这说明——SVM 对超参极其敏感,不调参等于白用。但注意:GridSearchCV 很慢,生产环境建议换BayesSearchCV(scikit-optimize 库),速度提升 5 倍。
4. 避坑指南:那些让我重跑三次才定位的隐藏雷区
4.1 现象:RF.dot生成后无法用 graphviz 渲染成RF.png,报错 “failed to execute WindowsPath”
原因:graph.bat脚本里写的是dot -Tpng RF.dot -o RF.png,但它依赖系统已安装 Graphviz,且dot.exe必须在 PATH 环境变量中。Windows 用户常只下载 Graphviz 安装包,却忘记勾选 “Add Graphviz to the system PATH for all users”。更隐蔽的是:某些新版 Graphviz 安装后dot.exe实际路径是C:\Program Files\Graphviz2.44\bin\dot.exe,而 PATH 里只加了C:\Program Files\Graphviz2.44,缺了\bin。
解决:
- 重新安装 Graphviz,务必勾选 PATH 选项;
- 或手动将
C:\Program Files\Graphviz2.44\bin添加到系统环境变量 PATH; - 终端输入
dot -V验证是否返回版本号; - 若仍失败,在
graph.bat第一行加@echo off,第二行加cd /d "%~dp0"(确保在当前目录执行)。
4.2 现象:result.py运行后预测结果.xlsx里日期列全是数字(如 44562),而非 “2021/1/4”
原因:小组作业数据-2021.xlsx中的日期列在 Excel 里显示为日期格式,但底层存储是 Excel 序列号(1900年1月1日为1)。Pandas 读取时默认按 float 解析,44562就是 2021年1月4日。result.py直接把这串数字写进 Excel,没做格式转换。
解决:在result.py写入前加类型转换:
# 假设 df_result 是你要写入的 DataFrame df_result['日期'] = pd.to_datetime(df_result['日期'], unit='D', origin='1899-12-30') # 或更稳妥:从原始 Excel 读取时就指定日期列 df_raw = pd.read_excel("小组作业数据-2021.xlsx", sheet_name="原始数据", parse_dates=['日期'])4.3 现象:SBS.py(序列后向选择)运行极慢,10 分钟没出结果
原因:SBS.py实现的是穷举式后向消除(从全特征开始,每次删一个最不重要特征),特征数 N=23 时,需评估 C(23,1)+C(23,2)+...+C(23,22) ≈ 8e6 次模型训练。原代码用LinearRegression还好,但若你改成RandomForestRegressor,单次训练 2 秒,总耗时超 180 天。
解决:
- 立刻停用 SBS,改用基于树模型的
SelectFromModel(快 1000 倍):
from sklearn.feature_selection import SelectFromModel selector = SelectFromModel(RandomForestRegressor(n_estimators=50), threshold='median') X_train_selected = selector.fit_transform(X_train, y_train)- 或用
RFE(递归特征消除),指定n_features_to_select=10限定数量。
4.4 现象:predict.csv预测结果和predict_result.csv对不上,差了一行
原因:predict.csv是原始未归一化数据,但predict_result.csv是模型对normalized_predict.csv的输出。而normalized_predict.csv是用训练集 scaler 转换的——如果predict.csv行数和train_test.csv列数不一致(比如predict.csv少了一列特征),scaler.transform 会静默失败,输出维度错乱。
解决:
- 运行前强制校验:
p_df = pd.read_csv("predict.csv") t_df = pd.read_csv("train_test.csv") assert p_df.shape[1] == t_df.shape[1] - 1, f"列数不匹配!predict 有{p_df.shape[1]}列,train_test 需要{t_df.shape[1]-1}列(不含标签)"predict.csv必须和train_test.csv有完全相同的列名顺序,连空格都不能差。
4.5 现象:ANN.py训练时 GPU 显存爆满,CUDA out of memory
原因:Keras 默认启用全部 GPU 显存。而本项目数据量小(<5000 行),根本用不到 GPU,强行启用反而因显存分配机制导致 OOM。
解决:在ANN.py开头加限制:
import os os.environ["CUDA_VISIBLE_DEVICES"] = "-1" # 强制 CPU 模式 # 或限制显存增长 import tensorflow as tf gpus = tf.config.experimental.list_physical_devices('GPU') if gpus: try: for gpu in gpus: tf.config.experimental.set_memory_growth(gpu, True) except RuntimeError as e: print(e)5. 模型对比与结果验证:用滚动回测代替静态划分,看清谁真能赚钱
5.1 为什么 train_test.csv 的 70/30 划分在金融场景中不靠谱?
train_test.csv是按时间顺序静态切分的:前 70% 当训练,后 30% 当测试。这在图像识别中 OK,但在股票预测中是灾难——它假设未来分布和过去完全一致。而现实是:2020 年疫情黑天鹅、2021 年教培行业政策,会让模型在“后 30%”上彻底失效。真正可信的验证方式是滚动时间序列交叉验证(Rolling Time Series CV)。我用sklearn.model_selection.TimeSeriesSplit重构了验证逻辑:
from sklearn.model_selection import TimeSeriesSplit import numpy as np # 假设 X, y 是归一化后的特征和标签 tscv = TimeSeriesSplit(n_splits=5) # 分 5 折 mae_scores = [] for train_idx, test_idx in tscv.split(X): X_train_fold, X_test_fold = X[train_idx], X[test_idx] y_train_fold, y_test_fold = y[train_idx], y[test_idx] # 以 RF 为例 rf = RandomForestRegressor(n_estimators=100) rf.fit(X_train_fold, y_train_fold) y_pred = rf.predict(X_test_fold) mae_scores.append(np.mean(np.abs(y_pred - y_test_fold))) print(f"滚动 CV MAE: {np.mean(mae_scores):.3f} ± {np.std(mae_scores):.3f}") # 输出:滚动 CV MAE: 0.892 ± 0.041 (比静态划分的 0.821 更保守)关键点:
TimeSeriesSplit保证每折的训练集都在测试集之前,杜绝未来信息泄露。5 折的 std=0.041,说明模型稳定性尚可;若 std>0.15,则需警惕过拟合。
5.2 预测结果的业务验证:不只是 MAE,要看方向胜率和盈亏比
predict_result.csv只给了数值预测,但交易真正关心的是:模型预测涨,第二天真涨的概率有多高?我写了段验证脚本,计算三个业务指标:
| 指标 | 计算公式 | 本项目实测值 | 说明 |
|---|---|---|---|
| 方向准确率 | count(pred>0 and actual>0) / count(pred>0) | 58.3% | 预测上涨时,实际涨的概率 |
| 盈亏比 | mean(abs(actual) when pred>0) / mean(abs(actual) when pred<0) | 1.32 | 预测对的平均涨幅 / 预测错的平均跌幅 |
| 夏普比率(简化) | (mean(return) - 0.02/250) / std(return) | 0.41 | 年化超额收益 / 波动率,>0.3 算合格 |
# 基于 predict_result.csv 计算 df_pred = pd.read_csv("predict_result.csv") df_pred['pred_dir'] = (df_pred['pred'] > 0).astype(int) # 1=涨, 0=跌 df_pred['actual_dir'] = (df_pred['actual'] > 0).astype(int) # 方向准确率(只看预测涨的情况) up_mask = df_pred['pred_dir'] == 1 acc_up = ((df_pred.loc[up_mask, 'actual_dir'] == 1).sum() / up_mask.sum()) # 盈亏比 up_returns = df_pred.loc[up_mask, 'actual'].abs().mean() down_returns = df_pred.loc[~up_mask, 'actual'].abs().mean() profit_ratio = up_returns / down_returns if down_returns != 0 else np.inf结果解读:58.3% 的方向准确率,意味着每 10 次预测上涨,约 6 次正确——这已超过随机猜测(50%),具备初步交易价值;盈亏比 1.32 表明“赚的时候比亏的时候多”,是正期望策略的基石。这才是答辩时能让导师眼前一亮的硬指标,而不是干巴巴的“MAE=0.82”。
5.3 从毕设到落地:如何把这份代码改成你的实盘信号生成器?
这份源码的终极价值,不是交差,而是成为你个人量化系统的种子。我把它升级为实盘可用的最小闭环,只需三步:
- 数据自动更新:替换
小组作业数据-2021.xlsx为实时接口。用akshare库(国内免费)替代 yfinance:
import akshare as ak df = ak.stock_zh_a_hist(symbol="000001", period="daily", start_date="20200101", end_date="20231231", adjust="qfq") # 返回包含 open/high/low/close/volume 的 DataFrame- 信号生成:在
result.py末尾加:
last_pred = y_pred[-1] # 最新预测值 if last_pred > 0.5: signal = "BUY" elif last_pred < -0.5: signal = "SELL" else: signal = "HOLD" print(f"⏰ {datetime.now().strftime('%Y-%m-%d')} 信号: {signal} (预测涨跌幅: {last_pred:.2f}%)")- 结果持久化:把每次信号写入 SQLite,方便回溯:
import sqlite3 conn = sqlite3.connect("trading_signals.db") df_signal = pd.DataFrame([{"date": datetime.now().date(), "signal": signal, "pred": last_pred}]) df_signal.to_sql("signals", conn, if_exists="append", index=False)从那以后我每次部署新策略,都强制走一遍滚动回测 + 方向胜率验证 + 信号日志写入,哪怕多花 20 分钟。因为一次没验证的“成功”,可能在实盘亏掉三个月工资。希望帮到你。
本文还有配套的精品资源,点击获取