ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

期末课设股票预测:数据切分与特征工程是关键

期末课设股票预测:数据切分与特征工程是关键 简介这是一份基于机器学习LSTM长短期记忆网络与sklearn的股票预测实战算法资源聚焦股票和基金预测场景覆盖数据采集、K线保存、特征工程、模型训练与回测的完整流程代码结构完整适合计算机、人工智能等相关专业学生用于期末课设、毕业设计或作业参考。压缩包共24个文件大小约2.69MB内含6个Jupyter Notebook实验脚本、5个Python源码文件、3个CSV行情数据、5个Excel表格以及Markdown介绍、txt项目说明等Notebook便于分步调试和效果展示Python脚本承担数据抓取、特征构建与模型调用文档和表格辅助理解项目背景与运行方式。资源具体设计了小市值结合盈利指标过滤策略验证、FFT滤波预处理、LSTM单票预测、sklearn机器学习单票回测等多个实战环节并配套项目说明、参考学习文档与数据集代码经测试可正常运行可直接对照学习也可在此基础上替换数据、调整参数或扩展算法适配课程设计、作业提交、毕业设计等多种需求。目前已有41人学习下载适合希望快速上手股票预测全流程并动手实践的高校学生与开发者。1. 期末课设里的机器学习股票预测真正拉开分数的是数据切分和特征构造期末课设里做股票预测最容易被低估的环节不是模型选得多新而是数据切分和特征构造。很多同学把train_test_split(random_state42)直接用在股价序列上得到一份非常漂亮的测试集分数却在项目答辩时被一句“你这组测试集里包含了训练集后面的行情信息模型是不是在背答案”问住。这个课设要解决的是给定一份历史行情数据用 python 源码从清洗、特征工程、模型训练、评估到预测结果可视化跑出一条完整且能被复盘的流程。适合应用统计、计算机相关专业正在赶期末课设的同学也适合想快速搭一套时间序列预测基准的工程师。整篇文章会围绕机器学习、股票预测、算法、python 源码这四个关键词展开重点落在“怎么做”而不是“模型有多玄”。2. 做预测之前先想清“股票预测”到底在预测什么2.1 回归任务还是分类任务先定目标再动代码在期末课设里最常见的两种定法一是预测未来 N 日的收盘价或收益率属于回归二是预测未来 N 日的涨跌方向属于分类。两者评估方式完全不同回归看 RMSE、MAE、MAPE分类看准确率、F1。我一般建议课设优先做回归因为回归任务的误差曲线更容易展示也方便后续用方向准确率补充说明。# 以回归为目标预测未来5日的累计收益率 df[label] df[close].shift(-5) / df[close] - 1.0 df df.dropna(subset[label])这段代码的逻辑是用第 T 天的收盘价作为基准计算第 T5 天相对第 T 天的收益率作为第 T 天样本的标签。shift(-5)表示把未来价格“向上移动”与当前行对齐。注意dropna必须做因为序列最后 5 行没有未来价格标签为空不能直接丢弃更不能填 0否则模型会把“不知道”学成“不涨不跌”。预测窗口 N 的选择是第一个要定的参数N 越大标签噪声越大模型越难拟合N 越小越像短期动量课设里常用 3 到 10 天。2.2 特征工程从行情数据里提取哪些有效信号机器学习里有一句话特征决定了上限模型只是逼近这个上限。股票预测课设里特征来源主要是原始行情和它的滞后变换。一个干净的、能写进项目说明的特征表如下特征名计算方式含义ret_1close.pct_change(1)前一日的收益率ma5close.rolling(5).mean()5 日均线反映短期趋势ma20close.rolling(20).mean()20 日均线反映中期趋势vol_ratiovolume / volume.rolling(20).mean()当日成交量相对 20 日均量的倍数high_low_range(high - low) / close当日振幅ret_5close.pct_change(5)过去 5 日累计收益labelclose.shift(-5) / close - 1.0未来 5 日收益预测目标构造特征的通用原则是只用 T 日及 T 日之前的数据生成 T 日特征。close.pct_change(5)在计算时不会用到未来数据安全但如果直接用close.shift(-5)这种去构造下游特征就会把未来的价格信息嵌入训练集这在课设里属于致命的数据泄露。做特征表时每一列后面最好备注“是否用到未来数据”这个习惯在答辩时非常加分。2.3 从零构造最小可用数据集用 pandas 构造训练集是课设里第一个能实际运行的环节。下面这段代码可以直接保存为prepare_data.pyimport pandas as pd import numpy as np def load_and_prepare(csv_path, predict_days5): df pd.read_csv(csv_path, parse_dates[date]) df df.sort_values(date).reset_index(dropTrue) # 基础特征 df[ret_1] df[close].pct_change(1) df[ma5] df[close].rolling(5).mean() df[ma20] df[close].rolling(20).mean() df[vol_ratio] df[volume] / df[volume].rolling(20).mean() df[high_low_range] (df[high] - df[low]) / df[close] df[ret_5] df[close].pct_change(5) # 标签未来 predict_days 日收益率 df[label] df[close].shift(-predict_days) / df[close] - 1.0 # 删除由滚动窗口产生的前几行空值以及最后没有标签的行 df df.dropna().reset_index(dropTrue) return df df load_and_prepare(stock_data.csv, predict_days5) print(df[[date, close, ret_1, ma5, ma20, label]].tail())代码里有两处需要注意。第一dropna()会同时删除滚动窗口产生的开头空值和标签缺失的末尾行这是符合逻辑的。第二sort_values(date)必须放在最前面如果原始 CSV 没有按日期排序后续所有滞后特征都会错位。我见过一份课设源码数据是倒序存的特征算完之后预测结果看起来很好其实是把未来的数据当成了历史这是比较典型的返工原因。2.4 按时间切分数据训练集和测试集不能随机打乱时间序列数据切分有一条铁律训练集必须全部在测试集之前。train_test_split默认随机打乱会从整个时间区间里抽出一部分做训练剩下的做测试这对股票预测没有任何参考意义。常见做法是固定一个截止日期之前是训练集之后是测试集。train_df df[df[date] 2023-01-01] test_df df[df[date] 2023-01-01]这种切分方式会把最近一段行情完整留出来做最终验收。切分时还要注意一点如果特征里有滚动窗口比如 ma20那么在截止日期附近训练集末尾和测试集开头使用的历史收盘价是来自同一段行情这不是泄露因为生成特征时只用到了当天及之前的数据。真正的坑是标签泄露要检查训练集中最后一行的 label 对应的时间点是否已经越过截止日期如果越过了需要在切分前用dropna或手动截掉尾部样本。3. 用 python 源码实现可复现的多模型对比基准3.1 最小实现写一个结构清晰的训练脚本期末课设的评审老师通常会先看代码能不能直接运行再看运行结果是否稳定。一个结构清晰的训练脚本应该把数据准备、模型定义、训练评估、结果输出四个部分拆开。下面这段代码是train_models.py的核心骨架from sklearn.linear_model import LinearRegression from sklearn.ensemble import RandomForestRegressor from sklearn.svm import SVR from sklearn.metrics import mean_absolute_error, mean_squared_error import numpy as np import pandas as pd feature_cols [ret_1, ma5, ma20, vol_ratio, high_low_range, ret_5] X_train train_df[feature_cols].values y_train train_df[label].values X_test test_df[feature_cols].values y_test test_df[label].values models { LinearRegression: LinearRegression(), RandomForest: RandomForestRegressor(n_estimators200, random_state42), SVR: SVR(C1.0, gammascale) } results [] for name, model in models.items(): model.fit(X_train, y_train) y_pred model.predict(X_test) mae mean_absolute_error(y_test, y_pred) rmse np.sqrt(mean_squared_error(y_test, y_pred)) mape np.mean(np.abs((y_test - y_pred) / y_test)) * 100 results.append({model: name, MAE: mae, RMSE: rmse, MAPE: mape}) result_df pd.DataFrame(results) print(result_df.round(4))代码先用一个字典集中维护模型再循环训练、评估、记录结果。这样做的好处是后续新增模型时只需要在models里加一行其余代码不用动。三个评估指标里MAPE 在 y_test 接近 0 时会变得异常大这是回归类预测的常见问题如果行情处于横盘区间MAPE 的参考价值会下降建议同时看 MAE 和 RMSE。3.2 多模型对比不同算法的适用边界课设里不需要堆砌十几个模型三到四个有代表性的就够线性回归做基线随机森林体现非线性拟合能力SVR 展示对高维特征的敏感性如果环境允许可以再加一个HistGradientBoostingRegressor。四类模型的对比情况如下模型适合场景训练速度调参重点易踩的坑线性回归特征与目标近似线性关系极快无对异常值敏感随机森林非线性、特征间有交互快n_estimators, max_depth对趋势外推能力弱SVR小样本、非线性慢C, gamma, kernel特征尺度敏感必须先归一化HistGBR中大规模数据快learning_rate, max_iter小数据集上容易过拟合我在做课设时会先跑线性回归把它的 RMSE 当作基准。如果随机森林和 SVR 在测试集上的表现连线性回归都明显不如说明特征构造有问题而不是模型问题。这是一个很有用的排错思路模型再复杂也要能稳定跑赢一个最简单的基线。3.3 方向准确率比 RMSE 更贴近业务含义的指标回归误差低不代表预测方向正确。股票预测课设里导师常问一个问题你这个模型预测的收益率是涨是跌和实际方向一致的比例有多高。方向准确率的计算很简单direction_acc np.mean(np.sign(y_pred) np.sign(y_test))np.sign把预测值和真实值都映射为 -1、0、1再比较是否一致。这个指标天然绕开了 MAPE 在零值附近的异常波动。我一般会把方向准确率随测试时间的变化画成折线图如果模型在一段持续上涨行情里方向准确率反而很低说明特征可能缺失了趋势项比如 ma20 与 close 的相对位置。这种分析会让课设报告看起来更有深度而不是只贴一张混淆矩阵。3.4 把训练、评估、预测封装成一次运行脚本完整代码不应该是一堆 Jupyter Notebook 单元格而应该是一个能直接跑通的脚本。常见做法是把主流程放在main()函数里数据准备、训练、评估、画图依次执行最后把预测结果保存为 CSVdef main(): df load_and_prepare(stock_data.csv) train_df, test_df split_by_date(df, 2023-01-01) results, y_pred train_and_evaluate(train_df, test_df) results.to_csv(metrics.csv, indexFalse) pd.DataFrame({actual: test_df[label].values, pred: y_pred}).to_csv(predictions.csv, indexFalse) if __name__ __main__: main()这样设计的目标是别人拿到你提交的代码只要安装好依赖执行一行python train_models.py就能复现最终结果。这对期末课设的评审效率非常重要也是项目说明里“可复现性”这一节最直接的支撑。4. 参数调优与时间序列交叉验证把模型稳定性和边界写进课设4.1 为什么 K 折交叉验证不适合股票预测场景Scikit-learn 的KFold默认会把数据分成 K 份随机抽取 K-1 份训练1 份验证。在时间序列上随机抽取会导致验证集的一些样本在时间上早于训练集样本模型等于用未来数据验证过去。我一般用TimeSeriesSplit替代from sklearn.model_selection import TimeSeriesSplit tscv TimeSeriesSplit(n_splits5, gap5) for train_index, val_index in tscv.split(X_all): X_tr, X_val X_all[train_index], X_all[val_index] y_tr, y_val y_all[train_index], y_all[val_index] # 每次训练集在时间上严格早于验证集gap5这个参数容易被忽略。当标签是未来 5 日收益率时训练集最后一行样本的 label 会用到训练集截止日期之后第 5 天的数据如果验证集紧挨着训练集开始这一小段重叠会导致交叉验证分数虚高。把gap设为预测窗口大小正好把这段缓冲期空出来。4.2 用 GridSearchCV 调参但交叉验证器必须换成 TimeSeriesSplit调参是课设里比较容易被追问的环节。直接用GridSearchCV默认的 KFold也会踩同样的时间泄露问题。正确的做法是把cv参数替换成TimeSeriesSplitfrom sklearn.model_selection import GridSearchCV from sklearn.ensemble import RandomForestRegressor from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler pipe Pipeline([ (scaler, StandardScaler()), (model, RandomForestRegressor(random_state42)) ]) param_grid { model__n_estimators: [100, 200], model__max_depth: [3, 5, 8], model__min_samples_split: [2, 5] } grid GridSearchCV( pipe, param_grid, cvTimeSeriesSplit(n_splits5, gap5), scoringneg_mean_absolute_error ) grid.fit(X_train, y_train) print(grid.best_params_, grid.best_score_)这里的StandardScaler放进了 Pipeline它的作用是保证每次交叉验证中scaler 只在当前训练折上拟合验证折的数据只用训练折得到的均值和方差做转换。这个细节是防止数据信息向过去泄漏的关键。对于随机森林这类树模型归一化实际上不会影响预测结果但放进 Pipeline 的好处是当换成 SVR 或线性模型时不需要改变整体结构。4.3 归一化与缺失值填补里的隐性数据泄漏先说归一化。一个常见的错误是在切分训练集和测试集之前对全部特征做StandardScaler().fit_transform(df)。这时 scaler 已经“见过”测试集的均值和方差相当于把未来数据的分布信息带进了训练阶段。正确做法是在训练集上 fit在测试集上只 transform。scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test)注意第二行用的是transform不是fit_transform。如果手滑写成了fit_transform测试集的统计信息会重新计算等于训练和测试各自做了归一化两个数据集的数据分布就不再可比。再看缺失值填补。时间序列特征里的缺失值通常出现在滚动窗口的最前面几天。常见做法是直接丢弃这些行而不是用整列均值填充。因为均值是全量数据的统计量填充进去之后模型会以为开头几天的价格接近历史平均水平这不符合真实交易场景。正确的做法是按行删除如果确需填充应该只使用该样本之前的窗口数据计算出的前向填充值。# 推荐直接删除空值 df df.dropna().reset_index(dropTrue) # 不推荐使用包含测试集信息的全局均值 df df.fillna(df.mean())4.4 把可复现细节整理进项目说明期末课设的项目说明不需要写长篇大论但要把数据切分方式、预测窗口、调参范围、评估指标这四个决策点写清楚。我一般建议按下面四个小节组织数据说明数据来源、时间跨度、字段含义、清洗规则。特征工程特征列表、对应计算方式、为什么选择这些特征。模型与调参对比了几种模型、参数搜索范围、TimeSeriesSplit 的切分设置。评估与失败案例分析测试集上的 MAE、RMSE、方向准确率以及最失败的模型为什么失败。这一节的价值在于项目说明不只是代码的说明书还是课设评审时回答“为什么这么做”的依据。评审老师不会要求你的模型准确率很高但会要求每一个技术选择都有可讲清楚的逻辑。5. 影子预测用一段“不可见”的行情验证算法是否真的有效期末课设里有一个容易被忽视的验证技巧把最近一段行情整体隐藏只让模型看到隐藏段之前的全部数据然后预测这段隐藏的行情。这种做法我叫它影子预测它模拟的是真实交易中“站在当下预测未来”的状态能有效检验模型是否依赖了未来的信息。实现时特征用全量序列计算但 label 必须只保留到训练截止日train_df df[df[date] 2024-01-31] shadow_df df[df[date] 2024-01-31] # 训练标签对应的时间点必须早于影子段起点 train_df train_df.dropna(subset[label]) model RandomForestRegressor(n_estimators200, random_state42) model.fit(train_df[feature_cols], train_df[label]) shadow_pred model.predict(shadow_df[feature_cols]) shadow_real shadow_df[label].values # 影子段行尾的 label 可能为 NaN需截断对比 mask ~np.isnan(shadow_real) shadow_pred shadow_pred[mask] shadow_real shadow_real[mask] shadow_mape np.mean(np.abs((shadow_real - shadow_pred) / shadow_real)) * 100 shadow_direction_acc np.mean(np.sign(shadow_real) np.sign(shadow_pred)) print(f影子预测 MAPE: {shadow_mape:.2f}%) print(f影子预测方向准确率: {shadow_direction_acc:.2%})这段代码要特别注意dropna的位置。影子段最后几行的 label 是由未来数据生成的但在这个模拟场景里这些未来的价格对模型而言是不可见的因此必须把shadow_real中的 NaN 截断而不是用别的值填充。如果模型在影子预测上的误差和早期测试集结果差距不大说明模型没有过拟合到某一段特定行情如果误差突然恶化优先检查特征列里是否混入了时间戳本身的信息。把影子预测的脚本单独存成一个文件在项目说明里附上运行日志和方向准确率可以作为整个课设代码有效性的直接证据。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进