简介:面向金融数据分析与机器学习初学者的完整股票预测项目,基于sklearn中的随机森林回归模型构建,提供数据获取、特征处理和模型训练等一整套实现。资源共包含15个文件,以4个Python脚本为核心,涵盖数据抓取、训练和预测等环节;附带1个CSV历史股价数据集、1个说明文档及多个项目配置文件,压缩包约24.25MB,目录结构清晰,便于直接运行和二次开发。当前已有287人学习下载,适合作为计算机、人工智能、金融商贸等相关专业的毕设、课设或实战练习。代码已经过测试运行,能完成股票价格预测的基本流程,并且在原有模型基础上保留了调参和对比的入口,可进一步扩展更多技术指标或其他机器学习算法。项目从数据清洗到模型评估层次分明,对理解sklearn建模流程和金融数据挖掘具有实际参考价值。
1. 为什么拿 sklearn 做股票预测要先想清楚这三个问题
拿 sklearn 做机器学习股票预测,是很多人入门机器学习的第一个完整项目:拉一段股票历史行情,算几个技术指标,丢进随机森林,输出“明天涨还是跌”。听上去顺理成章,可实际跑一轮就会发现,模型预测股票涨跌每次结果不一样,第一次准确率 72%,第二次可能是 68%,回测曲线很漂亮,换一段行情就翻车。这里不聊玄学,也不劝你用机器学习直接炒股,而是把“基于 sklearn 模型的机器学习股票预测”拆成一条能复现的路线:预测目标怎么定、特征怎么造、模型怎么选、结果怎么验证。适合刚学完机器学习基础、想用 python 完成完整项目的人,也适合做课程设计或者入门金融量化的同学。读完你至少能判断一件事:这个方向值不值得继续投入。
2. 预测目标与数据准备:先决定预测什么,再决定怎么写代码
预测目标决定后面所有代码的走向。很多人上来就把任务定义成“预测明天收盘价”,然后拿 sklearn 的回归模型硬拟合,结果测试集误差一塌糊涂,原因不是模型不行,而是目标选错了:股价是非平稳序列,直接回归价格等于让模型预测一个带趋势和噪声的随机游走。常见做法是把问题简化成分类:未来 N 个交易日收益率是否为正。这样模型只判断方向,不纠结精确数值,评估也更贴近交易里“涨跌”这个真实诉求。
N 的取值同样重要。用 1 个交易日做标签,噪声太大,模型学到的是盘中随机波动;用 20 个交易日,标签平滑但样本重叠严重,回测失真。我一般先用 5 个交易日,约一周持有期,兼顾信号强度和样本独立性。确定好“预测未来 5 日是否上涨”,再去准备数据,代码会清晰很多。
2.1 先确定预测对象:分类涨跌比回归价格更稳
| 预测对象 | 标签定义 | 适合模型 | 主要问题 |
|---|---|---|---|
| 回归预测收盘价 | 未来价格本身 | LinearRegression, SVR | 非平稳、噪声大,评估指标难解释 |
| 回归预测收益率 | 未来收益率数值 | LinearRegression, GBR | 仍难直接转化为交易信号 |
| 分类预测涨跌 | 未来 N 日收益是否大于 0 | LogisticRegression, RandomForest | 需要处理类别不平衡 |
| 分类预测幅度 | 涨跌幅是否超过阈值 | 同上 | 样本更稀疏,过拟合风险高 |
先跑通“分类预测涨跌”,等流程稳定了再尝试“分类预测幅度”。幅度预测虽然信息量更大,但阈值怎么定、样本够不够、类别怎么平衡,都是额外难题。初次做股票预测,分类方向是最稳的第一步。
既然目标定了,下一步就是规范数据。股票数据来源不稳定,网上随便下载的 CSV 列名可能都不一样。为了不把时间花在清洗上,我会先定一个统一标准:date, open, high, low, close, volume,日期升序,没有空值。凡是外部数据,一律先转换成这个格式再进入特征工程。
2.2 安装与数据读取:用 scikit-learn,不是 sklearn
先解决环境问题。PyPI 上的sklearn包已经弃用,正确安装名是scikit-learn,但导入代码仍然写import sklearn。很多初学者执行pip install sklearn之后能导入,却装到了老版本或残留包,后面调模型时各种报错。建议在干净环境里直接装scikit-learn。
pip install scikit-learn pandas numpyimport pandas as pd import numpy as np df = pd.read_csv("history.csv", parse_dates=["date"]) df = df.sort_values("date").reset_index(drop=True) assert df["close"].notna().all(), "close 列存在空值" print(df.head())逻辑说明:先按日期排序并重置索引,保证后续rolling和shift都基于正确的时间顺序。CSV 里的索引如果带跳跃,滚动窗口会算错。上传数据前先检查close列是否完整,我遇到过某个数据源后 20 行全是 NaN,特征算完一批 NaN,模型直接报错。
参数说明:parse_dates=["date"]把日期列转成时间类型,方便后续按时间切分;reset_index(drop=True)是必须做的,否则原索引不连续,iloc切分时容易踩坑。不要相信任何来源的数据一定干净,打印 head 只是第一道防线,还要看缺失值、重复日期和停牌日。
2.3 特征和标签:滑窗特征一步到位,注意 shift 方向
股票预测特征不追求多,追求“到时间点为止只用了历史信息”。技术指标是入门首选,因为它们本身就是由历史价格算出来的。下面这段代码生成三类特征:滑窗收益率、滑窗均线、滑窗波动率,再加一个简化版 RSI。
def add_features(df: pd.DataFrame) -> pd.DataFrame: df = df.sort_values("date").reset_index(drop=True) close = df["close"] for n in [5, 10, 20]: df[f"ret_{n}"] = close.pct_change(n) df[f"ma_{n}"] = close.rolling(n).mean() df[f"std_{n}"] = close.rolling(n).std() delta = close.diff() gain = delta.clip(lower=0).rolling(14).mean() loss = (-delta.clip(upper=0)).rolling(14).mean() rs = gain / (loss + 1e-9) df["rsi_14"] = 100 - 100 / (1 + rs) return df.dropna()逻辑说明:pct_change(n)计算的是当根 K 线相对 n 根 K 线前的收益率,rolling(n).mean()是截至当前时刻的移动平均,这两类特征都没有用到未来数据。RSI 用 14 日涨跌均值比,公式是经典版本,1e-9防止除零。代码在每根 K 线收盘后都能完整算出,不会把未来信息带进当前样本。
参数说明:窗口 5、10、20 是最常见的短中期组合,覆盖一周、两周、一个月的周期。std_n是波动率,用来捕捉风险变化;RSI 用来捕捉超买超卖。窗口不是越多越好,特征多了以后随机森林的max_features="sqrt"反而会稀释有效信号,初次先用三组窗口足够。
标签生成同样要用 shift,但方向完全不同。
def make_label(df: pd.DataFrame, horizon: int = 5) -> pd.DataFrame: df = df.copy() df["future_ret"] = df["close"].shift(-horizon) / df["close"] - 1 df["label"] = (df["future_ret"] > 0).astype(int) return df.dropna(subset=["label"])逻辑说明:shift(-horizon)表示未来第 5 天的收盘价相对今天的涨跌幅,正数标为 1,负数标为 0。这里有一个关键点:特征close用的是今天收盘价,标签用的也是今天到未来的收益,所以最后一根 K 线无法生成完整标签,dropna会把它们剔除。如果你把 shift 方向写反成shift(horizon),标签就变成了“过去 5 天是否上涨”,模型学到的全是历史,回测必崩。
2.4 时间序列切分:不要把训练标签伸进验证期
训练集和测试集不能随机打乱,必须按时间切分。但“按时间切分”也有隐蔽陷阱:标签是未来 5 日收益,如果训练集切在 2023 年最后一个交易日,训练集最后 4 个样本的标签已经伸进 2024 年的验证区间,模型等于提前见过了验证期的行情。
train_end = "2023-12-31" horizon = 5 train_idx = df.index[df["date"] <= train_end] train_cut = train_idx.max() + 1 # 训练集丢弃末尾 horizon 个样本,避免标签伸进验证期 train_ok = train_cut - horizon X_train = df.iloc[:train_ok].drop(columns=["label"])[feature_cols] y_train = df.iloc[:train_ok]["label"] # 测试集从 train_cut 开始,保留完整标签 X_test = df.iloc[train_cut:].drop(columns=["label"])[feature_cols] y_test = df.iloc[train_cut:]["label"] X_test = X_test.iloc[:-horizon] y_test = y_test.iloc[:-horizon]逻辑说明:train_ok = train_cut - horizon是这次切分的核心。第train_cut - 1个样本的标签用到train_cut + 3的数据,已经越过切分点;只有把训练集末尾horizon个样本丢掉,才能保证训练标签全部落在训练区间内。测试集末尾同样需要去掉horizon个样本,因为最后几条数据的标签不完整,保留它们会造成评估结果虚高或偏低。
这里也是“模型预测股票涨跌每次结果不一样”的一个隐藏来源。如果你每次重新运行脚本时数据排序不同、切分位置不同,结果自然不同。固定好数据顺序和切分边界,后面调参才可复现。
3. 模型选型与调参:从逻辑回归到随机森林的一步步替换
模型选型有一条朴素原则:先用简单的,再换复杂的。逻辑回归是股票预测最好的基线:训练快、可解释、不容易过拟合。如果逻辑回归在 AUC 上已经达到 0.55,随机森林可能只高一点点,但复杂度高很多;如果逻辑回归 AUC 只有 0.5,随机森林大概率也不会突然变神。下面从逻辑回归开始,一步步落到参数细节。
3.1 逻辑回归基线:特征标准化和 class_weight 是重点
股票数据里特征尺度差异很大,close是几十到几百元,rsi_14是 0 到 100,ret_5是百分比小数。逻辑回归对尺度敏感,必须做标准化。我习惯把标准化和模型放进同一个 Pipeline,这样交叉验证时每个 fold 都只会用训练集的均值和方差去变换验证集,不会造成数据泄漏。
from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LogisticRegression from sklearn.metrics import roc_auc_score model_lr = Pipeline(steps=[ ("scaler", StandardScaler()), ("clf", LogisticRegression(max_iter=1000, class_weight="balanced", random_state=42)) ]) model_lr.fit(X_train, y_train) prob_lr = model_lr.predict_proba(X_test)[:, 1] print("logit auc:", roc_auc_score(y_test, prob_lr))逻辑说明:Pipeline 把fit和predict串成一条流水线,训练时先对 X 拟合标准化器,再用标准化后的数据训练模型;测试时只用已经拟合好的标准化器做变换。如果你先在全量数据上算标准化,再切训练测试,验证集的信息已经参与了特征缩放,这属于一种常见泄漏。
参数说明:max_iter=1000是因为股票特征经过标准化后仍可能迭代慢,默认 100 不够时 sklearn 会报警;class_weight="balanced"让少数类获得更高权重,股票数据里涨跌样本往往不平衡,尤其震荡行情中“跌”的样本少,不加这个参数模型会倾向预测多数类。random_state=42是给逻辑回归内部随机数固定的,虽然线性模型随机性小,但保持可复现是好习惯。
3.2 决策树和随机森林:换模型的判断标准
逻辑回归的假设是特征与涨跌存在线性关系,但技术指标对股价的作用往往不是线性的,比如 RSI 低于 30 可能反弹、高于 70 可能回落,这种阈值效应树模型更擅长。决策树能自动做特征组合和非线性划分,但单棵树对噪声敏感,训练集 R2 可能很高,测试集直接崩。随机森林通过多棵树平均来压低方差,是目前 sklearn 股票预测里最常用的升级模型。
from sklearn.ensemble import RandomForestClassifier model_rf = RandomForestClassifier( n_estimators=300, max_depth=6, min_samples_leaf=20, max_features="sqrt", class_weight="balanced", oob_score=True, n_jobs=-1, random_state=42 ) model_rf.fit(X_train, y_train) prob_rf = model_rf.predict_proba(X_test)[:, 1] print("rf auc:", roc_auc_score(y_test, prob_rf)) print("oob auc:", model_rf.oob_score_)逻辑说明:随机森林不需要特征标准化,所以这里没有 Pipeline。fit之后predict_proba返回两个列,第二列是预测为“上涨”的概率,评估和回测都应该用概率而不是二分类结果。oob_score_是袋外样本的准确率,可以粗看模型是否稳定,但它不是最终评判标准。
参数说明:n_estimators=300已经够用,继续加到 1000 收益很小但训练变慢;max_depth=6是防止树长得太深,股票数据噪声大,深度超过 8 很容易记住训练集;min_samples_leaf=20强制每个叶子节点至少 20 个样本,让预测更平滑;max_features="sqrt"是随机森林的标准配置,减少树间相关性。这些参数不是拍脑袋,是多次对比 AUC 后得到的一个不差钱的起点。
3.3 用 TimeSeriesSplit 网格搜索:别让 KFold 打乱时序
调参时很多人直接GridSearchCV(cv=5),默认的 KFold 会把数据随机打乱再分成 5 份,训练集里可能出现 2020 年的数据预测 2018 年的验证集,这叫时序泄漏。股票预测必须用TimeSeriesSplit,它按顺序切分,每一折的训练集始终在验证集之前。
from sklearn.model_selection import TimeSeriesSplit, GridSearchCV param_grid = { "max_depth": [4, 6, 8], "min_samples_leaf": [10, 20, 40], "max_features": ["sqrt", 0.5] } tscv = TimeSeriesSplit(n_splits=5) grid = GridSearchCV( RandomForestClassifier( n_estimators=150, class_weight="balanced", random_state=42, n_jobs=-1 ), param_grid, cv=tscv, scoring="roc_auc", n_jobs=-1 ) grid.fit(X_train, y_train) print(grid.best_params_) print(grid.best_score_)逻辑说明:TimeSeriesSplit的每一折都会用更长的历史做训练,验证集始终紧跟在训练集后面。GridSearchCV 会把 5 折的 AUC 平均作为评分,best_score_是对未来表现的悲观估计,因为股票预测跨期越长,市场环境变化越大,分数会比你期望的低一些。
参数说明:scoring="roc_auc"比默认 accuracy 更适合不平衡样本。n_jobs=-1让网格搜索并行跑,但并行只加速计算,不改变结果。这里random_state=42同时出现在基础模型和GridSearchCV外层,确保每次网格搜索结果一致。
网格搜索跑完不要直接采信best_score_,还需要把最优模型放到第 2 章切出来的X_test上做最终验证。如果网格搜索分数 0.58,测试集分数 0.52,说明参数过拟合了训练段,要回退到更简单的参数组合。
4. 评估与回测:算清楚“预测准”和“赚不赚钱”是两回事
很多初次做股票预测的人看到测试集准确率 0.78 就兴奋,但准确率在股票数据里经常骗人。行情如果一年涨了 60%,模型只要一直预测“涨”,准确率轻松超过 0.65,实际什么都没学到。评估必须多层验证:先看 AUC 和 PR,再做回测看收益曲线,最后和买入持有基准对比。
4.1 用 AUC 和 AP 代替准确率评估涨跌方向
AUC 不依赖固定阈值,它衡量的是模型对正负样本排序的能力。AUC 等于 0.5 表示没有区分度,0.55 到 0.6 之间在股票预测里已经是值得继续改进的信号。比 AUC 更严格的是 Average Precision,它聚焦正例,也就是“预测上涨”里有多少真正上涨,这对交易更有参考价值。
from sklearn.metrics import roc_auc_score, average_precision_score print("rf auc:", roc_auc_score(y_test, prob_rf)) print("rf ap:", average_precision_score(y_test, prob_rf))逻辑说明:roc_auc_score吃的是测试集真实标签和模型输出的概率,不是predict出来的 0/1 结果。AUC 对类别不平衡不敏感,不会因为多数类占比高而虚高,这是它比 accuracy 适合股票预测的原因。
average_precision_score是 PR 曲线下面积,当正样本很少时,它的数值会比 AUC 低很多,但更贴近“预测上涨是否靠谱”这个业务问题。同样两个模型,AUC 相同的时候,AP 高的那个在真正上涨的日子里表现更稳定,回测收益也通常更好。
4.2 写一个最简单的回测函数:收益、最大回撤、年化
回测的代码要短,但逻辑必须严谨。这里有一个常见歧义:测试集里第 t 行的预测概率是在第 t 天收盘后得到的,如果当天买入,等于用了当天收盘价成交,还能吃到当天的收益,这在实盘中做不到。所以信号要shift(1),把 t 日的信号放到 t+1 日执行。
def quick_backtest(df, prob_col="prob", signal_th=0.55, horizon=5): df = df.copy() df["sig"] = (df[prob_col] > signal_th).astype(int) df["sig_exec"] = df["sig"].shift(1) df["future_ret"] = df["close"].shift(-horizon) / df["close"] - 1 df["strat_ret"] = df["sig_exec"] * df["future_ret"] df = df.dropna(subset=["strat_ret"]) equity = (1 + df["strat_ret"]).cumprod() drawdown = equity / equity.cummax() - 1 return { "累计收益": round(equity.iloc[-1] - 1, 4), "年化收益": round(equity.iloc[-1] ** (252 / len(df)) - 1, 4), "最大回撤": round(drawdown.min(), 4), "交易次数": int((df["sig_exec"] == 1).sum()), } X_test_plot = X_test.copy() X_test_plot["prob"] = prob_rf X_test_plot["date"] = df["date"].iloc[X_test.index] X_test_plot["close"] = df["close"].iloc[X_test.index] print(quick_backtest(X_test_plot, prob_col="prob", signal_th=0.55))逻辑说明:sig在 t 日根据概率生成,sig_exec = sig.shift(1)让信号延迟到 t+1 日生效。future_ret从 t 日收盘到 t+horizon 日收盘,策略收益只有信号生效那天才会吃到这段涨幅。这样就不会把 t 日当天的收益算进策略,避免回测曲线虚高。
参数说明:signal_th=0.55不是随便定的。逻辑回归给出的概率通常集中在 0.5 附近,用 0.5 做阈值会频繁开平仓,手续费先吃掉收益;抬高到 0.55 能过滤掉一部分噪声。这个阈值可以在 0.5 到 0.7 之间做敏感性测试,最优值不是固定的。
4.3 和基准比:超出买入持有才有意义
策略收益再高,也要先问一个问题:如果什么都不做,直接买入并持有到回测结束,收益是多少?牛市里买入持有可能赚 80%,你的策略赚 60%,表面上好看,实际跑输了基准。计算基准收益很简单:用最后一根收盘价除以第一根收盘价减 1。
bench_ret = X_test_plot["close"].iloc[-1] / X_test_plot["close"].iloc[0] - 1 strat_result = quick_backtest(X_test_plot) print("benchmark return:", round(bench_ret, 4)) print("strategy return:", strat_result["累计收益"])逻辑说明:基准收益是“无脑持有”的对照实验。策略收益只有在超过基准时,模型提供的涨跌信号才有实际价值。如果策略跑输基准,要么是信号质量不行,要么是交易频率太高导致成本吃掉收益,这时优先检查回测里的交易次数和最大回撤,而不是急着换模型。
这里也能解释一个常见疑问:为什么测试集 AUC 有 0.58,回测却不赚钱。因为 AUC 衡量的是排序质量,策略还要面对阈值、成本、持有期等多个环节的损耗,每个环节吃掉一点,最后收益就可能变负。评估股票预测模型,回测是唯一能让你说服自己的方式。
5. 避坑清单:模型预测股票涨跌每次结果不一样怎么办
前面几章的代码虽然能跑通,但换成真实数据后大概率会遇到几个经典问题。下面五条是我反复踩过的,每条都按“现象、原因、解决”来拆,方便你对照排查。
5.1 坑一:模型预测股票涨跌每次结果不一样
现象:同一份数据,第一次运行准确率 72%,第二次变成 68%,有时连 AUC 也跳来跳去。
原因:随机森林有自助抽样,逻辑回归有求解随机性,train_test_split默认也会随机打乱数据。只要其中一个环节没有固定随机种子,结果就不可能完全复现。另一个隐藏原因是数据索引在做concat或iloc后没有重建,导致切分逻辑每次拿到的行顺序不同。
解决:固定random_state=42,并且同时固定在三个地方:模型、train_test_split、GridSearchCV 的模型。时间序列切分不用train_test_split,但如果你用了,必须传shuffle=False。如果所有随机种子都固定后结果仍不同,先检查数据排序,df.sort_values("date").reset_index(drop=True)必须在特征计算之前执行。
5.2 坑二:特征或标签用错了未来数据,回测自欺欺人
现象:训练集 AUC 0.92,测试集 AUC 0.51,回测收益负得很稳定。
原因:未来函数泄漏。最常见的写法是df["ret_5"] = df["close"].pct_change(-5),符号写反后特征变成了“未来 5 天的收益率”;还有人用shift(1)去构造标签,模型实际在预测“昨天涨没涨”。
解决:检查所有特征列,凡是rolling、pct_change、diff产生的序列,都要确认它是用当前行和过去行计算的,不包含未来 K 线。标签必须用shift(-horizon)生成“未来收益”,测试时把最后一小段丢进dropna。一个快速的自测方法是把特征最大值日期打印出来,看是否晚于标签日期,如果特征日期比标签日期还新,一定有泄漏。
5.3 坑三:类别不平衡让准确率虚高
现象:测试集里“上涨”样本占 75%,模型全部预测上涨,准确率 0.75,但 AUC 只有 0.51。
原因:股票行情在多头阶段上涨天数远多于下跌天数,模型只要学到一个“默认涨”的偏置就能拿到高准确率。此时 accuracy 完全失真。
解决:评估一律改用roc_auc_score和average_precision_score,不要打印 accuracy 作为主要指标。模型侧加class_weight="balanced",让少数类样本的损失更高。不要急着用 SMOTE 过采样,时序数据里随机过采样会生成与未来样本重叠的伪样本,class_weight 在 sklearn 里已经够用。
5.4 坑四:切分时训练标签伸进验证期
现象:测试集前三天回测收益特别高,后面突然回落,整个回测结果不稳定。
原因:训练集最后一个样本的标签覆盖到验证期前四天,模型在训练阶段已经“看过”验证期初的行情,所以验证集最前端异常容易预测。
解决:用第 2.4 节的方法,在训练集末尾丢弃horizon个样本。更严谨的做法是把标签结束日期作为切分依据:训练集中每一行额外记录一个label_end_date = date + horizon 个交易日,切分时保证label_end_date <= train_end。用代码检查一下训练集最后一行的label_end_date,如果大于train_end,就是泄漏。
5.5 坑五:sklearn 和 scikit-learn 安装混了导致模型导入报错
现象:from sklearn.ensemble import RandomForestClassifier报No module named 'sklearn',或者明明装了 scikit-learn,一运行却提示版本太旧。
原因:PyPI 上还有个叫sklearn的废弃包,某些教程让读者pip install sklearn,结果环境和真正的scikit-learn冲突,导入时加载了错误路径。
解决:在干净的虚拟环境里执行pip install scikit-learn,不要装sklearn。然后用pip list | grep scikit-learn确认安装版本。如果已经装混了,先卸载两个包再重装。
6. 进阶技巧:把概率输出变成仓位信号,再滚动重训
先看一个只有 30 行的滚动重训流程:每次用过去 500 天数据训练,预测接下来 20 天,然后滑动窗口继续。这套流程比单次训练测试更接近实盘,也能缓解模型失效问题。
def rolling_train(df, model, train_window=500, test_window=20): prob_list, date_list = [], [] for start in range(0, len(df) - train_window - test_window, test_window): train = df.iloc[start:start + train_window] test = df.iloc[start + train_window:start + train_window + test_window] model.fit(train[feature_cols], train["label"]) prob = model.predict_proba(test[feature_cols])[:, 1] prob_list.extend(prob) date_list.extend(test["date"]) result = pd.DataFrame({"date": date_list, "prob": prob_list}) return result逻辑说明:训练窗口覆盖最近约两年交易日,预测窗口 20 天,模型每隔一个月重新训练一次。这样可以捕捉市场风格变化,避免用一个训练了三年的模型去预测完全不同的行情。参数可以按数据量调整,数据越多,训练窗口可以拉长到 1000 天。
参数说明:test_window=20在回测里等于一个月调仓一次,每次只对新的 20 天做预测。prob_list接收的是predict_proba的正类概率,后续回测用这个概率做阈值判断,而不是直接用 0/1 结果。
在滚动重训基础上,仓位管理是更实际的落地技巧。不要只看predict输出的 0 或 1,要用predict_proba输出连续概率。比如概率超过 0.6 才满仓,低于 0.6 但高于 0.55 只建半仓,低于 0.55 空仓。这样做的原因是:概率越接近 0.5,模型越没把握,投入资金应该越少。你可以用最后 20 天数据对比不同阈值下的累计收益和最大回撤,选出最稳的阈值区间。
另一个容易被忽略的验证方法是校准曲线。sklearn 里可以用calibration_curve看预测概率和实际上涨频率是否一致;如果模型给出 0.6 概率的样本实际上涨只有 0.52,说明概率被乐观估计了,阈值要相应抬高。
我现在拿到任何机器学习股票预测需求,第一反应不是换更复杂的模型,而是先确认四件事:预测目标、horizon、切分边界、评估指标。这四件事写不清楚,后面调参全是在碰运气。把 sklearn 这套流程跑顺之后,再尝试 XGBoost、LightGBM 或深度学习,也只是替换模型接口,整个数据与回测骨架不用重写。希望这篇内容能帮你在股票预测这条路上少踩几个坑。
本文还有配套的精品资源,点击获取