ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python机器学习股票预测毕设全攻略:数据、模型与避坑

Python机器学习股票预测毕设全攻略:数据、模型与避坑 简介一套基于机器学习的股票预测与分析Python项目定位为高分毕业设计98分主要面向计算机相关专业正在筹备毕设的学生也可作为课程设计、期末大作业或项目实战练习素材。项目经严格调试可正常运行提供完整源码与文档说明涵盖数据预处理、特征构建、模型训练与回测评估等关键环节。压缩包共约2000个文件、总容量693.45MB1675个png用于可视化展示与论文插图156个csv为历史行情数据112个npz与39个pth分别承载训练集与模型权重9个py为核心算法代码5个xml及md文档辅助配置与说明。内容预览显示涵盖苹果、美国银行等个股以及纳斯达克、罗素2000等指数数据便于进行多标的对比建模。目前已有613人学习浏览适合需要完整可运行项目、快速理解机器学习预测流程并直接用于毕业设计的读者。1. Python与机器学习做股票预测为什么毕业设计都爱选它一个金融专业或计算机专业的毕业设计最怕的是「题目听着大落地全是坑」。而「Python实现基于机器学习的股票预测和分析」恰好是那种看起来热门、资料又多、但真正做起来容易翻车的选题。刷热榜时你能看到「模型预测股票涨跌 每次结果不一样」这类高频吐槽说明很多人卡在了同一个地方不是不会调库而是没搞懂预测这件事本身的边界。这个课题要做的事其实很具体用 Python 抓取股票历史行情构造技术指标特征喂给机器学习模型随机森林、LSTM、XGBoost 这类让模型学会从历史数据里找规律然后用未来一段数据验证它是否真的能预测涨跌。它之所以适合做毕业设计是因为链路完整——数据采集、特征工程、模型训练、回测评估、可视化展示每个环节都有明确交付物源码加上文档说明就能构成一套完整的「研究工程」叙事。但你也得先接受一个反直觉结论模型在训练集上预测得越准在真实行情里亏得越惨的概率就越大。这篇笔记会把整个方案的落地路径拆开从数据到特征、从模型到回测最后把那些能让你少走两个月弯路的坑都标出来。适合正要开题、或者已经写了半截代码但结果不理想的人照着做、照着改。2. 数据与特征先搞清楚模型到底在学什么2.1 行情数据怎么拿常见数据源与取舍做股票预测第一步不是选模型而是拿数据。常见的做法是用免费的 Python 库直接拉行情比如akshare、tushare、yfinance。这些库的差异主要在数据覆盖范围和接口稳定性上akshare偏向国内 A 股接口多但偶尔会因网页结构变动而挂掉tushare老牌稳定但部分接口需要积分yfinance适合拿美股数据接口简单但国内访问偶尔超时。我一般建议毕设项目用akshare或tushare拿 A 股日线数据原因很简单A 股数据的涨跌停规则、交易时段更贴近国内评审老师的认知而且个股数量多方便做多股票对比实验。下面是用akshare拉取一只股票日线行情的最小示例import akshare as ak import pandas as pd # 拉取贵州茅台的历史日线数据 df ak.stock_zh_a_hist(symbol600519, perioddaily, start_date20180101, end_date20231231, adjustqfq) # akshare 返回的列名是中文统一改成英文方便后续处理 df.columns [date, open, close, high, low, volume, amount, amplitude, pct_change, change, turnover] df df[[date, open, close, high, low, volume, pct_change]] df[date] pd.to_datetime(df[date]) df df.set_index(date).sort_index() print(df.head())这段代码里有三个关键点。adjustqfq表示前复权如果不复权遇到除权除息时股价会突然跳水模型会把这种假跳变当成真实规律。sort_index()是必须的因为接口返回的数据虽然默认按日期排序但一旦你后续做多股票拼接顺序就不可信了。pct_change是涨跌幅很多模型直接拿它当预测目标比预测绝对价格稳定得多。拿到原始行情后别急着训练。先做两件检查一是看数据区间里有没有停牌导致的缺失日A 股通常直接缺行需要用reindex补上交易日并填充空值二是看一眼价格走势图确认前复权后没有明显的跳跃断层。这两步能避免后面八成以上的「预测结果特别离谱」问题。2.2 特征工程把价格序列变成模型能吃的东西机器学习模型吃的是特征矩阵不是原始 K 线。股票预测里最常用的特征有两类技术指标和时序统计量。技术指标包括均线 MA、相对强弱指标 RSI、MACD 的 DIF/DEA、成交量变化率等时序统计量包括过去 N 日的收益率、波动率、最高价回撤等。这些特征的意义在于把价格走势的「形态」和「动量」压缩成数值让模型不用自己去从 K 线里悟规律。下面这段代码构造了一组典型特征注意所有特征都只用当天的数据或过去的数据计算绝不能用未来数据import numpy as np def make_features(df, window5): df df.copy() # 移动平均线5日、10日、20日 for w in [5, 10, 20]: df[fma_{w}] df[close].rolling(windoww).mean() # 价格动量过去N日的累计涨跌幅 df[momentum_5] df[close] / df[close].shift(5) - 1 df[momentum_10] df[close] / df[close].shift(10) - 1 # 波动率过去N日日收益率的标准差 df[ret] df[close].pct_change() df[volatility_10] df[ret].rolling(window10).std() # RSI 相对强弱指标周期14 delta df[close].diff() gain delta.clip(lower0).rolling(window14).mean() loss (-delta.clip(upper0)).rolling(window14).mean() rs gain / (loss 1e-9) df[rsi_14] 100 - 100 / (1 rs) # 量比当日成交量 / 过去5日均量 df[volume_ratio] df[volume] / df[volume].rolling(window5).mean() # 删除含有NaN的行因为滚动窗口前几个值算不出来 df df.dropna() return df feature_df make_features(df) print(feature_df[[close, ma_5, momentum_5, rsi_14, volume_ratio]].tail())这里每个特征都通过rolling或shift实现滞后确保第 t 行的特征只包含 t 日及之前的信息。结尾的dropna()会丢掉前 20 行这个代价是必须付的——如果为了保留数据用 0 填充滚动窗口的 NaN模型会学到「市场刚开盘时均线是 0」这种伪规律。特征的数量不是越多越好。毕设阶段 8 到 15 个特征足够再多容易过拟合。还有一个容易被忽略的点特征必须做标准化或归一化但一定要在划分训练集之后再做否则测试集的信息会通过归一化的均值、方差泄漏进训练过程。正确做法是先train_test_split再对训练集 fit 一个StandardScaler然后用同一个 scaler 去 transform 测试集。2.3 预测目标预测涨跌方向比预测价格更靠谱模型的目标值怎么定预测明天的收盘价是 100 还是 101这种回归任务误差会非常大而且股价不平稳直接回归价格几乎必败。更稳的做法是预测涨跌方向——明天相对于今天是否上涨二分类或者预测未来 5 日的收益率符号。这样模型的任务从「精确猜价格」降维成「判断大概率方向」准确率做到 55% 以上就有实际意义。构造标签的代码很简单# 预测目标未来5个交易日是否上涨1涨 0跌 feature_df[future_ret_5] feature_df[close].shift(-5) / feature_df[close] - 1 feature_df[label] (feature_df[future_ret_5] 0).astype(int) # 最后5行没有未来数据直接删掉 feature_df feature_df.iloc[:-5]注意shift(-5)是往前看未来所以最后 5 行要删除。训练时如果用全部数据随机切分会造成「数据穿越」——模型用 2023 年的数据训练但测试集里混进了 2022 年时间顺序被打乱。正确做法是按时间顺序切分例如前 80% 时间做训练后 20% 做测试。后面避坑章节会单独展开。3. 模型选型与训练别一上来就上 LSTM3.1 常见模型的对比与选择理由很多人看到「机器学习预测股票」就想到 LSTM但毕设项目里 LSTM 不是首选。为什么LSTM 适合长序列依赖但日线级别的股票数据噪声极大LSTM 不仅训练慢而且极容易过拟合调参成本高。相比之下树模型和线性模型在结构化表格数据上的表现更稳且可解释性强——你可以用特征重要性说清楚模型到底靠什么做判断。下面这个表格列出了三种常用模型在股票预测这个任务里的典型表现方便你答辩时讲选型理由模型预测类型训练速度过拟合风险可解释性适用场景逻辑回归二分类涨/跌极快低强看系数简单基线验证特征有效性随机森林二分类/回归快中强特征重要性主力模型能处理非线性关系XGBoost二分类/回归中中高需调参中特征重要性追求分数时的进阶选择LSTM二分类/序列预测慢高弱仅当你要做序列模型专题时我一般建议毕设先做逻辑回归和随机森林作为基线然后选一个效果好的做主模型。如果你希望体现「深度学习」含量可以额外加一个 LSTM 做对比但不要指望它总比随机森林好——很多真实数据上 LSTM 反而被随机森林吊打。这个对比结果本身也是很好的论文素材。3.2 训练流程严格按时间顺序切分下面这段代码演示了完整的训练与评估流程以随机森林为例from sklearn.ensemble import RandomForestClassifier from sklearn.preprocessing import StandardScaler from sklearn.metrics import accuracy_score, precision_score, recall_score # 假设 feature_df 已经包含了特征列和 label 列 features [c for c in feature_df.columns if c.startswith((ma, momentum, volatility, rsi, volume))] X feature_df[features] y feature_df[label] # 按时间顺序前80%训练后20%测试 split_idx int(len(feature_df) * 0.8) X_train, X_test X.iloc[:split_idx], X.iloc[split_idx:] y_train, y_test y.iloc[:split_idx], y.iloc[split_idx:] # 先对训练集做标准化再用同一参数处理测试集 scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) # 训练随机森林 model RandomForestClassifier( n_estimators300, max_depth6, min_samples_leaf5, random_state42, n_jobs-1 ) model.fit(X_train_scaled, y_train) # 预测并评估 y_pred model.predict(X_test_scaled) print(准确率:, accuracy_score(y_test, y_pred)) print(精确率:, precision_score(y_test, y_pred)) print(召回率:, recall_score(y_test, y_pred))这里的参数不是随便拍的。max_depth6限制树的深度避免模型死记硬背历史行情min_samples_leaf5要求叶子节点至少 5 个样本防止学到极端个案random_state42保证实验结果可复现这一点在毕设里极其重要——评审老师可能会让你现场重跑如果你没有固定随机种子每次结果都不一样答辩场面会非常尴尬。评估指标上准确率是最直观的但股票涨跌通常不平衡大约 50% 对 50%但实际要看区间所以还应该看精确率和召回率。简单说精确率高意味着模型说「涨」的时候大概率真涨召回率高意味着模型能抓住大部分上涨机会。实际应用里你应该追求精确率因为在股市里少赚一次没关系多亏一次很伤。3.3 LSTM 怎么加进毕设一个不会翻车的最小结构如果你坚持要用 LSTM 展示深度学习能力我建议把它作为「对比模型」而不是唯一方案。最小可用的 LSTM 结构如下import tensorflow as tf from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout # 需要把特征整理成 3D 形状 (样本数, 时间步长, 特征数) def build_sequences(X, time_steps10): X_seq, y_seq [], [] for i in range(len(X) - time_steps): X_seq.append(X[i:itime_steps]) y_seq.append(y[itime_steps]) return np.array(X_seq), np.array(y_seq) X_train_seq, y_train_seq build_sequences(X_train_scaled, time_steps10) X_test_seq, y_test_seq build_sequences(X_test_scaled, time_steps10) model_lstm Sequential([ LSTM(64, return_sequencesTrue, input_shape(X_train_seq.shape[1], X_train_seq.shape[2])), Dropout(0.3), LSTM(32), Dropout(0.3), Dense(1, activationsigmoid) ]) model_lstm.compile(optimizeradam, lossbinary_crossentropy, metrics[accuracy]) model_lstm.fit(X_train_seq, y_train_seq, epochs20, batch_size32, validation_split0.1)这段代码里有两个容易踩坑的地方。build_sequences会额外吃掉前time_steps个样本所以训练集和测试集长度会略有缩水评估时要保证预测的样本量一致。Dropout(0.3)是防过拟合的关键如果你发现 LSTM 训练集准确率 98%、测试集 51%多半是 Dropout 太低或 epoch 太多。还有LSTM 的预测结果受初始化影响很大每次运行都不一样所以必须在开头设置全局随机种子tf.random.set_seed(42) np.random.seed(42)4. 回测与评估别只用准确率骗自己4.1 模拟交易回测把预测结果换算成资金曲线机器学习模型在测试集上的准确率只是第一步真正说服人的是「如果按模型的信号交易账户会变成什么样」。这就需要写一个简单的回测引擎遇到模型预测「涨」就买入预测「跌」就空仓按每天的收益率累加资金变化。下面的回测代码只做方向判断不考虑手续费和滑点作为毕设演示足够了def backtest(feature_df, y_pred, initial_cash100000): # 只用测试集部分 test_df feature_df.iloc[int(len(feature_df) * 0.8):] mask np.array(y_pred) 1 # 1表示预测上涨 # 模拟持仓预测上涨时持有否则空仓 daily_ret test_df[close].pct_change().fillna(0).values strategy_ret daily_ret * mask # 简单累乘收益 equity initial_cash * np.cumprod(1 strategy_ret) # 基准一直持有买入持有 bench_equity initial_cash * np.cumprod(1 daily_ret) return equity, bench_equity # 用随机森林的预测结果做回测 equity, bench backtest(feature_df, y_pred) print(策略最终资金:, round(equity[-1], 2)) print(基准最终资金:, round(bench[-1], 2)) print(超额收益:, round(equity[-1] - bench[-1], 2))这个回测有一个重要假设信号当日收盘产生次日开盘执行。上面的代码用了pct_change()计算的当日收益率实际上是当日收盘到次日收盘的收益而预测是用当日数据做出的所以看起来没有未来函数但严格来说应该用次日开盘价成交。毕设里可以在文档里说明这个简化假设并把「次日开盘执行」作为改进方向写进去。回测最怕的是曲线形状特别漂亮一条直线向上没有任何回撤那几乎一定是未来函数泄漏。正常的策略资金曲线应该有波折而且和基准曲线拉开差距的地方通常是某几个大波段。4.2 评估指标准确率、夏普比率、最大回撤除了准确率你还应该计算另外三个指标它们能反映策略的风险特征夏普比率衡量每承受一单位波动能换来多少超额收益。回测里的计算方式是strategy_daily_ret strategy_ret # 上面回测中计算的每日策略收益率 sharpe np.sqrt(252) * (strategy_daily_ret.mean() - 0) / (strategy_daily_ret.std() 1e-9) print(夏普比率:, round(sharpe, 3))252 是一年的交易日数量用于把日收益年化。夏普比率大于 1 已经算优秀毕设里做到 0.5 以上就能讲得过去因为这是纯技术指标预测没有考虑交易成本。最大回撤是衡量风险的核心指标它表示资金曲线从峰值跌到谷底的最大幅度cum_returns np.cumprod(1 strategy_ret) peak np.maximum.accumulate(cum_returns) drawdown cum_returns / peak - 1 max_drawdown drawdown.min() print(最大回撤:, round(max_drawdown * 100, 2), %)如果最大回撤超过 20%说明策略在市场下跌时几乎无法防守。很多「高准确率」策略最大回撤照样很大原因在于它虽然预测方向没错但只要错一次就亏得多、赚得少。这一点在论文里可以重点分析。4.3 特征重要性让答辩评审知道你模型的依据随机森林和 XGBoost 能输出特征重要性这是树模型比深度网络更适合毕设的另一个优势。拿到特征重要性后你不需要所有特征只保留前几个重要的重新训练模型往往更稳。importance pd.Series(model.feature_importances_, indexfeatures) importance.sort_values(ascendingFalse).plot(kindbar, figsize(10, 4))从我的经验看重要的特征通常是近期动量momentum_5和波动率而长期均线的重要性反而低。原因是 A 股短期走势受情绪影响大中长期均线更多反映趋势但延迟严重。答辩时如果被问「为什么选这些特征」你可以用这张图证明是数据决定的不是你拍脑袋定的。5. 避坑手册股票预测毕设最容易翻车的 5 个地方5.1 数据穿越模型用了未来数据而不自知现象训练集准确率极高测试集准确率也很高但回测曲线一路向上资金曲线毫无回撤。原因最常见的是用shuffleTrue随机打乱数据后再切分或者特征计算时用了shift(-n)没有删掉尾部数据。这导致测试集里混入了未来信息模型等于「开卷考试」。解决训练与测试必须按时间顺序切分用split_idx int(len(df) * 0.8)这种方式不要用train_test_split的默认随机模式。所有特征只能依赖过去数据如果代码里有shift()出现负参数检查它对应的标签有没有被正确丢弃。提示最简单的自查方法把训练和测试时间范围打印出来确认二者没有重叠。再随机挑测试集某一天的预测手动用这一天当天的特征去复算看是不是用了之后的行情。5.2 归一化泄漏StandardScaler 在切分前就 fit 了现象训练集和测试集的分数都很高但换一段新数据后分数暴跌。原因在train_test_split之前就对全量数据做了scaler.fit_transform()。scaler学到的均值和方差包含了测试集的信息归一化后的数据已经带上了未来数据的分布特征模型在测试集上「间接偷看」了答案。解决严格按「先切分再 fit再 transform」的顺序执行。写完代码后检查一遍scaler只能在大约第 80% 行的位置之前的数据上调用fit。5.3 随机性失控每次运行结果都不一样论文没法写现象同一份代码隔天跑准确率从 56% 变成 48%回测曲线完全不一样。原因随机森林、LSTM 乃至朴素贝叶斯都依赖随机数LSTM 的权重初始化也在 GPU 上有不确定性。论文里写的实验结果无法复现答辩时老师一句「再跑一次」就露馅。解决在所有涉及随机的地方设置固定种子np.random.seed(42)、random.seed(42)、tf.random.set_seed(42)并在随机森林里传random_state42。如果使用 PyTorch还要设置torch.manual_seed(42)。把种子写在文档的「实验环境」一节里。5.4 样本不平衡模型永远预测「不涨」现象模型测试集准确率 60%但看预测结果全是 0预测不涨一查才发现测试集里本来 60% 的样本就是跌的。原因股票涨跌标签有时不平衡尤其在熊市区间。模型发现全预测 0 就能拿到 60% 准确率于是懒得不学规律了。解决先检查y_test.value_counts()计算涨跌比例。如果不平衡可以用class_weightbalanced随机森林支持或在训练时降采样多数类。更重要的是用精确率和召回率评估而不是只盯准确率。5.5 过拟合训练集 98%测试集 51%现象随机森林在训练集上准确率接近满分但一到测试集就退化成抛硬币。原因树模型太深或没有限制叶子节点最小样本数。股票数据噪声极大树模型完全生长的后果就是记住每一根 K 线包括噪声。解决把max_depth调到 5–8min_samples_leaf调到 5–20n_estimators不用太大300 足够。如果用了 XGBoost加max_depth3、learning_rate0.01并且观察训练集和测试集的准确率差距差距超过 10 个百分点就继续调低复杂度。6. 进阶技巧用滚动窗口与集成模型把预测结果做稳到这一步你已经有了一个能跑通的基础方案。如果想让分数再往上走或者给论文增加亮点最值得做的有两个方向滚动窗口训练和多模型集成。滚动窗口训练的含义是不要只用前 80% 数据一次性训练模型而是每隔一段周期用最近 2 到 3 年的数据重新训练。这样模型能适应市场风格的变化因为 2018 年的规律和 2023 年的规律可能完全不同。实现上很简单写一个循环每次把训练集末端往后推一个月def rolling_train(feature_df, features, train_years2, retrain_months3): all_pred np.zeros(len(feature_df)) # 按月份切分测试区间 test_start pd.Timestamp(2021-01-01) test_end pd.Timestamp(2023-12-01) current test_start while current test_end: train_end current - pd.Timedelta(days1) train_start current - pd.DateOffset(yearstrain_years) train_mask ((feature_df.index train_start) (feature_df.index train_end)) test_mask ((feature_df.index current) (feature_df.index current pd.DateOffset(monthsretrain_months))) X_train feature_df.loc[train_mask, features] y_train feature_df.loc[train_mask, label] X_test feature_df.loc[test_mask, features] y_test feature_df.loc[test_mask, label] # 重用小节里的随机森林配置 clf RandomForestClassifier(n_estimators200, max_depth5, min_samples_leaf10, random_state42) clf.fit(X_train, y_train) all_pred[feature_df.index.isin(X_test.index)] clf.predict(X_test) current pd.DateOffset(monthsretrain_months) return all_pred这段代码会把你测试期的每一天都放在「模型没见过」的位置上比一次性切分更接近真实场景。如果滚动窗口的结果比一次性切分低别慌这很正常因为滚动窗口杜绝了数据穿越的边角料。重要的是它证明了你的流程是真实可用的。集成模型的做法更直白把你的数据分别扔给随机森林、XGBoost、逻辑回归让三个模型投票涨的票数多就预测涨。集成能显著降低单个模型的随机波动但前提是每个单模型都已经调得不算差。集成代码就几行from xgboost import XGBClassifier from sklearn.linear_model import LogisticRegression rf RandomForestClassifier(n_estimators300, max_depth6, random_state42) xgb XGBClassifier(n_estimators200, max_depth3, learning_rate0.05, random_state42) lr LogisticRegression(max_iter1000) models [rf, xgb, lr] probas [] for m in models: m.fit(X_train_scaled, y_train) probas.append(m.predict_proba(X_test_scaled)[:, 1]) # 预测涨的概率 avg_proba np.mean(probas, axis0) final_pred (avg_proba 0.5).astype(int)注意这里用的是平均概率而不是投票因为概率能更好反映模型置信度。如果某个模型的概率始终在 0.45 到 0.55 之间徘徊说明它没有判断力你可以试着去掉它再对比一次集成结果。我做这类毕设项目最深的一个体会是股票预测的源码并不难难的是对自己诚实。别为了图表好看去偷偷调数据范围也别一见到测试集准确率 60% 就觉得可以实盘了。把回测里的最大回撤和夏普比率老老实实算出来你的论文才站得住脚。这个课题的价值不在「预测得多准」而在于你完整走通了数据、特征、模型、评估这条链并且能说清楚每个环节为什么这么做。按照上面的步骤把代码写扎实你交出去的就不只是一份源码而是一个能讲清楚、能复现、能经受追问的完整研究过程。希望帮到你。本文还有配套的精品资源点击获取
返回列表