☰
机器学习股票预测大作业指南:数据处理、模型评估与避坑
2026/10/3 3:40:26 网站建设 项目流程

简介:一套面向计算机专业期末大作业与课程设计场景的机器学习股票预测算法源码项目,出自大三学生之手,经导师指导评审,获得99分。资源包共13个文件、约2.53MB,以6个Jupyter Notebook为主体,覆盖K线数据入库、FFT滤波、LSTM预测、小市值结合盈利指标过滤及Sklearn单票回测,另有2个Python脚本负责数据采集与特征处理,3个CSV文件提供示例数据,可直接运行复现。完整代码经过验证,能帮助初学者快速跑通“数据获取—特征构建—模型训练—回测评估”全流程;通过阅读笔记式Notebook和README还能理解各模块调用关系与排错思路。此外,项目保留中间数据与回测结果,便于对照验证每一步改动效果,适合作为课程设计、期末大作业或机器学习入门实战参考。目前已有83人学习下载,属于评分较高、实用性较强的完整项目。

1. 机器学习股票预测,到底能拿到什么样的“高分大作业”

“Python的机器学习股票预测算法源码项目”,这个标题在课程作业和毕设里出现频率极高,搜索它的人多半不是在找能实盘的赚钱系统,而是想交一份让老师挑不出毛病、能讲清楚原理、代码能跑出合理结果的课程项目。现实一点说,用机器学习预测股票涨跌,准确率做到所谓“稳定盈利”是不现实的,但作为一次完整的“数据获取 → 特征工程 → 模型训练 → 结果评估”的大作业,它几乎是所有选题里性价比最高的方向:数据免费、代码有现成框架、可视化效果好看、答辩时故事也好讲。

但有句血泪经验得先说在前头:股票预测这个题,最大的坑不在模型,而在数据处理和评估方式。很多同学第一次跑通代码,发现训练集准确率95%,以为稳了,一测测试集直接50%瞎猜水平,甚至发现模型在“用今天的信息预测昨天”。这类问题每一个都足以让答辩翻车。这篇文章把从数据清洗到模型评估的全流程拆开讲,每一步给可照抄的代码,最后集中列常见踩坑,目标是让你拿到一份能运行、能解释、老师问不倒的“高分大作业”。

2. 预测什么、用什么预测:先把问题的边界定清楚

2.1 数据源怎么选:tushare、akshare、baostock怎么取舍

做股票预测第一步是拿数据。常见的免费数据源有三个:tushare、akshare、baostock。从大作业的角度,我一般推荐baostock,或者tushare的积分接口。原因很简单:它们返回的都是pandas DataFrame,字段命名规整,不需要自己解析JSON,而且历史数据覆盖A股从2000年前后到现在的日线行情,足够做10年以上的回测。

三者的差别在细节上:

  • baostock:免费、无需注册token,但只提供行情和基础财务数据,数据更新到当天收盘后,接口偶尔抽风,重试几次能好。
  • tushare:社区积分够用的话很方便,日线接口返回的复权因子、涨跌幅字段直接帮你算好,省掉自己复权的麻烦。但新注册用户积分不够,部分接口调不了。
  • akshare:接口最丰富,但有相当一部分接口返回的字段名不稳定,今天叫date明天叫日期,做作业时为了修字段名浪费时间,性价比低。

我的建议是:优先tushare,如果没有积分就用baostock。下面代码以baostock为例,因为直接能跑通,不需要任何注册。

import baostock as bs import pandas as pd # 登录baostock,这个登录只是建立本地连接,不涉及账号 lg = bs.login() # 获取平安银行(000001)从2015年到2023年的日线数据 rs = bs.query_history_k_data_plus( "sz.000001", "date,code,open,high,low,close,preclose,volume,amount,pctChg", start_date='2015-01-01', end_date='2023-12-31', frequency="d", adjustflag="2" # 前复权,避免分红送股造成价格跳空 ) data_list = [] while (rs.error_code == '0') and rs.next(): data_list.append(rs.get_row_data()) df = pd.DataFrame(data_list, columns=rs.fields) bs.logout()

这里adjustflag="2"是前复权,做股票预测必须复权,否则分红除权造成的价格下跌会让模型以为“跌了”,其实是正常分红。前复权更适合做技术面特征计算,因为历史价格会被调整到当前视角,避免了图表上的虚假缺口。

2.2 标签怎么定:预测涨跌方向比预测价格靠谱得多

拿到行情数据后,第一个关键决策是:预测目标到底是什么。很多新手上来就做回归,用LSTM预测明天的收盘价,然后发现预测曲线总是比真实曲线“慢半拍”——昨天涨,模型预测今天涨,本质上只是在做一个滞后一日的跟踪,没有任何实际意义。

大作业里最稳妥的做法是把它定义成二分类问题:明天相对于今天是涨还是跌。这样模型的任务更简单,评估指标也更直观,答辩时你能够清楚地解释准确率、F1值代表什么。构造标签的代码如下。

# 预测目标:明天收盘价是否高于今天收盘价 df['close'] = df['close'].astype(float) df['label'] = (df['close'].shift(-1) > df['close']).astype(int) # shift(-1)会把明天的价格拉到今天这一行,所以最后一行是没有标签的 df = df.iloc[:-1].copy()

shift(-1)是这里最容易出错的地方。它把明天的收盘价“提前”到今天,用来判断今天的收盘价相对明天是高还是低。因为最后一行没有明天的数据,必须删掉,否则会用NaN参与训练,轻则报错,重则pandas自动丢弃那一行,你还没发现。做完这一步,检查一下df['label'].value_counts(),如果涨跌比例接近1:1,说明标签构造正常;如果出现极端偏差,就要回头查数据是否按时间排序过。

2.3 特征怎么造:不依赖未来数据的20个技术指标特征

特征工程是拿高分的关键。常见的做法是把技术指标当成特征,但这里有一个大坑:很多开源库计算指标时,默认使用了未来数据。比如有些版本的指标计算会对整段序列做中心滑动平均,或者用未来一段时间的最高价来算威廉指标——这在高频交易里叫“未来函数”,是大作业里最容易翻车的技术细节。

我的做法是用一个通用函数,基于已收盘的历史数据逐行滚动计算特征,保证每一行的特征只依赖于截至当天的数据。以下代码构造了动量、均线、波动率、成交量变化四类代表特征,实际作业中可以扩充到20个以上。

# 按时间正序排列,确保滚动窗口是“过去到当前” df = df.sort_values('date').reset_index(drop=True) # 动量特征:过去5日、10日收益率 df['ret_5'] = df['close'].pct_change(5) df['ret_10'] = df['close'].pct_change(10) # 均线偏离度:收盘价相对20日均线的偏离百分比 df['ma_20'] = df['close'].rolling(20).mean() df['ma_20_dev'] = (df['close'] - df['ma_20']) / df['ma_20'] # 波动率:过去10日收益率的标准差 df['volatility_10'] = df['close'].pct_change().rolling(10).std() # 成交量变化:今日成交量相对5日均量的比值 df['volume_ratio'] = df['volume'] / df['volume'].rolling(5).mean() # 删除前面因滚动窗口产生的NaN行 df = df.dropna().reset_index(drop=True)

这里的核心原则是:所有特征都只能由“截止到当天收盘已经存在的数据”计算出来。pct_change(5)用的是前5天到今天的收盘价,rolling(20).mean()用的是最近20天,都不包含未来信息。答辩时如果老师说“你怎么保证没有用未来数据”,你直接把这段特征构造逻辑展示出来,比嘴上解释一百遍都有说服力。

特征数量也不是越多越好。20个维度用随机森林或XGBoost还算稳,但如果你后面接了神经网络,特征太多而样本只有几万个,很容易过拟合。一般大作业控制在15到30个特征之间,足够展示工作量,又不会因为维度灾难导致模型学不动。

3. 模型选型与训练:从逻辑回归到LSTM的完整路线

3.1 为什么先跑逻辑回归:基线模型让你知道上限在哪

股票涨跌预测这个题目有一个常见误解:模型越复杂越好。实际上,真实行情里信噪比极低,你再怎么调LSTM,准确率也很难超过55%太多。所以正确路线是先跑一个逻辑回归作为基线,它训练快、可解释性强、几乎不可能过拟合,而且答辩时你可以说:“我用了简单模型作为baseline,准确率是X,复杂模型在此基础上提升了Y。”这套话术本身就比“我用了一个LSTM准确率52%”更有说服力。

逻辑回归在sklearn里跑起来也就几行代码,但这里必须注意一个数据处理细节:要做特征标准化。逻辑回归用梯度下降优化,特征尺度不一致会导致收敛慢甚至不收敛。股价是几十块钱的量级,收益率是百分之几,不标准化的话模型会把注意力全放在大数值特征上。

from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import StandardScaler from sklearn.metrics import accuracy_score, f1_score feature_cols = ['ret_5', 'ret_10', 'ma_20_dev', 'volatility_10', 'volume_ratio'] X = df[feature_cols].values y = df['label'].values # 按时间顺序切分:前80%训练,后20%测试 split_idx = int(len(X) * 0.8) X_train, X_test = X[:split_idx], X[split_idx:] y_train, y_test = y[:split_idx], y[split_idx:] scaler = StandardScaler() X_train = scaler.fit_transform(X_train) X_test = scaler.transform(X_test) # 注意用训练集拟合的scaler直接转换测试集 model = LogisticRegression(max_iter=1000) model.fit(X_train, y_train) y_pred = model.predict(X_test) print(f"准确率: {accuracy_score(y_test, y_pred):.4f}") print(f"F1: {f1_score(y_test, y_pred):.4f}")

两个细节值得说。第一,切分必须按时间顺序,不能直接用train_test_split随机打乱。股票数据是时间序列,用未来数据训练再去预测过去,准确率虚高没有任何意义。第二,scaler只能用训练集拟合,然后直接转换测试集,如果你在全部数据上做标准化再切分,测试集的信息已经渗入训练过程,属于典型的数据泄露。

3.2 复杂模型怎么加:随机森林与XGBoost的调参边界

基线跑通后,作业的工作量体现就在“模型对比”上。最常见的组合是加一个随机森林和一个XGBoost。这两个模型对特征标准化不敏感,可以直接用原始特征,但都有各自的过拟合风险。

随机森林的n_estimators不要一味加大,300到500就够,再多只会增加训练时间不提升效果。更重要的是max_depth和min_samples_leaf,股票数据噪声大,树一旦长得太深就会把历史行情中的随机波动当成规律,训练集准确率能到90%,测试集立刻打回原形。

from sklearn.ensemble import RandomForestClassifier rf_model = RandomForestClassifier( n_estimators=300, max_depth=5, # 限制深度,防止过拟合 min_samples_leaf=50, # 叶子节点最少样本数,让模型学大趋势而非个例 random_state=42, n_jobs=-1 ) rf_model.fit(X_train, y_train) y_pred_rf = rf_model.predict(X_test) print(f"随机森林准确率: {accuracy_score(y_test, y_pred_rf):.4f}")

min_samples_leaf=50这个参数在股票预测里几乎是救命稻草。默认值是1,意味着每个叶子节点可以只装一个样本,模型会把历史中某一天的特殊涨跌完全记住。设成50后,模型被迫寻找至少50个样本共同成立的规律,泛化能力强很多。如果你发现训练集准确率和测试集差超过15个百分点,优先调这个参数而不是加正则化。

XGBoost的话,learning_rate设0.05左右,n_estimators不用早期停止的话设300就够。注意XGBoost对缺失值有自己的处理逻辑,但我们的特征已经用dropna()处理过,不需要依赖这个特性。

3.3 LSTM要不要上:小样本下如何避免“深度学习翻车”

如果作业要求里写了“可以使用深度学习”,或者你想冲刺高分,加一个LSTM是有必要的。但必须说清楚:LSTM在几千到几万样本的日线数据上,效果大概率不如XGBoost,这是个正常现象,不代表你做错了。答辩时你可以理直气壮地说:“LSTM在长序列建模上有优势,但日线级别的特征序列长度较短、信噪比低,它的优势发挥不出来。”这句话本身就是对模型原理的理解体现。

LSTM的输入和传统模型完全不一样。它需要构造三维张量:(样本数, 时间步长, 特征维度),意思是每一个样本不是一行特征,而是一段连续的历史窗口。比如用过去30天的5个特征来预测第31天是涨是跌,那么样本形状是(N, 30, 5)。

import numpy as np from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout def make_sequence_data(X, y, seq_len=30): X_seq, y_seq = [], [] for i in range(seq_len, len(X)): X_seq.append(X[i - seq_len:i]) # 过去seq_len天的特征 y_seq.append(y[i]) # 当天的标签 return np.array(X_seq), np.array(y_seq) # 用同样的标准化逻辑,注意必须用训练集的scaler X_test_scaled = scaler.transform(X_test) # 这里以测试集为例 X_seq, y_seq = make_sequence_data(X_test_scaled, y_test, seq_len=30) model_lstm = Sequential([ LSTM(32, input_shape=(X_seq.shape[1], X_seq.shape[2]), return_sequences=False), Dropout(0.3), Dense(1, activation='sigmoid') ]) model_lstm.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy']) model_lstm.fit(X_seq, y_seq, epochs=5, batch_size=64, validation_split=0.1, verbose=0)

LSTM这里只写了测试集部分来演示构造流程,实际训练时你需要把训练集也转成序列格式。epochs=5是我习惯的起点,股票预测任务epochs超过10基本就开始过拟合了,训练集loss持续下降,验证集loss反而上升。LSTM(32)的32是隐藏单元数,这个值不要贪大,日线数据模式简单,32个单元足够捕捉周期规律了。注意input_shape里第一个维度是时间步长,第二个是特征维度,顺序反了会在fit时报维度错误,而且这种错误在报错信息里很不直观,排查起来很费时间。

4. 机器学习股票预测的避坑清单:五个让项目翻车的常见问题

4.1 现象:准确率极高但结果完全没用

训练集准确率95%,测试集准确率48%,这不是“模型没调好”,而是你的数据处理阶段出了问题。最常见的原因是特征或标签包含未来信息。比如用shift(-1)构造标签时,如果同时把明天的pctChg当成了特征,模型实际上是在“抄答案”。检查方法很简单:把feature_cols里的列逐一画时间序列,用眼睛对比特征和标签的走势,或者把数据按照日期正序排列后,随机抽几行人工验证特征是否只依赖当天及之前的数据。

解决方式是在特征计算完成后加一道“禁用清单”:把所有通过shift()负向移动过的列全部剔除出特征集,并且打印特征列名让老师看到。这比任何答辩话术都有说服力。

4.2 现象:每次运行结果都不一样

随机森林的random_state没设固定值,LSTM的初始化权重是随机的,这些都会导致每次跑出来的准确率略有浮动。有个搜索热词叫“模型预测股票涨跌每次结果不一样”,这几乎是股票预测项目里被问得最多的问题。原因就是没有固定随机种子。

解决方式是全局设置随机种子。Python层面、numpy层面、tensorflow层面各设一次,缺一个都不行。代码放在脚本最顶部,确保所有库import之后、任何数据处理之前执行。

import random import numpy as np import tensorflow as tf random.seed(42) np.random.seed(42) tf.random.set_seed(42)

另外,XGBoost和LightGBM内部也有随机性,XGBoost里设random_state=42即可。这样处理后,任何人在同一份数据上跑你的代码,结果都是一致的,这在大作业评分里是一个很加分的稳定性指标。

4.3 现象:特征里有大量NaN,模型直接报错

pct_change(5)会让前5行变成NaN,rolling(20).mean()会让前19行变成NaN,如果你连续计算多个特征,前几十行全是NaN。很多新手在这个环节选择df = df.dropna(),然后发现自己数据少了几十行——这没问题,但如果你的数据是按时间排序的,删除的行是最早的几十天,这也没问题。问题是有些人为了保留数据量,用fillna(method='bfill')把后面的值往前填,这等于把未来数据塞进了历史特征,模型学到的是“用明天填充的数据预测明天”,典型未来函数。

解决方式是先排序、再算特征、最后dropna(),并用reset_index(drop=True)重置索引。不要用bfill,也不要用全局均值填充滚动窗口产生的NaN。

4.4 现象:时间序列说得头头是道,切分却用了随机打乱

from sklearn.model_selection import train_test_split是股票项目里最常见的一行“自杀代码”。train_test_split默认随机打乱数据,你的测试集里会出现2021年的数据,训练集里会有2023年的数据,模型用未来信息训练,测试集准确率虚高。这里没有商量的余地:股票数据必须按时间顺序切分,训练集会早于测试集。

解决方式是手动切分,或者用TimeSeriesSplit做交叉验证。大作业场景下直接手动按比例切分最简单,写代码时记得加注释“按时间顺序切分,防止未来信息泄露”,让评分老师一眼看到你知道这个坑。

4.5 现象:用过采样解决涨跌不平衡,结果更差了

有些同学用SMOTE对训练集做上采样,试图把上涨和下跌样本比例拉平。方向上没错,但在时间序列上做SMOTE有一个致命问题:它会在样本之间插值,合成的新样本可能混合了不同时间的特征模式,破坏了时间上的因果结构。更实际的问题是,A股日线级别的涨跌样本本来就接近1:1,你检查一下就知道根本不需要过采样。

解决方式是:如果你的标签分布比例在4:6到6:4之间,不要做任何采样处理,直接训练。只有极端不平衡才需要处理,而股票日线数据很少出现这种情况。

5. 用滚动回测验证模型:高分大作业的“最后一公里”

5.1 为什么静态切分不够:模拟真实的交易节奏

前面用的按时间8:2切分只适合展示“模型在未知数据上的表现”,但大作业如果止步于此,答辩时很容易被追问:“你这个模型如果放到真实交易里,能不能赚钱?”静态切分无法回答这个问题,因为它只测了一次。真实场景中模型需要不断用新数据重新训练,所以更高阶的做法是滚动回测:把5年数据切成多段,每段先用前3年训练、后6个月测试,然后窗口向前滚动6个月,循环多次,最后把多次测试结果合并统计。这样才能得到一个有统计意义的评估结论。

5.2 模拟手续费和滑点:准确率之外你必须算的两笔账

光看准确率在股票预测作业里不算完整。真实交易里有交易成本,A股按双边万三到万五算手续费,还有印花税,如果换手率过高,就算预测准确率52%,收益也可能是负的。建议在回测里加一个简单的成本模型:每次预测到“上涨”就买入,次日收盘卖出,单次交易扣除0.1%的成本。这样回测结果里的收益率才是“可解释的数字”,而不是纸上富贵。

5.3 命中率稳定性和换手率:额外付加分的指标

大作业评分的核心参考指标有三个:准确率、F1、回测累计收益。但如果你想多拿分,建议再输出两个指标:预测为上涨时实际下跌的比例(看空时误判率),以及全样本的换手率。第一个指标衡量模型在“看涨”时的可靠性,第二个衡量模型是否过于频繁交易。我自己习惯把这两个指标打印在回测结果的最下方,作为“补充风险评估”,红笔一勾,作业的完成度立刻不一样。

5.4 复现性检查:一份能锁住随机性的代码才是好作业

最后养成一个习惯:所有代码跑完后,把模型参数和随机种子一并写进一个配置字典,保存成JSON文件。下次重跑时加载它,保证结果一致。我当时做这个项目最大的教训是没有在项目初期固定随机种子,结果中期加入了新特征后,所有旧结果都无法复现,白白重跑了一天实验。这份配置文件的代码不值得贴在这里,但请务必在你的项目里做同样的事,设置为seed = 42并全局生效。希望帮到你。

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

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

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

立即咨询