简介:面向毕业设计与课程设计场景,这份基于sklearn的股票预测项目包含完整Python源码与配套数据,适合希望快速搭建机器学习预测流程的初、中级学习者。代码采用随机森林回归模型,完整覆盖数据获取、特征处理、模型训练与结果评估等关键环节,每段代码都配有详细注释,能有效降低上手门槛。压缩包共14个文件,包括4个Python脚本、1个CSV股票数据集、1份依赖清单以及若干XML/IML等IDE工程配置,既可直接运行,也便于按模块查阅修改;整体大小仅24.25MB,轻量易部署。目前已有245人学习下载。下载后可获得可直接运行的模型代码、历史行情数据与环境配置说明,简单安装依赖即可复现预测结果;项目结构清晰,注释中还包含数据字段与模型调参提示,既能帮助新手理解sklearn建模全流程,也可作为毕业设计或期末大作业的高分参考模板。
1. 用python机器学习基于sklearn模型做股票预测,先别急着上深度学习
用python机器学习基于sklearn模型做股票预测,很多人第一反应是上LSTM、Transformer,但真正让代码翻车的往往不是模型不够深,而是数据泄漏和切分错误。我见过不少初学者把随机森林跑到0.98准确率,一上测试集就崩,原因就是把未来数据卷进了特征。下面这份流程直接给你:CSV行情数据怎么整理、特征怎么构造、未来5日涨跌标签怎么打、sklearn模型怎么调参,以及5个最常见的坑。适合要交课程设计、想用sklearn验证量化思路的人。提前声明:文里所有结果仅作技术验证,不构成投资建议。
2. 数据准备:先把「能喂给sklearn的行情数据」这一关过了
2.1 数据文件长什么样:字段、频率与复权方式
做股票预测的第一步不是拉模型,而是把行情文件整理成sklearn能吃的表格。我一般让CSV尽量保持七列:date、open、high、low、close、volume,外加一个可选的code列用来区分不同股票。日期必须能被解析成datetime类型,价格必须是数值型,成交量单位无所谓,但全表要一致,否则后面rolling计算会静默出错。
| 字段 | 类型 | 用途 |
|---|---|---|
| date | datetime | 排序、切分训练集/测试集 |
| open | float | 开盘价,可选特征 |
| high | float | 最高价,可选特征 |
| low | float | 最低价,可选特征 |
| close | float | 收盘价,预测的核心标的 |
| volume | int/float | 成交量,量能特征依赖它 |
频率上,日线是最稳的选择。sklearn这类经典模型在分钟级数据上信噪比太低,特征容易被撮合噪声淹没;周线样本量又不够。建议至少取3到5年日线,800根以上K线,才够支撑滚动窗口特征和后续的交叉验证。另一个容易被忽略的点是复权:除权除息日当天价格会跳空,未复权数据会让移动平均和收益率特征出现一根大阴线的假信号,模型学到"除息日必跌"这种伪规律。我每次都先做前复权处理,再用处理后的价格算特征。
2.2 从CSV到干净的数据框:读文件、排序与缺失处理
拿到文件后不要急着造特征,先做三件事:解析日期、按时间排序、检查缺失。日线数据最容易出现的问题是停牌日缺行和上市初期NaN,这两个都要在造特征前处理掉。
import pandas as pd # 读入CSV,date列直接解析成datetime df = pd.read_csv("stock_daily.csv", parse_dates=["date"]) # 按时间升序排列,避免肉眼查不出来的乱序 df = df.sort_values("date").reset_index(drop=True) # 缺失值处理:原始行情列的NaN直接删,特征计算产生的NaN后面统一处理 df = df.dropna(subset=["open", "high", "low", "close", "volume"]) print(df.head()) print(df.tail())这段代码里dropna(subset=...)只处理原始价格和成交量的缺失,不会误删后面特征计算留下的NaN。sort_values一定要加,很多接口或手工拼接的数据日期是乱的,不排序的话pct_change和rolling算出来全是错的,而且这种错非常隐蔽。reset_index(drop=True)是为了让行号连续,后续按布尔mask对齐特征和标签时少踩一个坑。
2.3 训练集/验证集用时间切分,别用train_test_split
这是整个股票预测流程里最容易抄错的一步。train_test_split默认是随机洗牌的,适合比赛数据,但不适合时序数据——它可能拿2020年的样本做训练、2019年的样本做验证,模型实际上"偷看"了未来的价格形态再去预测过去。正确的做法是按日期硬切:训练集在前,验证集在后,像历史回放一样。
split_date = "2023-01-01" train_df = df[df["date"] < split_date].copy() test_df = df[df["date"] >= split_date].copy() print(f"train: {train_df['date'].min()} ~ {train_df['date'].max()}, {len(train_df)} rows") print(f"test : {test_df['date'].min()} ~ {test_df['date'].max()}, {len(test_df)} rows")我一般按7:3或8:2分配训练集和测试集,但边界日期要稍微往后留一点余量。因为后面构造滚动窗口特征时,训练集头部会损失大量样本,实际参与训练的样本比看起来少。split_date选在交易日最省心,date列是datetime类型,直接字符串比较也能被pandas正确解析。硬切分虽然简单,但它保证了训练集的每一根K线都早于验证集,这一条是后面所有模型评估的地基。
3. 特征工程与标签构造:预测未来5日涨跌,别把未来数据带进来
3.1 用价格和成交量构造技术指标特征
sklearn模型不会自己理解K线,需要把价格和成交量转成特征。我常用的技术指标特征分四类:动量、均线偏离、波动率、量能变化。动量看一段时间涨了多少,均线偏离看当前价格离一段时间成本有多远,波动率看风险在放大还是收缩,量能看资金是否配合。
| 特征 | 计算方式 | 含义 |
|---|---|---|
| ret_5 | close.pct_change(5) | 5日动量 |
| ret_10 | close.pct_change(10) | 10日动量 |
| close_to_ma_5 | close / ma5 - 1 | 价格偏离5日均线幅度 |
| close_to_ma_20 | close / ma20 - 1 | 价格偏离20日均线幅度 |
| vol_std_10 | close.rolling(10).std() | 10日波动率 |
| vol_ratio | volume / vol_ma20 | 量比,放量/缩量 |
把这些特征写成一个函数,窗口作为参数,方便后面做敏感性测试和对比实验:
def build_features(df, windows=(5, 10, 20)): df = df.copy() # 动量类特征 for w in windows: df[f"ret_{w}"] = df["close"].pct_change(w) # 均线偏离:ma列只是中间结果,特征用偏离度 for w in windows: df[f"ma_{w}"] = df["close"].rolling(w).mean() df[f"close_to_ma_{w}"] = df["close"] / df[f"ma_{w}"] - 1.0 # 波动率 for w in windows: df[f"vol_std_{w}"] = df["close"].rolling(w).std() # 量能 df["vol_ratio"] = df["volume"] / df["volume"].rolling(20).mean() return df df = build_features(df)这里没有直接放close原始值,因为原始价格非平稳且数值跨度大,逻辑回归对量纲敏感,树模型虽然不怕,但模型容易抓住"高价股、低价股"这种和方向无关的伪规律。所有特征都设计成比率或差值的形式,天然对价格尺度不敏感。窗口参数默认设为(5, 10, 20):如果预测周期是5日,最小窗口也要到5;如果把预测周期改成10日,最好把最小窗口同步改成10,否则特征频率和标签频率不匹配。
3.2 标签怎么打:未来N日收益率二分类
标签是预测目标,定义方式是"未来第N个交易日相对今天的涨跌方向"。我习惯用5日作为预测周期:用shift(-5)取5天后的收盘价,与当前收盘价比对,大于0标为1(涨),否则标为0(跌)。
HORIZON = 5 # 预测未来5个交易日 df["future_ret"] = df["close"].shift(-HORIZON) / df["close"] - 1.0 df["label"] = (df["future_ret"] > 0).astype(int) # 看标签分布,评估类别平衡度 print(df["label"].value_counts(normalize=True))shift(-5)表示把5天后的数据"往上挪"到当前行,这正是未来信息的来源。它只能用于构造标签,绝不能出现在特征矩阵里,否则就变成用未来答案答题。value_counts(normalize=True)能快速看涨跌比例,理想情况在0.45到0.55之间;如果样本正好赶上单边上涨行情,跌类样本会明显偏少,后面评估模型时要额外小心。
3.3 特征-标签对齐,以及两个数据泄漏自查点
特征和标签都构造好后,需要统一做一次对齐,把头部因滚动窗口产生的NaN和尾部因shift(-5)产生的NaN一起清掉。这一步漏掉,sklearn要么直接报错,要么默默丢弃样本,更隐蔽的情况是训练集和测试集样本量差很多,但你不一定能察觉到。
feature_cols = [ "ret_5", "ret_10", "ret_20", "close_to_ma_5", "close_to_ma_10", "close_to_ma_20", "vol_std_5", "vol_std_10", "vol_std_20", "vol_ratio", ] X = df[feature_cols] y = df["label"] # 对齐:特征和标签都非空的样本才保留 mask = X.notna().all(axis=1) & y.notna() X = X[mask] y = y[mask] print(X.shape, y.shape)对齐之后,我每次都会做两个泄漏自查。第一,确认feature_cols里没有任何一列的名字和label、future_ret、close、open、high、low、volume重合;第二,确认特征里没有类似"当天是否涨停和次日收益混合体"这种从未来倒推的变量。这种泄漏最典型的症状是训练F1高到0.9以上,测试集却和瞎猜差不多。遇到这种结果先回来查对齐,通常一查一个准。
4. 模型训练与参数调优:逻辑回归、随机森林与梯度提升怎么选
4.1 三个模型的适用边界与sklearn实现
选模型前先明确一点:日线涨跌预测本质上是个高噪声的二分类问题,信噪比很低,没有哪个模型能稳定拿到高准确率。sklearn里三个常用模型各有性格:逻辑回归是线性基线,适合判断特征有没有基本区分度;随机森林稳健、抗过拟合,对特征量纲不敏感,是默认首选;梯度提升在同样参数下往往能比随机森林再压一点偏差,但调参不当会过拟合得很难看。
用一个字典把三个模型组织起来,方便后面统一做交叉验证和网格搜索:
from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier models = { "logit": LogisticRegression(max_iter=1000, class_weight="balanced", random_state=42), "rf": RandomForestClassifier( n_estimators=500, max_depth=8, min_samples_leaf=20, n_jobs=-1, random_state=42 ), "gbdt": GradientBoostingClassifier( n_estimators=300, max_depth=3, learning_rate=0.05, subsample=0.8, random_state=42 ), }逻辑回归一定要给class_weight="balanced",因为股价数据里涨跌类往往不均衡,不给这个参数模型会倾向预测多数类。随机森林我先拉大n_estimators,把max_depth限制在8以内,min_samples_leaf设为20,这三个参数组合下来的泛化能力比默认参数稳定。梯度提升的learning_rate设为0.05,配合subsample=0.8让每棵树只看80%样本,能明显抑制时序数据上的过拟合。random_state全部固定,保证你自己复现和给别人复现时结果一致,这在评分场景里很重要。
4.2 用TimeSeriesSplit做交叉验证,避免时间穿越
调参时不能再用GridSearchCV默认的KFold,要换成TimeSeriesSplit。它的切分方式保证每一折的训练集都早于验证集,和数据准备阶段的按时间切分思路一致。sklearn里还提供gap参数,用来在训练折和验证折之间留出间隔,这步对股票数据非常关键:相邻交易日的特征高度相关,不留间隔会让验证分数虚高。
from sklearn.model_selection import TimeSeriesSplit, GridSearchCV, cross_val_score tscv = TimeSeriesSplit(n_splits=5, gap=5) rf = RandomForestClassifier( n_estimators=500, max_depth=8, min_samples_leaf=20, n_jobs=-1, random_state=42 ) # 先跑一次交叉验证,看每折的F1 scores = cross_val_score(rf, X, y, cv=tscv, scoring="f1") print("fold F1:", scores.round(3), "mean:", scores.mean().round(3))gap=5的含义是训练折和验证折之间丢掉5根K线,这5根K线不参与训练也不参与评估。n_splits=5表示把X按时间顺序切成5段,逐次用前1、2、3、4段训练,用后一段验证。注意TimeSeriesSplit要求X和y都按时间升序排列,前面排序的功夫在这里兑现价值。如果每折分数忽高忽低,别急,那是不同年份行情涨跌规律变化导致的,属于正常现象。如果你的sklearn版本较老、不支持gap参数,把gap去掉也能跑,只是验证分数会偏高一点。
4.3 参数怎么设:先粗后细,网格别一次拉满
调参顺序我习惯固定为先定结构参数,再调正则化参数,最后看要不要加减特征。max_depth和min_samples_leaf决定树的复杂度,这两个先定;n_estimators和learning_rate是产能参数,更多树配更低学习率通常更好,但受训练时间限制;class_weight和subsample这类正则化参数放在最后微调。推荐一套不太会翻车的初始范围:
- max_depth:5到10,股票特征少、特征相关性高,太深的树纯属记题
- min_samples_leaf:20到100,样本量不大,设太小时验证集分数波动剧烈
- n_estimators:随机森林300到500,GBDT 200到300
- learning_rate:0.03到0.1
网格搜索时建议每次只动1到2个参数。一次全部展开会导致组合数爆炸,还会在噪声数据上微调到奇怪的组合。看一个最小示例:
param_grid = { "max_depth": [6, 8], "min_samples_leaf": [30, 60], } grid = GridSearchCV( RandomForestClassifier(n_estimators=300, n_jobs=-1, random_state=42), param_grid=param_grid, cv=tscv, # 复用4.2的TimeSeriesSplit scoring="f1", n_jobs=-1, ) grid.fit(X, y) print("best params:", grid.best_params_) print("best F1:", grid.best_score_)scoring选f1而不是accuracy是故意的。涨跌不平衡时accuracy会被多数类带跑,看起来0.6的准确率,实际上对少数类一点判断力都没有。n_jobs=-1让网格搜索并行跑,特征矩阵只有几万行时很快就能出结果。拿到best_params_之后,我习惯用最优参数重新fit整个训练集,然后在测试集上只评估一次。反复看测试集调参,本质上就是把测试集变成了训练集。
5. 避坑排查:股票预测代码最常见的5个翻车现场
5.1 训练集0.98、测试集0.52:大概率是未来函数
现象:训练F1或准确率接近满分,模型像是"记住了"全部涨跌,一换到测试集就基本靠猜。这是股票预测代码里最常见也最容易被忽视的问题。
原因:特征里混进了未来信息。最常见的是特征构造时用了包含当天及未来数据的指标,比如shift(-1)构造的"次日动量";或者是把future_ret、label之外的泄露列留在了feature_cols里。pandas的shift负参数很多人用过一次之后把它当普通工具,忘了它是从未来取值。
解决:回到3.3的对齐步骤,逐个检查feature_cols的构造来源。可以写一段自动检查逻辑:对每一列特征,验证它第t行的值是否只用到第t行及之前的数据。查出问题列就删掉重跑。血泪经验是:一旦训练集分数高到不真实,优先怀疑数据泄漏,而不是先庆祝。
5.2 train_test_split随机洗牌:时间穿越
现象:测试集分数比按时间切分高好几个点,回测曲线也漂亮,一上实盘就露馅。
原因:训练集和测试集样本来自重叠时间区间,模型见过测试区间的"邻居样本",等于提前背了答案。股票数据的自相关性很强,今天和明天的特征几乎一样,随机打乱后训练集里混进测试期的邻近样本,模型自然表现好。
解决:统一改用按日期切分或TimeSeriesSplit。一个反直觉的点是,这种时间穿越在树模型上比线性模型更隐蔽——树模型能精准记住位置临近样本的特征组合,所以训练和验证分数看起来都合理,反而更能蒙人。
5.3 准确率虚高、PR曲线惨不忍睹:类别不平衡
现象:准确率0.62,看着不错,但F1只有0.3,预测出的"涨"全部集中在某几段单边行情上。
原因:多数时间市场是震荡或阴跌,跌类样本更多,模型学到的策略是"无脑预测跌",准确率也维持在0.5以上。准确率在类别不平衡时说明不了模型对目标类别的判别力,它只反映了多数类被覆盖了多少。
解决:给分类器加class_weight="balanced",评估时看F1、召回率和混淆矩阵。如果加了权重仍然偏向多数类,考虑对训练集做下采样,或者把预测周期从5日拉长到10日,提升标签本身的信噪比。
5.4 特征在除权日出现断崖:复权没做
现象:回测里某天模型给出极端预测,掐指一算正好是年报分红季。特征图在某个日期出现一根突兀尖峰,而价格本身没有真实波动。
原因:未复权数据在除权除息日跳空,pct_change和rolling特征把这次除权记成了暴跌或暴涨,模型学到"某类日期必跌"这种伪规律,实盘里对应日期根本不存在这样的价格行为。
解决:数据准备阶段先做前复权再算特征。如果是免费数据接口拉的,确认接口返回的是不是复权价;如果是行情软件导出的CSV,导出前选前复权选项。检查方法很简单:看close序列里有没有单日超过15%的跳变,排除真实涨跌停后还在的,基本就是除权缺口。
5.5 反复用同一份测试集调参:验证集被污染
现象:网格搜索跑了几十轮,每次都用测试集分数决定下一步往哪调,测试集分数越调越高,换个时间段重新切分就全线崩盘。
原因:调参过程本质上在做某种形式的优化,目标函数是测试集F1。你看了十次测试集,模型已经间接记住了测试集的形态,它不再是没见过的数据。
原因:把数据切成三份:训练、验证、测试。网格搜索和人工调参只看验证集分数,全部定稿后再碰测试集。如果数据量不够支撑三份切分,就用TimeSeriesSplit的交叉验证分数代替固定测试集,至少不会把同一段行情反复当答案看。
6. 让代码从「能跑」到「高分」:特征重要性与一个10行回测
6.1 用feature_importances删掉噪音特征
特征建了一堆,并不是每条都有用。随机森林训练完之后,我第一步永远是打印特征重要性,把排序垫底的列删掉再跑一遍。这个习惯对逻辑回归尤其有效——它比分多、每条都带点噪声,删掉无关特征后F1反而更稳。
import pandas as pd imp = pd.Series(rf.feature_importances_, index=feature_cols).sort_values(ascending=False) print(imp.head(10))6.2 用10行代码看策略是否跑赢买入持有
分数只是模型的体检报告,策略层还要看资金曲线。把预测结果转成仓位信号,用次日收益率计算策略净值,和买入持有对比,能直观看出模型预测的方向到底有没有被市场兑现。
df_valid = df.copy() df_valid["pred"] = float("nan") df_valid.loc[mask, "pred"] = rf.predict(X) # mask和X来自3.3的对齐结果 df_valid = df_valid.dropna(subset=["pred"]).copy() df_valid["ret_1"] = df_valid["close"].pct_change() df_valid["strategy_ret"] = df_valid["ret_1"] * df_valid["pred"].shift(1) df_valid["strategy_net"] = (1 + df_valid["strategy_ret"].fillna(0)).cumprod() df_valid["benchmark_net"] = (1 + df_valid["ret_1"].fillna(0)).cumprod() print(df_valid[["date", "pred", "strategy_ret", "strategy_net", "benchmark_net"]].tail())mask和X是3.3里做好的对齐结果,df_valid.loc[mask, "pred"]把预测值写回原表对应行,未被预测的行保持NaN,dropna直接丢弃。shift(1)是为了避免用当天预测信号买入当天涨幅的同步幻觉,实际交易里信号最早也要次日开盘执行。这份回测不扣手续费和滑点,真实环境需要在strategy_ret里额外减去双边费用。
我自己现在跑任何股票预测代码,都养成了一个习惯:先出三个数——训练F1、验证F1、回测年化,三个数都对得上才继续往下走,任何一个数异常就先回数据准备环节排查,而不是急着换模型。这个习惯帮我躲过好几次"测试集漂亮、实盘打脸"的尴尬,希望也能帮到你。
本文还有配套的精品资源,点击获取