ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用sklearn做股票预测:特征工程到回测全流程实战

用sklearn做股票预测:特征工程到回测全流程实战 简介面向毕业设计、期末大作业和课程设计场景的 sklearn 股票预测资源基于 Python 机器学习实现股价回归预测适合有一定基础、需要快速搭建完整项目的学生参考。压缩包共 14 个文件核心为 4 个 Python 脚本和 1 个 CSV 股价数据集另外包含 6 个 xml 工程配置、txt 依赖清单及 .gitignore/.iml 辅助文件整体大小 24.25MB目录结构清晰便于直接部署。代码完整覆盖随机森林回归、数据获取与模型结果对比等环节配有逐段注释新手也可以理解特征处理、训练评估与预测流程。已有 245 人浏览/学习属于手写 98 分且导师认可的高分项目下载后按依赖清单安装环境即可复现股票预测全过程适合作为答辩演示、课程报告或毕业设计的重要支撑。1. 用 sklearn 做股票预测先想清楚你要的是高分还是收益打开任何一个招聘网站或者课程论坛“python机器学习基于sklearn模型的股票预测代码所需数据高分代码”都是出现频率最高的资源标题之一。搜索这个标题的人通常分成两类一类是交机器学习课程作业的学生需要一份能跑通、图表齐全、答辩能讲清楚的项目另一类是刚接触量化交易的新手想看看 sklearn 这套通用工具到底能不能预测股价涨跌。先说结论这个方向能做、值得做但要用对评价标准。如果你拿准确率去要求它它大概率让你失望如果你拿“特征构造是否合理、数据划分是否有未来函数、评估指标是否完整”去要求它它是很好的实战项目也是机器学习标准流程的最佳练手场景之一。这篇文章的定位就是让“高分代码”这四个字落到实处。我会把整个 sklearn 股票预测拆成六步走从数据清洗到特征工程从模型训练到回测评估每一步都给出可以直接抄的代码块然后把参数含义和踩坑点写在代码后面。新手可以照着跑通熟手可以跳过基础部分看边界和坑。2. 把股价数据变成机器学习能吃的样本特征构造与标签生成2.1 数据源选型tushare、akshare 还是本地 CSV做股票预测第一步是拿到历史行情数据。常见做法是使用 tushare 或者 akshare 这类开源数据接口它们都不需要额外注册复杂的权限就能拿到日线级别的历史数据。但考虑到网络环境和接口稳定性问题我一般建议先把数据落地成 CSV后续所有实验都基于本地文件操作这样可复现性最强也不会因为接口限流导致实验中断。数据格式上我们最少需要五列date交易日期、open开盘价、high最高价、low最低价、close收盘价如果有 volume成交量更好。下面这一段代码演示了如何把接口数据转成标准化 CSV。import pandas as pd # 假设 df 是从数据接口拉下来的原始数据列名可能是中文或英文 df pd.read_csv(raw_stock_data.csv) # 统一列名只保留我们需要的字段 df.columns [date, open, high, low, close, volume] # 按日期排序防止原始数据乱序 df df.sort_values(date).reset_index(dropTrue) # 保存成标准格式 df.to_csv(stock_clean.csv, indexFalse) print(df.head())这段代码的逻辑非常直白第一是列名清洗因为不同的数据接口返回的字段名差异很大统一列名能避免后面特征工程阶段频繁报 KeyError第二是排序时间序列数据最忌讳乱序如果日期是乱的后面所有窗口计算都会错位。这里要注意的是reset_index(dropTrue)必不可少否则索引会带着原来的顺序切片时容易踩坑。2.2 特征窗口怎么定用过去 N 天预测未来 M 天股票预测的核心逻辑是把时间序列问题转化成监督学习问题。也就是说我们要构造出“特征 X 和标签 y”的对应关系用过去 N 天的数据作为特征预测未来 M 天的涨跌方向。这个 N 和 M 怎么定是整个项目第一个真正需要动脑的地方。N 太小比如只用 1 天特征信息量太少模型学不到趋势N 太大比如用 60 天特征维度膨胀在数据量不大的时候反而容易过拟合。我做这个项目时的默认配置是 N10也就是用过去 10 个交易日的行情特征来预测未来 5 个交易日是涨还是跌。10 个交易日正好是两周在日线数据上既能捕捉短期动量又不会引入太多噪声。标签的生成方式同样关键。很多人直接用“明天收盘价比今天高”作为正样本这种做法太敏感一天的波动噪声太大模型学出来的东西不稳定。更好的做法是计算未来 M 天的收益率然后设置一个阈值超过阈值才算涨。import numpy as np df pd.read_csv(stock_clean.csv) # 计算未来 5 个交易日的收益率 df[future_return] df[close].shift(-5) / df[close] - 1 # 标签未来 5 日收益率大于 2% 记为 1涨否则记为 0跌 df[label] (df[future_return] 0.02).astype(int) # 删除无法计算未来收益率的最后 5 行 df df.dropna().reset_index(dropTrue) print(df[[date, close, future_return, label]].tail(10))这里有个细节需要特别说明shift(-5)的含义是把 5 天后的收盘价拉到今天这一行然后用它除以今天的收盘价再减 1得到的就是“从今天起持有 5 天的收益率”。阈值 0.02 不是拍脑袋定的它相当于 5 天涨 2%换算成年化收益大约在 100% 以上属于比较强的上涨信号。如果你做的是短线交易可以把阈值降下来如果你做的是中线可以把阈值提高。2.3 技术指标特征MA、RSI、MACD 的 sklearn 化实现有了标签还不够我们还必须给模型喂特征。原始价格序列本身不适合直接做特征因为股价的绝对值受股本、送转等因素影响不同股票之间的价格没有可比性。业界通用的做法是把原始价格转换成技术指标。我常用的特征组合是三个移动平均线 MA用于刻画趋势相对强弱指标 RSI用于刻画超买超卖以及收益率本身用于刻画短期动量。这三个指标覆盖了趋势、震荡、动量三个维度对 sklearn 的树模型和线性模型都比较友好。# 特征工程基于 stock_clean.csv 构造技术指标 df pd.read_csv(stock_clean.csv) # 1. 简单移动平均线 MA5 和 MA10 df[ma5] df[close].rolling(window5).mean() df[ma10] df[close].rolling(window10).mean() # 2. 价格相对于均线的偏离度比直接用价格更稳定 df[close_ma5_ratio] df[close] / df[ma5] - 1 # 3. RSI 指标默认 14 天 delta df[close].diff() gain delta.clip(lower0) loss -delta.clip(upper0) avg_gain gain.rolling(window14).mean() avg_loss loss.rolling(window14).mean() df[rsi] 100 - (100 / (1 avg_gain / avg_loss)) # 4. 当日收益率 df[daily_return] df[close].pct_change() # 丢弃 NaN 行 df df.replace([np.inf, -np.inf], np.nan).dropna().reset_index(dropTrue) feature_cols [close_ma5_ratio, rsi, daily_return] print(df[feature_cols].describe())这套特征构造有几个易错点。第一rolling(window5).mean()计算的是前 5 天的均值是严格用历史数据算出来的不存在未来函数第二pct_change()默认计算的是当天相对前一天的涨跌幅第一行结果是 NaN 需要丢弃第三RSI 计算过程中可能出现除以零的情况——当 avg_loss 为 0 时说明 14 天内全是上涨此时 RSI 应该等于 100但公式里会出现无穷大所以用replace([np.inf, -np.inf], np.nan)先处理掉再统一 dropna 是标准做法。3. 训练集与测试集划分时间序列的切法和普通分类完全不同3.1 为什么不建议用 train_test_split 随机切分很多初学者拿到特征和标签后第一反应是调用train_test_split(X, y, test_size0.2, random_state42)来划分数据集。这在普通分类问题上没问题但用在股票预测上就是严重的方法论错误更会让“高分”两个字离你而去。原因很简单股票数据是时间序列相邻两天的样本之间存在极强的相关性。如果用随机切分训练集里会混入测试集时间段的样本模型相当于提前“看过未来”测试集上的准确率会虚高。等到你拿着这个模型去预测未来的真实数据时效果会断崖式下跌——这就是典型的“数据泄露”。正确的做法是按时间顺序切分前 70% 的时间段作为训练集后 30% 的时间段作为测试集。sklearn 提供了TimeSeriesSplit专门解决这个问题但我建议第一步先手动切分逻辑更透明也方便后文做可视化对比。3.2 按时间切分的完整代码与验证逻辑from sklearn.model_selection import train_test_split # 读取特征和标签 df pd.read_csv(stock_features.csv) X df[feature_cols].values y df[label].values # 手动按时间顺序切分前 70% 训练后 30% 测试 split_idx int(len(df) * 0.7) X_train, X_test X[:split_idx], X[split_idx:] y_train, y_test y[:split_idx], y[split_idx:] print(f训练集样本数: {len(X_train)}, 测试集样本数: {len(X_test)}) print(f训练集时间范围: {df[date].iloc[0]} ~ {df[date].iloc[split_idx - 1]}) print(f测试集时间范围: {df[date].iloc[split_idx]} ~ {df[date].iloc[-1]})这里有一个重要提醒int(len(df) * 0.7)得到的是样本条数的 70%但由于我们的特征是 10 天窗口滚动构造的实际上这个切分点对应的“信息截止日期”并不是第 70% 天而是第 70% 天再往前推 10 天。严格来说训练集最后一个样本用到了第split_idx - 1天的数据测试集第一个样本用的信息截止到第split_idx 9天中间有 9 天的信息重叠。不过因为我们是站在第split_idx天做决策预测的是未来 5 天所以这个重叠在日线策略的可接受范围内。3.3 特征缩放StandardScaler 的 fit 和 transform 不能混用sklearn 里很多模型对特征的尺度敏感尤其是 SVM 和逻辑回归如果特征不在同一量纲下模型会把绝对值大的特征当成更重要。RSI 的取值范围是 0 到 100收益率的取值范围只有 0.01 到 0.001 量级如果不做标准化RSI 会主导整个模型的决策。标准化代码很简短但有一个坑必须讲清楚StandardScaler的fit方法只能用在训练集上然后用训练集的均值和标准差去transform测试集绝对不能用全量数据去fit否则又造成了信息泄露。from sklearn.preprocessing import StandardScaler scaler StandardScaler() # 关键只在训练集上 fit X_train_scaled scaler.fit_transform(X_train) # 测试集用训练集的参数做 transform X_test_scaled scaler.transform(X_test) print(f训练集均值: {scaler.mean_}) print(f训练集标准差: {scaler.scale_})为什么必须这样因为测试集模拟的是“未来未知数据”。在真实场景里你在训练模型的那一刻是拿不到未来数据的均值和标准差的。如果你用全量数据计算均值和标准差相当于让测试集的信息渗透到了训练阶段测试集上的表现就会偏乐观。4. 建模与训练逻辑回归、随机森林、SVM 三模型对比4.1 逻辑回归先跑通基线别急着上复杂模型股票预测项目的第一个模型我强烈建议用逻辑回归。不是因为它的预测能力最强而是因为它训练快、可解释性强、不容易翻车适合作为基线。如果逻辑回归的效果就不错说明特征构造得好如果效果很差换更复杂的模型大概率也救不回来。from sklearn.linear_model import LogisticRegression from sklearn.metrics import accuracy_score, classification_report # 逻辑回归模型设置最大迭代次数避免收敛警告 model_lr LogisticRegression(max_iter1000, random_state42) model_lr.fit(X_train_scaled, y_train) # 预测 y_pred_lr model_lr.predict(X_test_scaled) print(逻辑回归准确率:, accuracy_score(y_test, y_pred_lr)) print(classification_report(y_test, y_pred_lr))参数说明max_iter1000是因为 sklearn 的逻辑回归默认用 L-BFGS 优化器当特征维度较高或者数据量较大时默认的 100 次迭代可能不收敛在命令行里表现为 ConvergenceWarning解决方案就是调大迭代次数。random_state42是固定随机种子保证每次运行结果一样——很多人在这个项目上遇到“每次跑出来的结果不同”的问题根源就是没有固定随机种子。这个坑在第五章还会细讲。4.2 随机森林用特征重要性理解模型看到了什么跑通逻辑回归之后第二个推荐模型是随机森林。随机森林的优势在于它能自动处理特征之间的非线性关系而且不需要特征标准化直接喂原始特征也行。此外随机森林自带特征重要性评估能告诉我们模型最依赖哪些特征。from sklearn.ensemble import RandomForestClassifier # 随机森林模型 model_rf RandomForestClassifier( n_estimators200, max_depth6, min_samples_leaf10, random_state42, n_jobs-1 ) model_rf.fit(X_train, y_train) # 随机森林不需要标准化 y_pred_rf model_rf.predict(X_test) print(随机森林准确率:, accuracy_score(y_test, y_pred_rf)) print(特征重要性:, model_rf.feature_importances_)这里的三个核心参数值得展开说。n_estimators200是树的数量太少容易欠拟合太多训练时间线性增加而收益递减200 是一个平衡点。max_depth6限制每棵树的深度防止单棵树过拟合股票数据的信号本来就弱让树长得太深很容易记住噪声。min_samples_leaf10要求每个叶子节点至少 10 个样本进一步强制模型学到“群体规律”而不是“个体特例”。训练结束后一定要看一眼feature_importances_这能帮你验证特征工程做得对不对。如果rsi特征的重要性接近 0说明这个特征在当前数据上没有区分度下次迭代可以考虑替换成别的指标。4.3 SVM需要标准化且对核函数敏感SVM 是第三个候选模型。它在小样本高维场景下表现好但有一个明显的性格特征对特征缩放极其敏感而且核函数的选择直接决定模型的边界形态。在股票预测这个场景里我一般用 RBF 核因为它能拟合非线性边界而线性核在特征简单时表现尚可、在特征复杂时就不够用了。from sklearn.svm import SVC # SVM 模型使用 RBF 核 model_svm SVC(kernelrbf, C1.0, gammascale, random_state42) model_svm.fit(X_train_scaled, y_train) y_pred_svm model_svm.predict(X_test_scaled) print(SVM 准确率:, accuracy_score(y_test, y_pred_svm))C1.0是正则化参数控制误分类的惩罚力度C 太小模型容易欠拟合C 太大模型容易过拟合1.0 是经验默认值。gammascale表示 RBF 核的宽度根据特征数量自动缩放这个设置比手动指定数值更稳妥——手动指定 gamma 是 SVM 调参里最容易翻车的操作设大了模型变成一个个孤立点包围圈设小了整个决策边界近乎线性。4.4 三模型对比别只看准确率要看精确率和召回率的取舍把三个模型跑完后很多人习惯用准确率一锤定音。但在股票预测里准确率是一个会骗人的指标。假设涨跌样本比例是 6:4一个“永远预测涨”的傻瓜模型准确率就有 60%而你的模型准确率只有 58%难道说明模型比傻瓜还差吗显然不是。正确的对比方式是同时看精确率Precision和召回率Recall。精确率回答的是“模型预测涨的样本里真正涨了多少”召回率回答的是“所有真正涨的样本里模型抓到了多少”。这两个指标存在天然矛盾我们做股票预测时更需要精确率——因为预测错了要亏钱宁可错过不能做错。from sklearn.metrics import precision_score, recall_score, f1_score models { Logistic Regression: (y_pred_lr, LR), Random Forest: (y_pred_rf, RF), SVM: (y_pred_svm, SVM) } for name, (y_pred, tag) in models.items(): precision precision_score(y_test, y_pred) recall recall_score(y_test, y_pred) f1 f1_score(y_test, y_pred) print(f{name}: Precision{precision:.4f}, Recall{recall:.4f}, F1{f1:.4f})如果跑出来的结果显示随机森林的精确率最高那最终模型就选随机森林然后用它去做回测。如果逻辑回归的精确率和随机森林差不多那优先选逻辑回归——模型更简单、部署成本更低、上线后出了问题也更好排查。这符合“先选简单模型再考虑复杂模型”的工程原则。5. 常见问题排查与避坑五个让项目翻车的隐藏雷区5.1 安装 sklearn 报错“sklearn” 包已废弃现象执行pip install sklearn后import 时提示警告信息甚至在某些新版本环境下直接报错。原因PyPI 上名为sklearn的包已经被标记为废弃正确的包名是scikit-learn。很多教程还在用旧写法导致新用户装错了包。解决卸载掉旧包重新安装正确包。pip uninstall sklearn -y pip install scikit-learn安装完成后在 Python 里验证一下版本import sklearn print(sklearn.__version__)如果能看到 1.0 以上的版本号说明环境没问题。这里多提醒一句在 conda 环境里优先用conda install scikit-learn因为 conda 会自动处理与 numpy、scipy 的依赖版本兼容问题pip 在某些情况下会强行升级 numpy 导致其他库崩溃。5.2 每次运行结果都不一样随机种子没固定现象同一个模型、同一份数据连续跑三次准确率和预测结果每次都不同。原因sklearn 里的很多模型有随机性比如随机森林的样本抽样、SVM 的迭代初值、逻辑回归的求解器初始化。如果不固定随机种子模型每次训练都会从一个不同的起点出发。解决在所有涉及随机过程的模型里设置random_state42。42 只是习惯换成任何整数都行关键是固定。另外train_test_split里的random_state也要固定否则连训练集和测试集的划分都会变。这里有更隐蔽的一层如果你用了numpy的随机函数或者pandas的sample方法也需要设置np.random.seed(42)否则数据预处理阶段的随机性还是会导致结果飘。5.3 测试集准确率很高实盘一用就亏数据泄露现象回测报告很漂亮准确率 85%但拿到真实行情上一测预测结果和抛硬币差不多。原因这是最典型的“未来函数”问题。可能在特征构造时用了未来数据比如用第 T1 天的收盘价来算第 T 天的特征也可能在数据标准化时用全量数据 fit或者数据切分时用了train_test_split随机切分导致测试集信息混入训练集。解决从头检查三个环节。第一特征列里有没有用到shift负数的情况——shift(-1)本身没有错关键是看它构造的特征是否在“决策时刻”已经可知。第二标准化只允许在训练集上 fit。第三切分必须按时间顺序用TimeSeriesSplit或手动切片严禁随机切分。from sklearn.model_selection import TimeSeriesSplit tscv TimeSeriesSplit(n_splits5) for train_index, test_index in tscv.split(X): X_train_fold, X_test_fold X[train_index], X[test_index] # 每折都是按时间顺序划分TimeSeriesSplit的价值在于它生成了多组时间顺序的切分可以在多段历史上做交叉验证比单一切分更稳健。但它也有缺点每一折的训练集都比前一折大测试集永远在训练集之后模型评估的计算量会成倍增加。5.4 涨跌样本严重不平衡模型全部预测“跌”现象训练完成后打印分类报告发现精确率很高召回率却为 0看预测结果发现模型把全部样本都预测成了多数类。原因股票市场的上涨天数通常少于下跌天数特别是加了 2% 的上涨阈值后正负样本比可能达到 1:3 甚至更低。sklearn 的模型默认追求整体准确率在样本不平衡时把所有样本预测成多数类反而能获得最高准确率。解决在模型里设置class_weightbalanced让模型自动对少数类样本赋予更高的权重。model_rf_balanced RandomForestClassifier( n_estimators200, max_depth6, min_samples_leaf10, class_weightbalanced, random_state42 )设置之后模型会牺牲一部分整体准确率换取对少数类更高的召回率这是股票预测场景下正确的事。如果你用了classification_report观察结果会发现模型开始能捕捉到一部分上涨样本了。另一个进阶选项是用imblearn库做 SMOTE 过采样但那属于锦上添花的操作先把class_weight用好更务实。5.5 特征全部是同一只股票的衍生指标模型过拟合现象训练集上准确率极高测试集上一落千丈两极分化严重。原因股票数据本身信噪比极低如果只用一只股票的数据做训练模型很容易把这只股票历史中的偶然模式当成了普遍规律。这就是过拟合的典型表现。解决至少用 3 到 5 只不同行业、不同周期的股票数据混合训练。比如同时用银行股、消费股和科技股的数据拼接成一个数据集模型的泛化能力会明显提升。拼接时注意不同股票的价格水平不同必须把特征统一到技术指标形态上而不是直接用原始价格。6. 进阶技巧滚动训练与回测曲线让预测落地到交易决策6.1 滚动训练模型要不断适应新行情到这一步你已经有了一个经过验证的模型但还有一个现实问题市场风格会变模型会过时。上一年适合的模型参数下一年可能就不灵了。解决办法是“滚动训练”每隔一段时间用最近的数据重新训练模型。retrain_period 20 # 每 20 个交易日重新训练 window_size 200 # 每次训练只用最近 200 天数据 for i in range(window_size, len(X_test_scaled), retrain_period): # 取最近 window_size 条数据训练 train_data X_test_scaled[i - window_size:i] train_label y_test[i - window_size:i] model_rf.fit(train_data, train_label) # 对接下来 retrain_period 天的数据做预测 start i end min(i retrain_period, len(X_test_scaled)) pred model_rf.predict(X_test_scaled[start:end]) # 记录预测结果后续用于回测window_size200约等于 10 个月交易日数量这个窗口兼顾了样本量和新近性。值得注意的是这里用的是“扩展窗口”逻辑你也可以用固定窗口区别在于扩展窗口用了所有历史数据训练适合数据量小时固定窗口只保留最近一段时间的数据更适合数据量充足且市场风格切换较快的情况。6.2 回测曲线的绘制评估预测结果到底能不能赚钱评估模型价值的唯一方法是模拟真实的交易过程。简单回测逻辑是模型预测明天上涨就买入预测下跌就空仓。收益曲线就是每天按照策略操作后的账户净值变化。import matplotlib.pyplot as plt # 使用测试集的真实收益和模型预测结果做回测 test_return df[daily_return].values[split_idx:] capital 1.0 # 初始资金 strategy_nav [capital] # 记录策略净值 buy_hold_nav [capital] # 记录买入持有净值 for i in range(len(y_pred_rf)): # 策略预测为 1涨则持有并赚取当日收益预测为 0 则空仓 if y_pred_rf[i] 1: capital * (1 test_return[i]) strategy_nav.append(capital) # 买入持有策略每天都不动 buy_capital 1.0 for r in test_return: buy_capital * (1 r) buy_hold_nav.append(buy_capital) plt.figure(figsize(10, 5)) plt.plot(strategy_nav, labelStrategy) plt.plot(buy_hold_nav, labelBuy Hold) plt.legend() plt.title(Backtest Comparison) plt.show()这份代码的逻辑是标准的策略净值计算策略净值按照模型预测决定持仓或空仓买入持有策略净值则一直随市场波动。两条曲线的差值就是模型创造的超额收益。注意真实回测还要考虑交易手续费和涨跌停无法交易的因素但作为课程设计级项目这份回测足以证明模型的预测在统计上有区分度。6.3 概率输出与阈值调优用 predict_proba 代替 predict最后一个技巧比所有参数调优都管用不要用predict做买卖决策而是用predict_proba拿模型输出概率再根据概率设定自定义阈值。# 获取预测概率 y_prob_rf model_rf.predict_proba(X_test_scaled)[:, 1] # 默认阈值是 0.5但我们可以调高到 0.6 提高精确率 custom_threshold 0.6 y_pred_custom (y_prob_rf custom_threshold).astype(int) print(默认阈值精确率:, precision_score(y_test, y_pred_rf)) print(自定义阈值精确率:, precision_score(y_test, y_pred_custom))阈值调优的逻辑是如果你希望策略更保守就提高阈值模型只有在胜率较高时才给出买入信号如果你希望策略更激进就降低阈值不错过任何可能上涨的机会。阈值怎么选取决于你对风险的承受能力这是一个可以写进答辩报告里的亮点。我在做这个项目时最常见的一个习惯是把概率打印出来看看模型在 0.4 到 0.6 之间大量犹豫的样本长什么样再倒推特征是哪里出了分歧这比盲目调参有用得多。希望这些经验对你有实际帮助。本文还有配套的精品资源点击获取
返回列表