news 2026/9/26 14:16:48

机器学习股票价格预测:避开数据泄漏与过拟合陷阱,构建可靠模型

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
机器学习股票价格预测:避开数据泄漏与过拟合陷阱,构建可靠模型

简介:这份资源围绕股票价格分析与预测,演示如何用机器学习完成从数据清洗、特征构造到模型训练与评估的完整流程,适合具备一定Python基础、希望系统入门金融机器学习的开发者。压缩包体积仅45KB,共包含2个文件:一个可直接运行的Python实践脚本与一份依赖清单文本,前者承载建模主流程,后者标明运行环境所需组件。资源已被441人学习,属于小而精的实操案例。内容涉及数据预处理中的缺失值与异常值处理、标准化归一化及时间序列的移动平均/差分去趋势,特征工程涵盖MACD、RSI等技术指标构造,模型层面覆盖线性回归、决策树、随机森林、支持向量机与LSTM/GRU等算法,并给出训练/测试集划分、k折交叉验证、MSE/RMSE/R^2评估及网格搜索/随机搜索调参思路,便于对照代码理解从数据到预测的每个环节。同时强调政策变化、突发事件等非量化因素对模型的影响,提醒结合专业知识与风控策略使用,帮助读者建立更全面的建模认知。

1. 机器学习算法预测股票价格:先绕过那个最诱人的误区

每隔一段时间,就会有人把「基于机器学习算法的股票市场股票价格的分析及预测」这类项目从网上下载下来,打开压缩包,看到里面的 .ipynb 和 .py 文件,然后问同一个问题:这个能让我赚钱吗?我的回答通常泼冷水——如果这个包能稳定赚钱,作者不会把它传成压缩包。但它依然值得做,而且是个极其适合练手机器学习全流程的方向。因为股票数据的分析预测绕不开一整套真实的工作链:数据清洗、特征构造、模型选型、回测验证。这套链路的通用性极强,做完一次,你会真正理解什么是过拟合、什么是数据泄漏、为什么测试集的准确率是假的。这篇文章适合两类人:一是准备做课程设计或毕业设计,需要一个能讲清楚原理和代码的项目;二是刚入门机器学习,想找一个结构化程度高、数据免费可得的数据集来练手。目标很具体:让你把压缩包里的东西拆开、理解、改造,最终跑出一版属于自己的预测流程。

2. 股票数据预测的底层逻辑:先搞清你预测的到底是什么

2.1 预测目标不是涨跌,而是价格区间和概率

很多人拿到股票数据后第一件事就是跑回归,试图直接预测明天的收盘价。这个思路不是不对,但落地时会有个很尴尬的问题:价格预测的误差评估非常反直觉。今天股价是 10 块,模型预测明天是 10.05,误差 0.5%,看起来挺准。但股价的绝对数值包含了公司基本面、市场情绪、板块轮动等多种因素,而这些因素大多数不在你的特征矩阵里。你用一个只包含历史价格和成交量的模型去预测绝对价格,本质上是在用一个残缺的信息集预测一个完整信息集决定的结果,误差小是运气,误差大是常态。

我一般会在项目的第一步把预测目标改掉:不直接回归收盘价,而是构造一个二分类或三分类目标。比如「明日收盘价相对今日收盘价的涨跌幅是否超过 0.5%」,或者「未来 5 日最高价是否突破某个阈值」。这样做有两个好处。第一,分类问题的评估指标(准确率、F1、混淆矩阵)在汇报时远比回归的 MSE 好解释;第二,分类目标天然平滑了价格数值的噪声,模型更容易学到稳定的模式。如果你保留回归任务,也建议把预测目标从「绝对价格」换成「收益率」或者「对数收益率」,这两个序列的统计特性更平稳,也更适合作为机器学习的输入。

2.2 特征工程是决定模型上限的隐性环节

这个项目的特征工程,比选哪个模型更值得花时间。我见过太多人直接把「开盘价、收盘价、最高价、最低价、成交量」五列原始数据喂进随机森林,然后抱怨准确率上不去。这不是模型的问题,是特征里没有信息增量。常见做法是构造三类特征。

第一类是技术指标类,用 pandas 的 rolling 窗口计算。均线 MA5、MA10、MA20 反映趋势,RSI 反映超买超卖,MACD 的 DIF 和 DEA 反映动量变化,布林带的上中下轨反映波动区间。第二类是时序滞后类,把昨日的收盘价、今日的开盘价、过去 N 日的收益率均值作为特征。这类特征能让模型感知到「最近发生了什么」,而不仅仅是「当前是什么状态」。第三类是交叉特征,比如成交量与价格同时放大的信号、收盘价在布林带中的相对位置。这类特征量少但往往最关键。

需要注意的是,特征构造有个容易翻车的点:窗口计算时的边界填充。pandas 的 rolling 函数默认在窗口不足时产生 NaN,很多人直接 dropna() 删掉,导致早期样本全部消失。我建议用 min_periods 参数控制最小窗口数,比如df['MA10'] = df['close'].rolling(window=10, min_periods=5).mean()。这样前 4 行用不满 10 日的数据先算出一个弱化的均值,不至于直接砍掉一段样本。

3. 数据获取与预处理:拿到干净可用的股票数据

3.1 免费数据源的选择与取舍

写这类项目最常用的数据源是 yfinance(Yahoo Finance 的 Python 接口)和 Tushare。如果面向国内用户做课程设计,Tushare 更友好;如果追求零配置、跨平台,yfinance 更方便。我一般推荐一个混合方案:先用 yfinance 快速验证流程,再用 Tushare 拉 A 股数据做正式实验。原因是 yfinance 偶尔会出现数据缺失或停牌日处理不规范的情况,而 Tushare 的 A 股日线数据质量更稳定,但需要注册 token。

下面是一个用 yfinance 拉取数据的基准代码:

import yfinance as yf import pandas as pd # 拉取某只股票近两年的日线数据 df = yf.download( "600519.SS", # 贵州茅台,A股后缀 .SS start="2022-01-01", end="2024-01-01", auto_adjust=True # 自动复权,避免除权跳空影响 ) # 只保留需要的列,并重命名 df = df[["Open", "High", "Low", "Close", "Volume"]].copy() df.columns = ["open", "high", "low", "close", "volume"] # 检查缺失值比例 print(f"缺失值数量: {df.isnull().sum().sum()}") print(f"数据量: {len(df)} 行")

yfinance 的auto_adjust=True参数值得专门说明。股票在除权除息日会产生价格跳空,如果不做复权处理,模型会把这种「假价格变化」当成真实信号,导致训练出的模型在实盘时频繁误判。auto_adjust会自动按复权因子修正历史价格,保证价格序列连续。这个开关一定要打开。

3.2 训练集与测试集的分割:不能用随机打乱

股票数据是有时间顺序的序列数据,分割时必须遵循时间顺序,绝不能像图像分类那样用train_test_split随机打乱。否则模型会看到未来的数据,测试集的准确率会虚高到让你误以为发现了圣杯。这是一个项目中最隐蔽但也最常见的错误。

推荐的划分方式是按时间切片:

# 按时间顺序分割:前 80% 训练,后 20% 测试 split_idx = int(len(df) * 0.8) train_df = df.iloc[:split_idx].copy() test_df = df.iloc[split_idx:].copy() # 特征列与标签列 feature_cols = ["open", "high", "low", "volume", "ma5", "ma10", "rsi", "momentum"] X_train = train_df[feature_cols].values y_train = train_df["target"].values X_test = test_df[feature_cols].values y_test = test_df["target"].values

这里有个容易被忽略的坑:特征列中的均线、RSI 等指标是用整个数据集的滚动窗口算出来的。如果在分割数据集之后再计算特征,那么训练集前 N 行的均线值其实用到了测试集的数据,这属于特征泄漏,比标签泄漏更隐蔽。我一般会把「构造特征」这个步骤放在分割之前完成,但只在训练集上计算 rolling 均值,测试集的特征用训练集的统计量来填充。更严格的做法是分两段独立计算特征,先用前 80% 的数据算出一个基准窗口,再让测试集的特征从训练集末尾延续过来。

4. 算法选型与训练:从「十大机器学习算法」里挑适合股票预测的

4.1 先做算法横向对比:用一个小实验定方向

如果你在搜索引擎里搜「机器学习算法 股票价格预测」,会看到各种帖子把十大机器学习算法列了一遍,从线性回归到朴素贝叶斯,仿佛每个都能用。但实际上,经过我的反复测试,真正在股票这类带噪声、非平稳、弱信号的数据上表现稳定的,通常集中在三类模型里:集成树模型(XGBoost/LightGBM 或随机森林)、线性模型(逻辑回归用于分类目标)、以及长短期记忆网络 LSTM。支持向量机在小样本上表现尚可,但在金融时序上调整核函数的收益很低,性价比不如树模型。

我不建议上来就选 LSTM。原因很实际:LSTM 对数据量要求高、训练慢、调参维度多,课程设计的展示周期内很难调到理想状态。更务实的路线是先用随机森林或 XGBoost 跑出一个基线结果,如果效果不够再考虑 LSTM。下面用 scikit-learn 的随机森林配合简单网格搜索做演示:

from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import GridSearchCV from sklearn.metrics import accuracy_score # 定义模型与候选参数 model = RandomForestClassifier(random_state=42, class_weight="balanced") param_grid = { "n_estimators": [100, 200], "max_depth": [3, 5, 7], "min_samples_leaf": [5, 10] } grid = GridSearchCV( model, param_grid, scoring="f1", # 用 F1 而不是准确率,因为涨跌样本通常不平衡 cv=5, n_jobs=-1 ) grid.fit(X_train, y_train) best_model = grid.best_estimator_ y_pred = best_model.predict(X_test) print(f"最优参数: {grid.best_params_}") print(f"测试集 F1: {accuracy_score(y_test, y_pred):.4f}")

用class_weight="balanced"是处理股票预测不平衡问题的关键参数。多数股票在多数交易日里涨跌幅都在 ±0.5% 以内,如果按涨/跌二分类,正负样本基本接近五五开,但如果按「大涨/大跌/震荡」三分类,震荡类经常占 70% 以上。不平衡样本下如果直接用准确率做网格搜索的评分,模型会倾向把所有样本都预测成多数类,F1 能更真实反映少数类的识别能力。

4.2 时间序列交叉验证:网格搜索里的隐藏漏洞

上面的代码里有个隐藏问题:GridSearchCV默认使用 KFold 交叉验证,会随机打乱样本,这在时序数据上是错的。随机打乱意味着每折训练集中都混入了未来数据,验证分数会被抬高。正确做法是使用TimeSeriesSplit:

from sklearn.model_selection import TimeSeriesSplit # 用时间序列交叉验证替代默认 KFold tscv = TimeSeriesSplit(n_splits=5) grid = GridSearchCV( model, param_grid, scoring="f1", cv=tscv, n_jobs=-1 )

TimeSeriesSplit的机制是:第一折用前 20% 训练、后 20% 验证;第二折用前 40% 训练、后 20% 验证,依此类推。这样每一折都严格保证训练数据在时间上先于验证数据。这种验证方式下的分数通常会比随机 KFold 低几个百分点,但更接近真实表现。如果你在汇报时只说「准确率 80%」而没说清楚验证方式,内行一听就知道是随机分割的虚高结果。

4.3 让 LSTM 作为进阶方案:什么时候值得换

如果树模型已经能在测试集上拿到稳定高于阈值的 F1,我通常不推荐再上 LSTM。但如果你的数据量超过两三年日线数据,且希望捕捉更复杂的时序依赖,LSTM 可以作为对比模型写进报告。下面是一个用 Keras 实现的最简结构:

import numpy as np from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout # 构造 3D 输入:样本数, 时间步长, 特征数 def make_sequences(X, y, seq_len=10): X_seq, y_seq = [], [] for i in range(seq_len, len(X)): X_seq.append(X[i - seq_len:i]) y_seq.append(y[i]) return np.array(X_seq), np.array(y_seq) # 需要先对特征做标准化,否则 LSTM 收敛很慢 from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test) X_train_seq, y_train_seq = make_sequences(X_train_scaled, y_train, seq_len=10) X_test_seq, y_test_seq = make_sequences(X_test_scaled, y_test, seq_len=10) model = Sequential() model.add(LSTM(64, return_sequences=True, input_shape=(10, X_train.shape[1]))) model.add(Dropout(0.3)) model.add(LSTM(32, return_sequences=False)) model.add(Dense(1, activation="sigmoid")) model.compile(optimizer="adam", loss="binary_crossentropy", metrics=["accuracy"]) model.fit( X_train_seq, y_train_seq, epochs=20, batch_size=32, validation_data=(X_test_seq, y_test_seq) )

LSTM 有几个必调的参数:seq_len是时间窗口长度,我试过 5、10、20,日线数据上 10 天窗口最常见;return_sequences=True是为了堆叠第二层 LSTM,单层时可删除;Dropout层必须加,否则训练几轮后损失下降很快但验证集不降,这是典型的时序过拟合。这里的StandardScaler必须只 fit 训练集,不能对整个数据集标准化后再切分,否则又是数据泄漏。

5. 股票预测项目的避坑指南:回测里最常见的 5 个翻车现场

5.1 未来函数泄漏:正确率 95% 但是废模型

现象:用随机森林训练后,测试集准确率高达 95%,怎么看都不对劲。检查特征和标签后发现问题出在「当日收盘价」同时出现在特征和标签里。如果目标变量包含当日收盘价,而特征矩阵也包含当日收盘价,模型实际上是在读答案。

原因:构造特征时把目标日期的已知信息误当作特征输入了。最常见的是用 T 日的 close 预测 T 日的涨跌,但 close 本身就是涨跌的结果。

解决:把特征统一前移。典型做法是用 T 日及之前的数据构造特征,预测 T+1 日的标签。即在代码里执行y = df["target"].shift(-1),然后丢弃最后一行。这个shift(-1)是股票预测项目里最重要的数据操作之一。

5.2 紧贴训练集的测试集:模型的记忆性幻觉

现象:测试集紧挨着训练集的最后一天,测试集前几天的预测准确率明显高于测试集后半段。整体指标还行,但越往后越差。

原因:树模型记住了训练集末尾的模式,测试集初期与训练集尾部模式相似,因而得分虚高。时间序列交叉验证下这个问题会被放大,因为每一折的训练集都覆盖到验证集的前一天。

解决:训练集和测试集之间强制留出隔离带,比如空出 10 个交易日不用于训练也不用于验证。这 10 天的数据相当于模型从未见过的过渡期,能有效检验模型的泛化能力,而不是短期记忆。

5.3 技术指标参数不同导致数据不一致

现象:把数据从日线改为周线后,模型表现骤降,但训练代码完全没动。

原因:MA5、RSI 这些指标的窗口参数是按天数设定的,改成周线后 MA5 等于 5 周均线,含义完全变了。而股票数据缩采样后样本量也缩小数倍,模型训练不充分。

解决:数据频率改变后必须同步调整特征参数,同时重新做网格搜索。这个坑不深但容易忘,层数多了以后很难排查。

5.4 用未来数据填充缺失值

现象:数据集里某只股票停牌两天,用fillna(method="ffill")填充后没有报错,但最后预测结果漂移严重。

原因:前向填充在前场数据缺失时是合理的,但如果在切分之后才做填充,填充值可能用到测试集的信息。更隐蔽的是用interpolate()线性插值,插值结果同时参考了前后两个方向的数据。

解决:缺失值填充必须在数据集切分之前完成,并且记录填充规则。停牌日的数据不能凭空造,最简单可靠的方案是直接删除停牌日行,只在连续交易日的序列上做特征计算。

5.5 模型调参只盯训练集准确率

现象:手动调整随机森林的max_depth从 3 调到 10,训练集准确率从 60% 涨到 90%,于是认为模型变好了,但测试集分数反而下降。

原因:这是标准的过拟合路径。max_depth增加时,模型从「学习模式」变成了「背数据」,训练集上的提升是背诵能力的体现,不是预测能力的提升。

解决:每次调参后同时打印训练集分数和测试集分数,观察两者的差距。当训练集提升而测试集停滞时,立即停止增加复杂度。我在做这类项目时习惯把max_depth控制在 3 到 7 之间,min_samples_leaf不小于 5,这两个参数是控制随机森林过拟合最有效的旋钮。

6. 把预测结果落地成一个可用的信号:用序列收益曲线验证策略

预测模型跑完不是终点。如果你要拿这个项目去答辩或写报告,最有力的交付物不是准确率数字,而是一张累计收益曲线。做法很简单:把模型预测结果视为交易信号,预测「涨」就持币买入,预测「跌」就空仓。然后计算按此信号交易后的资金曲线,与「买入持有」策略对比。

# 生成交易信号:预测涨则持有,否则空仓 test_df["signal"] = y_pred test_df["daily_return"] = test_df["close"].pct_change() test_df["strategy_return"] = test_df["daily_return"] * test_df["signal"] # 累计收益 test_df["strategy_cum"] = (1 + test_df["strategy_return"]).cumprod() test_df["benchmark_cum"] = (1 + test_df["daily_return"]).cumprod() # 计算年化收益与最大回撤 def max_drawdown(series): return (series / series.cummax() - 1).min() strategy_annual = test_df["strategy_cum"].iloc[-1] ** (252 / len(test_df)) - 1 benchmark_annual = test_df["benchmark_cum"].iloc[-1] ** (252 / len(test_df)) - 1 print(f"策略年化收益: {strategy_annual:.2%}") print(f"买入持有年化收益: {benchmark_annual:.2%}") print(f"策略最大回撤: {max_drawdown(test_df['strategy_cum']):.2%}")

这段代码的输出有两个关键观察角度。第一,策略年化收益不一定高于买入持有,能高于就已经很好了,因为股票整体是上涨的,空仓本身就会跑输。第二,最大回撤比年化收益更能说明模型的实际价值。机器学习模型在这类任务中的真正贡献不是每次都预测对,而是在市场剧烈下跌时给出正确的不交易信号,从而压低回撤。我在复盘这类项目时的最深刻感受是:用机器学习做股票分析,最大的实际收获不是赚钱策略,而是建立了一套「永远区分训练表现和真实表现」的评估习惯。如果回测指标没有把交易成本、滑点和印花税算进去,那再漂亮的曲线也只是纸面富贵。加一个固定的每日交易成本,比如单边 0.1%,你的曲线会立刻诚实很多。这也正是这类压缩包项目的价值所在——它逼迫你去面对很多远比模型本身更影响结果的东西,希望这些经验能帮你在自己的项目里少走几步弯路,少交一点学费。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 14:14:54

BBDown命令行工具:B站视频本地化永久保存方案

1. 项目概述:为什么B站视频“看即失去”,而你需要一个真正可控的本地资源库B站视频如何永久保存?这问题背后藏着一个被很多人忽略的事实:你刷到的每一个高清番剧、每一段干货教程、每一支创意MV,本质上都不是你的。它们…

作者头像 李华
网站建设 2026/9/26 14:14:22

宏碁OMR318驱动失效深层解析:HID协议与Win11签名兼容性实战

1. 项目概述:这不是“装个驱动”那么简单,而是理清外设与系统握手的底层逻辑宏碁暗影骑士OMR318鼠标,市面上一款定位中端游戏场景的RGB光电鼠标,外观硬朗、侧键布局合理、DPI档位可调,但它的核心痛点——驱动缺失或失效…

作者头像 李华
网站建设 2026/9/26 14:13:50

深度学习论文复现指南:多途径找代码与核心技巧

1. 为什么“找代码”本身就是一项核心科研能力1.1 从一篇论文到一份可运行代码的距离很多人读论文的时候会有一种错觉:论文写得清清楚楚,公式推导完整,实验设置也列了表格,那复现应该就是“照着做”的事。但真正动过手的人都知道&…

作者头像 李华
网站建设 2026/9/26 14:13:49

夸克网盘信用制扩容原理与1TB稳定获取指南

1. 项目概述:这不是“薅羊毛”,而是一次对网盘服务逻辑的深度拆解“2026年夸克网盘免费扩容1TB空间指引(保姆级教程)”——这个标题一出来,很多人第一反应是点开、收藏、转发给朋友,然后心里嘀咕&#xff1…

作者头像 李华
网站建设 2026/9/26 14:13:47

OpenHands开源AI编程Agent实战:从重构到测试全流程解析

一次偶然的机会,我把一个满是历史包袱的老项目交给 OpenHands 这个开源 AI 编程 Agent 来处理,体验完全出乎我意料。需求本身并不复杂:把散落在十几个文件里的工具函数统一收口到一个公共模块,同步改掉所有调用点,再顺…

作者头像 李华
网站建设 2026/9/26 14:13:12

Navicat for MySQL 10.0.11 简体中文版实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华