☰
股票预测毕业设计:从特征工程到时间序列验证的完整避坑指南
2026/10/14 9:20:01 网站建设 项目流程

简介:这是一份面向计算机相关专业毕业设计及课程设计场景的Python机器学习实战资源,主题聚焦股票预测与分析,既适合正在完成毕设的学生,也适合希望提升项目实践能力的学习者。包内共2000个文件,包含1675个图片文件用于展示可视化结果,156个CSV格式的历史股票数据,112个NPZ格式的预处理数据,39个模型权重文件及9个Python源码文件,另含XML配置、项目说明文档等,压缩包整体约693.45MB。源码经过严格调试可直接运行,并附带项目说明文档,帮助理解从数据清洗、特征工程到模型训练与结果分析的全流程。目前已有614人学习下载,是一套结构完整、可用于答辩演示的高分毕业设计参考资料。

1. 股票预测的毕业设计,真正要交付的不是“预测”

某同学的毕设题目是“基于机器学习的股票预测”,训练集上准确率接近百分之九十,回测曲线画出来一路向上,结果一上模拟盘就连续打脸,答辩时被问了三个问题:你预测的到底是什么?验证集是怎么切的?有没有用未来数据?他一个都答不利索,最后拿了中等分数。这不是个例。用机器学习做股票预测,难点从来不在模型跑不跑得通,而在于预测目标定义得是否合理、数据有没有泄漏、验证过程是不是在骗自己。所谓“高分毕业设计”,真正拉开差距的恰恰是工程细节:任务定义、数据清洗、特征构造、严谨的时间序列评估。本文按这条链路拆开讲,每步直接给可复现代码和参数说明,适合想把“Python+机器学习+股票预测”做成毕设,又不想在答辩台上翻车的人。

2. 数据与特征工程:为什么“涨跌”比“价格”更值得预测

股票数据本质上是带噪声的非平稳时间序列,价格水平会随市场整体环境漂移。直接把“明天收盘价是多少元”作为预测目标,相当于让模型去猜一个随机游走的绝对位置,难度大,误差度量也很尴尬——差三毛钱算准还是不准?更难办的是,这类回归任务在答辩时很难自圆其说:均方误差小,不代表策略赚钱;均方误差大,也不代表方向判断差。所以,我一般会先把预测目标从“价格”改成“方向”。

2.1 预测目标先定类:回归价格、分类涨跌、多分类分档

当前常见的做法是把问题建模成分类任务,下面这张表对比了三种目标的优劣,供选题时参考:

任务类型预测目标示例优点缺点
回归未来N日收盘价直观,容易理解价格非平稳,噪声大;评估指标难定义
二分类未来N日收益率大于0目标明确,和买卖决策直接相关类别不平衡,单边行情下容易走偏
三分类未来N日涨幅>1%为涨,跌幅>1%为跌,中间为震荡更贴近交易实践,输出信息量大阈值需要人为设定,不同股票适用参数不同

我通常建议主任务选“三分类”或“二分类”。原因是它们对应的评价指标(准确率、召回率、F1、AUC)是答辩时能讲清楚的东西,还能配合后续的模拟交易画出收益曲线,形成“预测—决策—验证”的完整闭环。回归可以保留一个模型作为对比实验,证明你尝试过不同设定,但别把它当主线。预测目标定下来之后,后续所有特征工程和标签构造都围绕它展开,方向就不会乱。

2.2 数据获取与清洗:日线数据的处理边界

数据源方面,公开的财经数据接口都能拿到日线数据,字段一般包含股票代码、日期、开盘价、最高价、最低价、收盘价、成交量、成交额。毕设场景用日线就够了,不需要更高频的数据。真正要注意的是清洗逻辑:第一,停牌日成交量经常为0,这类记录要剔除;第二,除权除息会造成价格跳空,需要用前复权或后复权数据保证价格连续;第三,缺失值不能全局填充,要按股票代码分组处理,避免把一只股票的收盘价填到另一只股票里。

import pandas as pd def load_and_clean(df): # df: 个股日线数据,至少包含 date, open, high, low, close, volume df = df.sort_values("date").reset_index(drop=True) # 剔除停牌日:成交量为0的记录直接丢弃 df = df[df["volume"] > 0] # 缺失值优先前向填充(沿用前一天价格),仍缺失则删除 df = df.ffill().dropna() # 除权除息问题:若数据源未复权,建议用后复权价重新计算开高低收 # 这里假设数据源已提供复权价格 return df

这段逻辑的关键是“按时间排序”和“先剔停牌再补缺失”。排序保证后续所有rolling窗口计算都基于正确的时间顺序;剔停牌是为了防止连续多个0成交量把均线压出一个假信号。如果数据源给的收盘价不是复权价,常见做法是先取复权因子,把开盘、最高、最低、收盘四列统一乘上因子,再进入后续流程。清洗阶段宁可多删几行,也不要留下会让模型学到跳空假信号的脏数据。

2.3 量价特征怎么造:均线、波动率与成交量的组合

特征工程是整条链路里投入产出比最高的一步。纯价格序列对模型的区分度很低,但从价格和成交量衍生出的“相对指标”才有预测力。我一般分为两类:一类是经典技术指标,如简单移动平均、指数移动平均、RSI、MACD、布林带;另一类是时序统计特征,如近N日收益率、波动率、量能变化率。后者在树模型里尤其管用,因为树模型善于用特征之间的相对关系做切分。

def make_features(df, windows=[5, 10, 20]): df = df.copy() # 1日收益率:最基础的动量特征 df["ret_1"] = df["close"].pct_change() for w in windows: # 均线:衡量当前价格相对历史均价的偏离 df[f"ma_{w}"] = df["close"].rolling(w).mean() # 波动率:近w日收益率标准差,衡量不确定性 df[f"std_{w}"] = df["close"].rolling(w).std() # 动量:w日累计收益率 df[f"ret_{w}"] = df["close"].pct_change(w) # 量能变化:成交量相对w日均量的变化 df[f"vol_ratio_{w}"] = df["volume"] / df["volume"].rolling(w).mean() # 只需保留计算所需的中间列,避免把原始价格序列带进模型 drop_cols = ["open", "high", "low", "volume"] df = df.drop(columns=[c for c in drop_cols if c in df.columns]) # 删除因rolling产生的前置空值行 return df.dropna()

这里每个窗口参数都有实际含义:窗口越短,捕捉的动量越敏捷,但噪声越大;窗口越长,信号越平滑,但滞后越明显。毕设里取5、10、20三档是常见配置,它们是日线交易者最常用的周期,答辩时能解释得通。注意量能变化率用除法而不是减法,因为不同股票的成交量基数差异很大,比值可以跨股票比较,减法做不到。很多新手在这里会直接堆几十个指标,结果特征之间高度相关,模型解释性骤降。我的习惯是先做一小批,跑一个基线,再看特征重要性逐步增加,而不是一口气全塞进去。

2.4 标签构造与泄漏边界:训练与验证的“楚河汉界”

标签的定义方式直接决定模型学的是什么。如果目标是“未来3日上涨还是下跌”,那么第t行的标签应该是用t+3日的收盘价算出来的收益率符号。这里有一个容易被忽略的边界:标签“必须要用到t日之后的数据”,这部分是合理的;但特征的每一行只能使用t日及其之前的信息,绝对不能掺入t+1到t+3的数据。把这两条边界划清楚,后面的时间序列切分才有意义。

def make_label(df, horizon=3, up_th=0.01, down_th=-0.01): df = df.copy() # 未来horizon日收益率 future_ret = df["close"].shift(-horizon) / df["close"] - 1 # 三分类:上涨、震荡、下跌 df["label"] = pd.cut( future_ret, bins=[-float("inf"), down_th, up_th, float("inf")], labels=[0, 1, 2], # 0=跌, 1=震荡, 2=涨 ).astype("int") # 标签为NaN的行(数据末尾)无法确定未来收益,直接丢弃 return df.dropna(subset=["label"])

pd.cut的边界值得说清楚:阈值1%不是拍脑袋定的,它对应日线级别的一个“可感知波动”。阈值太小,模型会把微小波动当信号,导致预测结果频繁翻转;阈值太大,又会让多数样本落进“震荡”类,类别分布严重失衡。建议先算一下目标股票的日收益率分位数,再选择涨幅和跌幅的阈值。另外,horizon参数要和后面模拟交易的持有期保持一致。你预测的是“3天后的方向”,回测时就必须持有3天再结算,否则预测目标和交易结果对不上,整个评价体系就崩了。

3. 模型训练与评估:从Baseline到对比实验

数据准备好了,接下来面临的选择是“用哪类模型”。很多同学一上来直接上LSTM,理由是“股票数据是时间序列”,但这个认知在工程实践里经常翻车。表格类特征(收益率、均线、波动率)在梯度提升树模型上的表现往往优于RNN,而且训练稳定、可解释性强。更合理的做法是搭一组对比实验:线性模型做基线,梯度提升树做主力,LSTM做加分项。

3.1 三类模型怎么选型:基线、主力与加分项

模型训练速度对表格特征效果可解释性答辩定位
逻辑回归极快一般,依赖特征工程强,系数可直接解释基线模型,验证特征有效性
梯度提升树快好,自动处理非线性关系较强,特征重要性可输出主力模型,重点分析和调参对象
LSTM慢数据量小时不如树模型弱,近乎黑匣子对比实验,展示时序建模能力

逻辑回归的价值不在于预测多准,而在于快速检验“特征里有没有信号”。如果逻辑回归在验证集上的准确率明显高于随机猜测,说明特征构造有效,后续复杂模型才有意义;如果逻辑回归完全跑不出区分度,那你先别急着调参,回去重新审视特征和标签。梯度提升树是实际效果和工程成本最平衡的选择,它不需要对特征做标准化,对缺失值有原生处理,还能输出特征重要性用于分析。LSTM在这个场景里比较“娇气”,样本量不够时容易过拟合,训练时间还长,但如果毕设论文里能加上一组“树模型 vs LSTM”的对比数据,工作量观感和理论深度都会好很多。

3.2 时间序列分割:为什么不能直接用KFold

股票数据最特殊的地方在于它有时间顺序。普通KFold会把数据随机打乱划分,导致训练集里出现比验证集更晚的样本——模型等于提前看过了“未来”。这在真实场景里完全不可行,是答辩时最容易暴露的硬伤。正确做法是时间序列的滚动前推分割:每一折的验证集必须严格位于训练集之后。

def ts_cv_split(n_samples, n_splits=5, gap=1): # 按时间顺序划分训练/验证集,验证集永远在训练集之后 fold_size = n_samples // (n_splits + 1) for i in range(n_splits): train_end = (i + 1) * fold_size val_start = train_end + gap val_end = val_start + fold_size train_idx = list(range(0, train_end)) val_idx = list(range(val_start, min(val_end, n_samples))) yield train_idx, val_idx

gap参数很多人不理解,它表示训练集末尾和验证集开头之间空出的若干天。设置gap的理由是:股票数据存在自相关性,紧邻验证集前一天的样本,其标签和验证集首日的标签可能高度相关,把它隔开能让验证更接近真实预测场景。默认gap可以取1到3天,毕设里取1即可。还有个细节:每一折的验证长度要一致,才能让各折的评估指标可比较。这个分割器可以直接配合sklearn的交叉验证API使用,我习惯把它单独写成一个函数,方便在复现实验时保证切分方式统一。

3.3 训练代码与关键参数:以梯度提升树为例

选好模型后,训练本身并不复杂,难点在参数配置。下面以梯度提升树分类器为例给出一套可跑的流程。特征列建议统一放在X里,标签列单独放,保持“特征矩阵只含历史信息,标签列来自未来”的边界。

import xgboost as xgb from sklearn.metrics import accuracy_score, f1_score, roc_auc_score # 假设 df 已经通过 make_features 和 make_label 构造完成 feature_cols = [c for c in df.columns if c not in ["date", "label"]] X = df[feature_cols].values y = df["label"].values # 用时间序列切分获得训练/验证索引 train_idx, val_idx = next(ts_cv_split(len(df), n_splits=4, gap=1)) X_train, X_val = X[train_idx], X[val_idx] y_train, y_val = y[train_idx], y[val_idx] params = { "max_depth": 4, # 控制单棵树复杂度,日线特征量不大,深度4足够 "learning_rate": 0.05, # 学习率调小,配合更多树,提高泛化 "n_estimators": 500, # 树数量,配合早停使用 "subsample": 0.8, # 行采样,缓解过拟合 "colsample_bytree": 0.7, # 列采样,增加树之间的多样性 "scale_pos_weight": 1.2, # 类别不平衡时提高少数类权重 "eval_metric": "auc", "early_stopping_rounds": 50, } model = xgb.XGBClassifier(**params) model.fit( X_train, y_train, eval_set=[(X_val, y_val)], verbose=False, ) y_pred = model.predict(X_val) print("准确率:", accuracy_score(y_val, y_pred)) print("加权F1:", f1_score(y_val, y_pred, average="weighted"))

几个关键参数要结合股票数据的特点来调。max_depth在日线特征下不建议超过6,特征少而弱时树太深就是硬记样本。learning_rate和n_estimators是一对组合,学习率越小需要的树越多,但训练时间也越长,配合early_stopping_rounds可以让树在验证集指标不再改善时自动停止,省去手动试n_estimators的功夫。subsample和colsample_bytree两个采样参数对于噪声大的股票数据几乎是必开的,它们能显著降低过拟合风险。scale_pos_weight针对类别不平衡,如果二分类里“涨”的样本少于“跌”的样本,把它设为“负类数量/正类数量”的比值即可。训练完成后,建议把model.feature_importances_保存下来,后面写文档和答辩时,这就是“为什么选这些特征”的直接依据。

3.4 评估指标怎么选:不要只盯准确率

股票预测里准确率这个指标单独拿出来没有意义。举个例子,某段时间下跌天数占70%,模型什么都不学,天天猜“跌”,准确率也有70%,但这种模型毫无价值。真正要看的是一组组合指标:

指标说明在股票场景里的作用
准确率预测正确的样本占比直观,但受类别分布影响严重
召回率真实上涨中预测正确的比例衡量模型捕捉上涨机会的能力
F1分数精确率和召回率的加权平均类别不平衡时比准确率可靠
AUC排序能力指标不依赖阈值,适合评估模型本身的区分度

除了这些机器学习的常规指标,毕设里强烈建议加一个“模拟交易”评估:用验证集数据,按模型的预测信号做买入/卖出,计算累计收益率和最大回撤。理由很简单——答辩组老师最常问的一句话就是“准确率这么高,为什么没有用它去交易赚钱?”你有模拟交易的收益曲线做支撑,比单纯讲准确率有说服力得多。模拟交易只在验证集上做,训练集绝不参与,这是纪律,也是后面避坑章里最强调的一条红线。

4. 避坑指南:股票预测毕设最容易翻车的5个细节

这一章是血泪经验总结。以下是5个我在实际项目里反复见到的坑,每一个都真实发生过,每一个都足够让整个毕设的结论失去意义。

4.1 标准化泄漏:验证集的数据“剧透”问题

现象:训练前对全部数据做了一次StandardScaler归一化,结果准确率出奇地高,但换一段新数据后模型瞬间失灵。

原因:用全量X的均值和标准差去归一化,相当于验证集的信息已经通过统计量进入了训练过程。模型在训练时“偷看”了验证集的分布,准确率虚高。

解决:先fit训练集,再transform验证集和测试集。

from sklearn.preprocessing import StandardScaler # 错误做法:全量拟合 # scaler = StandardScaler() # X_all = scaler.fit_transform(X) # 正确做法:只拟合训练集 scaler = StandardScaler() X_train_s = scaler.fit_transform(X_train) X_val_s = scaler.transform(X_val)

这个坑的隐蔽之处在于,你在训练集上拟合scaler,结果看起来完全正常,报错也永远不会出现,但验证集指标被系统性抬高。答辩时如果被问到“数据是怎么预处理的”,这个问题几乎是必问,把正确写法讲清楚,反而是加分项。

4.2 标签定义踩坑:预测周期和交易执行对不上

现象:模型预测“明天上涨”,你按信号第二天买入,结果发现收益平平,回测曲线和预测准确率对不上。

原因:标签用的是“明日收益率”符号,但实际操作中当天收盘才能确认信号,第二天开盘才能买入,存在时间错配;而且单日收益噪声太大,模型学到的近似随机信号。

解决:标签改为“未来N日收益率符号”,N建议取3或5,同时模拟交易按同样的持有期执行。预测周期和交易周期不一致,是回测曲线好看的常见假象来源。

4.3 未来函数混进特征:一个简单验证技巧

现象:某些特征(比如“当日收盘价相对未来最高价的位置”)在验证集上表现很好,但模型在新数据上完全没有延续性。

原因:特征计算时偷看了未来的数据。典型场景是计算技术指标时没有shift,把当天的价格信息泄漏到了“预测当天”的特征里。

解决:做一个污染测试。把数据里的收盘价整体shift(-1)并乘以100,让未来一天的价格变成一个明显异常值,然后重新计算特征矩阵,对比异常前后的特征值。

def leak_check(df, feature_fn): # 污染未来价格:把明天的价格放大100倍 dirty = df.copy() dirty["close"] = dirty["close"].shift(-1) * 100 # 如果特征计算依赖了未来数据,污染后特征值会发生剧烈变化 orig_feat = feature_fn(df) dirty_feat = feature_fn(dirty) # 比较前80%的行(这些行的未来数据已被污染) diff = (orig_feat - dirty_feat).abs().max() return diff

这个检查的核心逻辑是:特征应该只由“当时及之前”的数据决定,如果把未来的价格污染掉,当前的特征值不应该变。任何一个特征在污染测试下发生明显变化,都要立刻回头检查它的计算逻辑,看看是不是漏了shift。这个技巧在答辩时讲出来,会比说“我检查过没有未来函数”有说服力得多。

4.4 样本不平衡:不要被准确率欺骗

现象:模型准确率75%,看起来不错,打开混淆矩阵才发现,真实的“上涨”样本只召回20%,其余全部被预测成“下跌”。

原因:下跌天数占多数,模型学会了“猜下跌”这个捷径,准确率自然不低,但对投资决策完全没用。

解决:使用class_weight或scale_pos_weight调整类别权重,让模型对少数类样本更加敏感。更重要的是评估指标切换为F1/AUC,而不是单独看准确率。在答辩时主动讲出“准确率在类别不平衡时会失真,所以我以F1和AUC为准”,这是成熟工程师思维的直接体现。

4.5 回测过拟合:参数反复调出来的“完美曲线”

现象:回测收益曲线非常漂亮,年化收益惊人,但一旦换一只股票或换一段行情,曲线立刻垮掉。

原因:整个毕设周期里反复在同一段数据上试参数,模型和参数实际上已经“记住”了这段数据里的噪声。

解决:把数据切成训练、验证、测试三块。所有特征筛选和调参都在训练+验证上进行,模型定型之后才允许跑一次测试集,测试集的结果就是最终答案。更严格的做法是采用第5章的滚动窗口伪实盘验证——每一组参数都在多段行情里独立评估,从根本上压缩“偶然拟合”的空间。这一条是“高分毕业设计”和“普通毕业设计”的分水岭。

5. 滚动窗口伪实盘验证:让答辩里的“预测”经得起追问

普通的时间序列切分只验证了一个时间点之后的表现,而论文里最扎眼的问题往往是“你验证的这段行情正好是牛市,换个熊市还会这么准吗”。滚动窗口伪实盘验证就是为了回答这个问题:让模型像真实交易一样,每过一段时间用截至当时的数据重新训练一次,预测未来,然后移动窗口继续,把整个历史周期完整地走一遍,模拟出真实环境下逐步产生预测结果的过程。

def pseudo_live(df, feature_cols, train_days=500, horizon=3, step=20): X = df[feature_cols].values y = df["label"].values dates = df["date"].values results = [] start = 0 while start + train_days + horizon < len(df): train_end = start + train_days # 用截至train_end的数据训练,预测train_end之后horizon日的方向 model = xgb.XGBClassifier( max_depth=4, learning_rate=0.05, n_estimators=200, subsample=0.8, colsample_bytree=0.7, ) model.fit(X[start:train_end], y[start:train_end]) pred = model.predict(X[train_end:train_end+1])[0] actual_idx = train_end + horizon results.append({ "date": dates[actual_idx], "pred": pred, "actual": y[actual_idx], }) start += step return pd.DataFrame(results)

train_days控制训练窗口大小,它决定模型能看到多远的历史;horizon与标签定义的周期一致,预测3日方向就持有3日再验证;step控制模型的更新频率,step越小,训练越频繁,计算量越大。一般日线数据取train_days=500(约两年交易日)、step=20(每月重训一次)是兼顾效果和速度的配置。跑完pseudo_live之后,你可以画出两条曲线——基准买入持有策略的净值曲线,以及模型信号驱动策略的净值曲线。两条线放在同一张图里,再加上最大回撤标注,这张图就是文档说明里最有分量的实验结果。

答辩时被追问概率最高的三个问题,也可以直接从这套验证逻辑里找到答案。“准确率这么高为什么不能稳定赚钱”——因为回测没有计算滑点和手续费,且市场结构会变化;“为什么用树模型不用LSTM”——表格特征下树模型性价比更高,LSTM作为对比实验展示过时序建模能力;“特征依据是什么”——特征重要性分析显示动量类和波动率类特征贡献最大。这些问题如果在论文的结论与展望部分提前写清楚,答辩现场就不会被动。我见过太多回测曲线漂亮但说不清验证逻辑的毕设,最后都被问垮了;反而是验证严谨、承认局限的项目拿到了不错的结果。某同学的毕设最后测试集准确率并不高,但他把数据切分、防泄漏、滚动验证讲得明明白白,答辩组反而给了很高的评价。后来我带新入行的人做这个方向,一定会先问三个问题:标签怎么定义的?验证集怎么切的?有没有偷看未来数据?这三件事想明白了,模型再朴素也站得住。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询