简介:面向毕业设计、期末大作业与课程设计的Python机器学习实战项目,基于sklearn完成股票预测模型,覆盖数据读取、特征处理、多模型回归预测与结果对比。资源共14个文件、约24.25MB,主体为4个Python脚本和1份CSV数据文件,另有依赖包清单与若干工程配置文件;代码均带详细注释,新手可读懂建模全流程,依赖说明便于快速复现运行环境。压缩包内数据与代码分离,多个入口脚本分别对应不同预测策略,便于二次修改与结果验证。目前已有245人浏览下载,适合需要快速搭建完整项目的学生参考,也适合导师要求较高的高分毕设或课程设计直接部署使用。整体结构紧凑,下载后简单配置即可运行,能有效节省数据清洗与调参时间。
1. 说的不是玄学:用 sklearn 做股票预测到底在预测什么
刚接触 python 机器学习的时候,很多人对基于 sklearn 模型的股票预测有个误解:以为它能告诉你明天收盘价是多少、下周该不该买入。实际上,这类“高分代码”能稳定交付的,是用历史行情训练一个 sklearn 分类器,预测未来一个交易日或未来几天的涨跌方向。它的价值不在收益率,而在把「数据获取 → 特征构造 → 模型训练 → 回测评价」这条链路完整跑通。做课程设计、毕业设计,或者想入门量化交易,拿它练手是合适的;指望它替代人工判断,那是把它用错了地方。
真实场景里,你拿到的原始数据只有日期、开盘价、收盘价、最高价、最低价、成交量这些字段,股票预测代码要解决的核心问题是:从这些字段里构造出能描述“昨天状态”的特征,再找到“今天涨跌”作为标签。整个项目的难点不在模型调参,而在数据对齐和防止未来函数泄漏。这篇文章我会从数据准备讲起,把特征、模型、回测、踩坑一条龙讲完,你跟着步骤就能在本地把代码跑起来。
2. 先把行情数据弄干净:从日线数据到能喂给 sklearn 的训练集
网上流传的股票预测代码,十份里有八份输在数据处理上。sklearn 不管你的数据是从哪来的,它只认「特征矩阵 X 和标签向量 y」,并且要求 X 里的每一行都只包含此刻之前已知的信息。这一章先把地基打牢。
2.1 用 baostock 拉日线数据:最小脚本与字段含义
获取A股日线行情的常见做法是用 baostock,免费、不需要 token,返回的是 pandas DataFrame,对 sklearn 生态很友好。下面是一个最小脚本,拉取一支股票最近三年的日线数据。
import baostock as bs import pandas as pd # 登录 baostock 数据服务 lg = bs.login() if lg.error_code != '0': raise RuntimeError(f'baostock 登录失败: {lg.error_msg}') # 拉取日线数据,adjustflag=2 表示前复权 rs = bs.query_history_k_data_plus( "sh.600000", "date,code,open,high,low,close,volume,amount,turn", start_date='2021-01-01', end_date='2023-12-31', frequency="d", adjustflag="2" ) rows = [] while rs.error_code == '0' and rs.next(): rows.append(rs.get_row_data()) df = pd.DataFrame(rows, columns=rs.fields) bs.logout() print(df.head()) print(df.shape)代码逻辑说明:先调用bs.login()建立会话,然后通过query_history_k_data_plus按股票代码和日期区间拉取日线。字段里date是交易日,open/high/low/close是价格,volume是成交量(股),amount是成交额(元),turn是换手率。frequency="d"指定日线,adjustflag="2"表示前复权,这个参数很关键,后面避坑章节会细说。
注意 baostock 返回的字段默认是字符串,需要手动转数值。我在实际写代码时一般会加一步:把df[['open','high','low','close','volume','amount','turn']]用pd.to_numeric批量转换,再按日期排序、重置索引,避免后面特征计算时出现字符串拼接的诡异报错。
2.2 缺失值、停牌与时间索引:训练集里不能有未来函数
拿到原始日线后,第一件事不是训练,而是检查数据完整性。常见情况有三种:某几天停牌导致行数不足;刚上市的新股历史行情太短;除权除息日价格跳空。
# 转换日期并排序 df['date'] = pd.to_datetime(df['date']) df = df.sort_values('date').reset_index(drop=True) # 检查缺失值 print(df.isnull().sum()) # 缺失值处理:价格类用前向填充,成交量用 0 填充 df['volume'] = df['volume'].fillna(0) df[['open', 'high', 'low', 'close']] = df[['open', 'high', 'low', 'close']].fillna(method='ffill') # 设置时间索引 df = df.set_index('date')这里的前向填充(ffill)是有讲究的:如果某天 high 缺失,用前一天的最高价填充,比用均值填充更符合交易实际——停牌后复牌,价格自然会延续上一个交易日的水平。成交量填 0 是因为停牌期间确实没有成交。
关于“未来函数”,它是指特征里混入了未来信息。举个最常见的翻车写法:有人把「当天的收盘价」同时当作特征和标签的一部分,训练时准确率能到 95%,回测时一塌糊涂。正确做法是:预测 T 日的涨跌,特征只能用到 T-1 日及之前的数据。这一点在第 5 章会展开,这里先建立意识。
2.3 特征矩阵怎么拼:把「今天收盘」变成「明天的涨跌标签」
特征工程的常见做法是计算历史窗口的技术指标:过去 N 日收益率、最高价与最低价的振幅、成交量变化率、换手率均值等。这些指标在 T-1 日收盘后就能算出,不会泄漏未来。
# 构造特征 close = df['close'] volume = df['volume'] # 过去 5 日收益率(T-1 日可知) df['ret_5'] = close.pct_change(5) # 过去 10 日波动幅度 df['high_low_10'] = df['high'].rolling(10).max() / df['low'].rolling(10).min() - 1 # 成交量 5 日均值与当日比值 df['vol_ratio_5'] = volume / volume.rolling(5).mean() # 标签:T 日收盘价相对 T-1 日收盘价的涨跌,1 表示上涨,0 表示下跌 df['label'] = (close.shift(-1) > close).astype(int) # 删除带缺失值的行,并只保留特征列和标签列 feature_cols = ['ret_5', 'high_low_10', 'vol_ratio_5'] df_model = df[feature_cols + ['label']].dropna()pct_change(5)算的是 5 个交易日的累计收益率,rolling(10).max()是过去 10 天最高价的最高值。构造完特征后,label用的是close.shift(-1),表示下一交易日的收盘价,这就是我们要预测的目标。整个 DataFrame 里每一行的特征都只使用当天及之前的数据,不会泄漏。
如果你想预测未来 5 天是否上涨,可以把标签改成df['close'].shift(-5) > df['close'],但注意此时特征最好也用更长窗口,否则信息量不够。常见做法是把 5 日均线、10 日均线都加进特征,让模型有更多“历史状态”可看。
3. 涨跌分类还是回归价格:先定目标函数再选 sklearn 模型
很多初学者拿到股票数据后,第一反应是用LinearRegression预测明天的收盘价,画出来一条“预测线”,看起来很漂亮。但这种做法十有八九是过拟合。这一章说清楚为什么,以及怎么选更合适的目标。
3.1 回归预测收盘价:为什么课程设计里十有八九是过拟合
回归模型的损失函数是均方误差,模型会把注意力放在“把价格数值猜准”上。但股票价格是非平稳序列,今天的 10 元和明天的 10.05 元在模型眼里差异很小,可对交易来说这是两个完全不同的方向。
from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_squared_error X = df_model[feature_cols_plus] # 包含更多特征的矩阵 y_price = df['close'].shift(-1).dropna() # 预测下一个交易日的收盘价 model = LinearRegression() model.fit(X_train_price, y_train_price) pred = model.predict(X_test_price) print(mean_squared_error(y_test_price, pred))回归模型的均方误差可能很小,因为它只是在“重复昨天的价格”。你把预测结果画出来,预测曲线和真实曲线几乎重合,会误以为模型很厉害。实际上模型学到了一个近似“今天收盘价就是明天收盘价”的平庸解。这也是为什么我几乎不用回归做股票预测:它给出的结果很难转化为交易信号。
相比之下,把问题转化成「明天是涨还是跌」的二分类,模型的输出可以直接对应交易动作——预测1就持有或买入,预测0就观望或卖出。分类模型的评价指标(准确率、召回率)也更直观,能够直接回答“这个模型到底靠不靠谱”。
3.2 分类标签怎么定义:未来一天、未来五天与阈值
分类标签不是只有“明天涨跌”一种定义。具体怎么选,取决于你要做的交易频率。
| 标签定义 | 计算方式 | 适用场景 |
|---|---|---|
| 次日涨跌 | close.shift(-1) > close | 短线、T+1 交易 |
| 未来 5 日涨跌 | close.shift(-5) > close | 周频调仓 |
| 未来 N 日收益超阈值 | close.shift(-N) / close - 1 > 0.03 | 过滤小波动,只抓大行情 |
| 三分类(涨/平/跌) | 涨幅 >0.5% 为涨,<-0.5% 为跌,其余为平 | 更贴近真实交易,但类别不平衡严重 |
我一般建议初学者从「次日涨跌」开始,因为它和日线数据天然对齐,代码改动最小。唯一要注意的是:A 股是 T+1 交易制度,今天预测明天上涨、明天买入,最早后天才能卖出,实际收益会被交易成本吃掉一部分。做课程设计不需要考虑这些,但做实证时要心里有数。
标签阈值也不是拍脑袋定的。你可以在代码里对比不同阈值下正负样本的比例,选一个让样本不至于严重失衡的阈值。比如把阈值设为 0,如果上涨天数占 55%、下跌占 45%,这个分布对分类器来说还算健康。
3.3 随机森林、逻辑回归与 LightGBM:选型对比和参数起点
sklearn 里能做分类的模型很多,常用的有三类:线性模型、树模型、集成模型。
from sklearn.ensemble import RandomForestClassifier from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.metrics import accuracy_score X = df_model[feature_cols] y = df_model['label'] # 按时间顺序切分,shuffle=False 很关键 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, shuffle=False ) # 逻辑回归:线性基线 lr = LogisticRegression(max_iter=1000) lr.fit(X_train, y_train) # 随机森林:非线性基线 rf = RandomForestClassifier(n_estimators=100, max_depth=5, random_state=42) rf.fit(X_train, y_train) for name, model in [('LogisticRegression', lr), ('RandomForest', rf)]: pred = model.predict(X_test) print(f'{name} accuracy: {accuracy_score(y_test, pred):.4f}')这里shuffle=False是时间序列切分的关键,随机打乱会把未来数据混进训练集,造成数据泄漏。逻辑回归的优点是训练快、可解释性强,系数能直接看到每个特征的权重方向;随机森林能捕捉非线性关系,不容易被个别异常值带偏。
选型建议:如果特征数量少(十几个以下),先跑逻辑回归当基线,效果好就够用了;如果特征多、数据量大,优先尝试RandomForestClassifier,它的n_estimators控制在 100 左右,max_depth从 5 开始调。LightGBM 的LGBMClassifier在 sklearn 接口里也能用,训练更快,但对新手的坑更多,后面避坑章节会提到。
4. 训练、回测和评价:别让准确率骗了你
模型训练出来之后,最大的坑不是模型不收敛,而是你以为它收敛得很好。股票预测代码里,训练准确率高不代表能赚钱,甚至不代表模型学到了任何规律。这一章讲怎么正确地切数据、做回测、读指标。
4.1 时间序列不能随机切分:按时间做 train/valid/test
普通机器学习任务会把数据随机打乱,训练集和测试集分布一致。股票数据不行,因为 2021 年的行情和 2023 年的行情分布完全不同。正确做法是严格按时间顺序切分:前 70% 做训练,中间 15% 做验证调参,最后 15% 做测试。
train_size = int(len(df_model) * 0.7) valid_size = int(len(df_model) * 0.15) train = df_model.iloc[:train_size] valid = df_model.iloc[train_size:train_size + valid_size] test = df_model.iloc[train_size + valid_size:] X_train, y_train = train[feature_cols], train['label'] X_valid, y_valid = valid[feature_cols], valid['label'] X_test, y_test = test[feature_cols], test['label']为什么一定要留验证集?因为调参过程本身就是一种“偷看测试集”的行为。你反复在测试集上试参数,最后选出来的模型一定是被测试集“过拟合”的。用验证集做调参,测试集只用来做最终评估,评价结果才可信。
参数调优时,可以用GridSearchCV,但注意它的默认cv是 KFold 随机切分,必须改成TimeSeriesSplit。sklearn 里自带TimeSeriesSplit,用法和 KFold 一样,但切分时保证训练集时间永远在验证集之前。
4.2 回测函数怎么写:信号、持仓与收益统计
回测的本质是模拟交易:根据模型预测信号决定持有还是空仓,然后计算账户净值曲线。最小回测逻辑如下。
import numpy as np def backtest(close, signal, commission=0.001): """ close: 每日收盘价序列 signal: 每日持仓信号,1 表示持有,0 表示空仓 """ position = signal.shift(1) # 次日开盘执行当日信号,避免未来函数 daily_ret = close.pct_change().fillna(0) strategy_ret = position * daily_ret - commission * position.diff().abs().fillna(0) equity = (1 + strategy_ret).cumprod() # 计算年化收益率和最大回撤 total_return = equity.iloc[-1] - 1 max_drawdown = (equity.cummax() - equity).max() return total_return, max_drawdown # 生成信号:模型预测为 1 且概率大于 0.55 时才持有 prob = rf.predict_proba(X_test)[:, 1] signal = pd.Series((prob > 0.55).astype(int), index=test.index) total_return, max_drawdown = backtest(test['close'], signal) print(f'总收益率: {total_return:.2%}, 最大回撤: {max_drawdown:.2%}')signal.shift(1)这一步是回测的生命线:T 日收盘时产生信号,T+1 日开盘才执行,如果不 shift,就等于你“看到明天涨了才买入”,收益会虚高。手续费按 0.1% 计,是 A 股双边佣金加滑点的保守估计,不要省这一行。
4.3 评价指标怎么读:准确率、召回率与盈亏比
准确率是股票预测代码里最容易被滥用的指标。假设测试集里 70% 的样本是上涨,一个“永远预测上涨”的模型准确率就是 70%,看起来很高,但它没有任何实际价值。
from sklearn.metrics import classification_report pred = rf.predict(X_test) print(classification_report(y_test, pred, target_names=['下跌', '上涨']))重点关注召回率(recall)和精确率(precision)的组合:预测上涨的样本里,有多少真的涨了(精确率);所有真正上涨的样本里,模型抓到了多少(召回率)。做多策略更看重精确率,因为预测错误就要亏钱;选股策略更看重召回率,因为错过机会虽然不亏钱,但也没有收益。
另外一个实用指标是盈亏比:统计预测正确时的平均收益率和预测错误时的平均亏损率,比值大于 1.5 才说明模型有正向期望。这些统计可以直接在回测结果上计算,不要只看准确率一个数。
5. 避坑记:股票预测代码最容易翻车的五个位置
这部分是实战里最容易让代码“看起来能用、实际不能用”的五个坑。我按「现象 → 原因 → 解决」的方式写,方便你对照排查。
5.1 未来函数泄漏:训练阶段准确率 90%,回测一塌糊涂
现象:训练集和测试集上准确率都接近 90%,但一上回测,净值曲线一路向下,和模型预测方向完全相反。
原因:特征或标签里混入了未来信息。最典型的写法是把 T 日的所有字段(包括 close)都放进特征,同时标签又是“T+1 日是否上涨”。模型相当于同时看到了今天的收盘价和明天的涨跌,训练时当然准。
解决:构造特征时,统一用shift()把所有特征右移一天,或者严格按“T-1 日及之前的数据生成特征”来写。检查方法很简单:把每个特征列和标签列打印出来看最后几行,如果特征里出现未来的数值,就说明泄漏了。
5.2 随机种子没固定:同一个模型每次跑结果都不一样
现象:同一份数据、同一个模型,每次运行输出的准确率和回测收益都不同,有时甚至从盈利变成亏损。
原因:随机森林、逻辑回归求解过程都有随机性。train_test_split没固定random_state,模型内部也没固定随机种子,每次运行都在“重新抽签”。
解决:一切随机操作固定种子。在脚本开头设置np.random.seed(42),并在每个模型里显式传入random_state=42;train_test_split也要加random_state=42。这样跑出来的结果才可复现、可复核。
5.3 标签不平衡:七成上涨样本把模型学成了「一直看涨」
现象:模型预测结果里几乎全是 1(上涨),准确率 70%,但回测亏钱。
原因:A 股长期看是上涨的,日线级别上涨样本天然多于下跌样本。模型发现“全预测上涨”就能拿高分,于是走了捷径,没有真正学习特征模式。
解决:先统计y.value_counts(),确认正负样本比例。如果上涨超过 65%,考虑调整阈值、改用class_weight='balanced',或者用SMOTE做少数类过采样。最实用的是用概率阈值代替默认 0.5:只预测概率超过 0.6 的样本为上涨,其余一律看跌。第 4 章回测代码里的prob > 0.55就是这个思路。
5.4 数据端口期没对齐:训练集和测试集横跨不同市场环境
现象:模型在 2021-2022 年数据上训练,测试集是 2023 年,效果还行;但换到 2018 年的测试数据,模型完全失灵。
原因:股票数据是非平稳的,不同年份的市场波动率、行业风格差异很大。模型在牛市数据上学的规律,在熊市里可能完全反着来。
解决:切分数据时固定采用时间顺序,已经做到;更严谨的做法是滚动窗口训练:每次用过去 3 年数据训练,预测下一个月,然后窗口整体后移。这样评估的是模型“持续学习”的能力,而不是一次性赌对一段行情。
5.5 前复权和后复权选错:历史价格被「调整」得面目全非
现象:同样一只股票,有人用前复权数据训练,有人用后复权数据训练,特征值完全不同,模型结论也对不上。
原因:复权是为了消除除权除息造成的价格突变。前复权以最近价格为基准,历史价格会被调整,所以最新价格是真实价格,但历史价格不是当时的真实价格;后复权则相反。
解决:训练时统一用前复权(adjustflag="2"),因为回测时你需要用最新价格计算收益。但要注意,前复权数据在每次除权后都会变化,所以“固定一份历史数据”这个说法本身就是错的。正式做实验时,应该在脚本里记录数据下载时间和复权方式,存档备查。
6. 把每次跑出的结果稳定下来:随机种子、滚动窗口与保存现场
代码能跑通只是第一步,真正能用于对比实验的股票预测代码,要做到“可复现、可比较”。这一章给你三个具体习惯,都是实战里反复踩出来的经验。
第一个习惯是全局固定随机种子。我的做法是在脚本最开头写:
import numpy as np import random np.random.seed(42) random.seed(42)所有 sklearn 模型都显式传random_state=42。这样每次跑的结果完全一致,才有心情去对比特征或参数变化带来的真实差异。
第二个习惯是滚动窗口训练。不要只跑一次训练-测试就下结论。常见做法是写一个循环,从 2021 年开始,每 6 个月重新训练一次模型,预测下一个月,把每次的准确率和收益记录下来。这样得到的结果比单次切分稳定得多,也能看到模型在不同市场环境下的真实表现。
第三个习惯是保存模型和特征列表。训练完成后,用joblib.dump把模型保存到本地,同时把feature_cols存成 JSON。隔一周再跑代码时,哪怕数据源更新了,也能用旧模型和旧特征去对比新数据,知道模型有没有“过时”。
import joblib import json joblib.dump(rf, 'best_rf.joblib') with open('feature_cols.json', 'w') as f: json.dump(feature_cols, f)模型保存下来,之前调参的心血才不会白费。我自己的习惯是每次实验都建立一个目录,存模型、特征和回测曲线,命名带上日期和参数,方便后面复盘哪一个改动真正起了作用。
这套 sklearn 股票预测流程,从数据到回测,每一步我都踩过上面这些坑。最大的教训就是:股票预测代码的价值不在于模型多花哨,而在于数据有没有对齐、评价有没有骗人、结果能不能复现。希望这些经验帮到你,少走点我走过的弯路。
本文还有配套的精品资源,点击获取