ARTICLE DETAIL

资讯详情

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

LSTM预测股票数据:从数据清洗到时间窗口构建的完整预处理指南

LSTM预测股票数据:从数据清洗到时间窗口构建的完整预处理指南 开头先问一个直击灵魂的问题你有没有见过那种训练好的LSTM模型在回测里曲线跟实际走势贴得像双胞胎一上实盘就拉胯的我见过很多次而且踩过一遍之后发现问题基本不在模型结构而在喂给模型的那些数据本身。LSTM预测股票数据这件事真正拉开差距的往往不是Layer怎么搭、神经元放多少个而是前面那段谁都不愿意细看的数据预处理。今天这篇就把我实际操作中的一套完整流程摊开讲从拿到原始的股票行情数据开始怎么清洗、怎么构建特征、怎么归一化、怎么切分时间窗口一直到能把数据直接送进LSTM模型里训练。整个过程中哪些地方容易埋雷、哪些坑我替你先踩过了都会写清楚。适合刚开始做时间序列预测的Python学习者也适合那些模型怎么调都不收敛、怀疑人生的朋友——数据这关过了后面会顺很多。1. 核心思路为什么LSTM预测股票要先啃数据预处理这块硬骨头1.1 LSTM对数据的“挑剔”远超你的想象LSTM本质上是一个基于梯度下降训练的神经网络它对输入数据的要求比传统机器学习模型更苛刻。你看树模型比如XGBoost你给它喂原始数据特征量纲不统一、有缺失值它照样能跑出个结果但LSTM不行它对数值范围极其敏感。原因在于LSTM内部使用了sigmoid和tanh作为激活函数。sigmoid的输出范围是(0,1)tanh是(-1,1)如果输入数据的数值范围是几十几百甚至上千比如股价从几块钱到几百块钱激活函数在训练初期就会进入饱和区梯度要么消失要么爆炸。我见过最多的一个报错场景就是用原始股价直接训练LSTMLoss在训练两轮之后直接变成NaN怎么调学习率都没用。这背后就是数值范围没做归一化导致的梯度问题。另外LSTM的输入是一个三维张量形状是(samples, timesteps, features)。这意味着你做预处理的时候不能像处理普通表格数据那样一行一列喂进去就行必须把原始数据重构成时间步序列。这个重构的过程如果出了问题模型接收到的时序关系就是混乱的你等于让模型在错误的上下文里去学规律。1.2 股票数据到底“脏”在哪里很多人觉得股票数据是从行情软件里导出来的交易所给的官方数据还能脏到哪去实际上脏得离谱。我举几个实际会遇到的情况第一非交易日缺失。周末、节假日没有交易数据但数据源不会帮你标注哪天是节假日你需要自己处理这些空隙否则模型会把周一和周五之间硬生生看成连续相邻的时间点。第二停牌。某些股票停牌几天甚至几个月数据源直接给你空一排或者用前值填充甚至可能直接跳过去。如果不加处理模型可能会把停牌前后两个交易日当成相邻交易日学出来的东西完全是错的。第三复权问题。股票会分红、送股、拆股价格会发生跳变。如果直接用不复权价格你会看到股价在某天突然跌了一大截或者涨了一大截但这不是市场行为而是除权除息造成的。不处理复权问题模型等于在学一些根本不存在的价格变化。第四数据缺失和异常值。有些数据源在某个时间段可能拉取失败出现NaN或者极端异常值比如某个字段突然多了一位小数。这些脏数据如果不处理训练过程就是一场灾难。1.3 预处理在整个LSTM项目里的定位我的个人习惯是整个LSTM股票预测项目的耗时分配大概是数据预处理占六成模型调参占三成剩下的一成才是把模型跑起来。这不是夸张数据预处理直接决定了模型学习的上限。打个比方LSTM模型就像一台精密的发动机数据预处理就是进入发动机之前的燃油净化系统。原油不是不能用但里面杂质太多发动机跑不了多久就会出问题。预处理做得越细燃油越纯净发动机才能稳定输出动力。这个类比同样适用于LSTM你给模型的数据越干净、结构越合理模型能学到的规律就越真实预测结果就越有参考价值。2. 动手清洗获取股票数据后的第一轮加工2.1 数据源选哪个怎么选做A股数据国内能用的开源库主要有tushare、akshare和baostock。我这里不踩一捧一就事论事说下各自特点。Tushare是老牌库数据质量高但现在的积分体系让新用户很头疼很多好用的接口需要积分才能调。Akshare是爬虫聚合型的免费接口多数据覆盖广但偶尔会有字段格式不稳定的情况。Baostock是完全免费的接口简单数据稳定性也还行适合个人学习和研究。如果你只是想跑通一个完整的LSTM项目我的建议是先用baostock或akshare注册简单、接口免费等数据需求复杂了再考虑tushare的积分接口。我自己初学阶段用的就是baostock简单够用。2.2 用pandas把原始数据拉回来并做基础清洗假设你已经拿到了某只股票的历史日线数据一般包含date日期、open开盘价、high最高价、low最低价、close收盘价、volume成交量这几个字段。第一步是把这些数据装进pandas的DataFrame里然后做基础清洗。import pandas as pd import numpy as np # 假设df是从数据源接口拿到的原始DataFrame # 列名可能是英文也可能中文统一处理一下 df.columns [date, open, high, low, close, volume] # 1. 日期列转成datetime类型并设为索引 df[date] pd.to_datetime(df[date]) df.set_index(date, inplaceTrue) # 2. 按日期升序排序这个很重要 df.sort_index(inplaceTrue) # 3. 去除完全重复的行 df.drop_duplicates(inplaceTrue) # 4. 检查缺失值 print(df.isnull().sum())这里有一个特别容易踩的坑原始数据拿回来之后行顺序不一定是升序的。有的数据源是倒序返回最新日期在最上面有的正序有的干脆乱序。如果没有先排序就直接进模型整个时间序列的因果关系就全乱了LSTM学到的“规律”就没有意义。所以排序这一步请务必放到所有操作的最前面。2.3 缺失值和异常值的处理策略缺失值的处理没有一刀切的标准答案完全取决于缺失的上下文。我常用的策略有三种如果缺失的是某一天的某几个字段而这个缺失在时间上是孤立的我倾向于用前后两天的均值填充。如果是连续多天的缺失说明可能遇到了停牌这时候要谨慎。直接用前面的值填充会引入很大的误差更稳妥的办法是从数据源重新拉取确认这段缺失是停牌还是拉取失败导致的。如果是分红送股导致的价格跳变这不在“缺失值”的范畴内需要用复权处理后面专门讲。异常值方面我通常会统计每个字段的分布找出那些超出正常范围的数据点。比如某天的close价格突然比前后两天高出或低出20%以上大概率是脏数据需要人工核实。# 缺失值处理示例前后均值填充 df[close] df[close].interpolate(methodlinear) # 异常值检测示例以close为例计算前后变化的百分比 df[pct_change] df[close].pct_change() # 找出变化超过20%的异常点这里要根据实际标的调整阈值 outliers df[abs(df[pct_change]) 0.2] print(outliers)注意阈值的选择要结合实际标的。波动大的股票一天涨跌20%可能就是正常的而波动小的蓝筹股这个比例就值得关注了。不要想当然地套用一个固定的阈值。2.4 复权处理股票数据里最容易翻车的一环复权这个事我说得直白一点不处理复权的股票数据就是带着“假信号”的数据。除权除息会让股价在同一天出现一个巨幅跳空但这个跳空跟市场供需没有任何关系它只是公司财会操作在价格上的映射。复权分为前复权和后复权。前复权是以当前价格为基准调整历史价格后复权是以最早价格为基准调整之后的价格。做时间序列预测我推荐用前复权数据。原因是前复权更新比较方便数据源的接口一般都支持直接获取前复权价格。用baostock的话拉取数据时直接传入复权参数就行它会返回调整后的价格。拿到复权价格之后你跟不复权价格核对一下会发现历史价差拉开了很多——这不是数据错了而是复权调整的正确结果。3. 构建特征让模型从K线里读出它该学的东西3.1 原始字段直接喂给LSTM好不好直接拿open、high、low、close、volume这五个原始字段喂给LSTM能不能跑能跑但效果一般。原因在于股票价格这种原始数值本身包含的趋势信息很弱而且噪音很多。模型需要在有限的特征维度里找出规律你给它一堆包含着大量冗余信息的东西它学出来的效果自然不好。传统量化里的技术指标虽然被很多人批评为“玄学”但它们本质上是价格和成交量的统计变换可以帮LSTM把数据里隐含的短期趋势、动量、波动情况等特征显性化。所以我的习惯是在原始字段之外再叠加一些衍生特征。3.2 我常用的衍生特征组我常用的衍生特征包括这几个移动平均线MA、指数移动平均线EMA、相对强弱指标RSI、收益率return、价格波动率volatility。每一个特征的构建方式都不复杂但组合起来能给模型提供更丰富的视角。# 移动平均线 df[ma5] df[close].rolling(window5).mean() df[ma10] df[close].rolling(window10).mean() df[ma20] df[close].rolling(window20).mean() # 指数移动平均线 df[ema12] df[close].ewm(span12, adjustFalse).mean() df[ema26] df[close].ewm(span26, adjustFalse).mean() # 收益率当日收盘相对于昨收的变化率 df[return] df[close].pct_change() # 波动率用最近N日收益率的标准差表示 df[volatility] df[return].rolling(window20).std() # RSI计算 delta df[close].diff() gain delta.clip(lower0) loss -delta.clip(upper0) avg_gain gain.rolling(window14).mean() avg_loss loss.rolling(window14).mean() rs avg_gain / avg_loss df[rsi] 100 - (100 / (1 rs))很多人在这一步会犯一个致命错误直接用现成的技术指标库比如ta-lib计算指标确实效率高但你并不清楚每个指标用了几根K线、平滑参数怎么设的以及它在前端会不会用到未来数据。我在实际操作中除非性能瓶颈很明显否则更倾向于自己手动算关键指标这样每一步的可控性都尽在掌握。3.3 特征相关性检查挑出真正有用的列特征构建完成之后不要急着直接丢给模型。先检查一下特征之间的相关性把冗余度太高的特征剔除掉。为什么因为互相高度相关的特征等同于把同一个信息复制了好几份会干扰LSTM的注意力分配。# 计算特征相关性矩阵 corr_matrix df.corr() # 筛选出与目标变量比如次日收益率相关性绝对值大于某阈值的特征 target_corr corr_matrix[next_return].abs().sort_values(ascendingFalse) print(target_corr)需要跟你们说清楚的是相关性低不代表特征没用。LSTM是能学习非线性关系的模型一个特征和目标之间的线性相关系数只有0.02但它可能在特定条件下对预测有重要贡献。所以相关性检查更像是帮助你发现冗余特征而不是帮你确定哪些特征“有用”。3.4 时间特征与避免未来数据泄露股价预测里有一个特别关键的敏感性话题未来数据泄露look-ahead bias。简单说就是你在构造特征时不小心用到了t时刻之后才能知道的数据导致模型在训练时“偷看”了未来的信息。最常见的泄露来源是目标值的构造。如果你要预测的是第t1天的涨跌方向那么t时刻的特征绝对不能包含t1天的数据如果你要预测的是t1天的收盘价那t时刻的特征也不能包含t1天当天的任何价格信息。听起来很基础但我在实际看别人的项目代码时经常看到有人用t1天的数据去计算当天的移动平均线这就是典型的未来函数。另一个容易忽略的是时间周期性特征。股票市场有很强的星期效应Monday effect、Friday effect不同月份也有季节性差异。把这些信息编码进特征对模型往往有帮助。# 添加星期几作为特征 df[weekday] df.index.dayofweek # 添加月份作为特征 df[month] df.index.month # 序号编码用于表示时间顺序 df[time_idx] range(len(df))4. 归一化与数据切分这里埋着最容易踩的雷4.1 为什么LSTM推荐用MinMax归一化而不是标准化归一化、标准化这两个词经常被混用但在LSTM这个场景下我推荐使用MinMax归一化而不是StandardScaler标准化。原因要从LSTM的激活函数讲起。LSTM的默认激活函数是tanh输出范围是(-1,1)。如果数据是零均值、单位方差的标准化之后大部分数值会落在(-3,3)之间tanh在这个区间里有一段区域是接近饱和的梯度非常小。而MinMax归一化把数据拉伸到(0,1)区间这个区间对应tanh的线性区间梯度最大学习效率最高。我在实操中的选择是把价格类特征归一化到(0,1)区间把一些比例类特征如收益率、RSI也归一化到(0,1)区间。这样所有特征在数值范围上保持一致性模型训练更平稳。from sklearn.preprocessing import MinMaxScaler # 只对特征列做归一化目标列单独处理 feature_cols [open, high, low, close, volume, ma5, ma10, ma20, return, volatility, rsi] scaler MinMaxScaler(feature_range(0, 1)) scaled_features scaler.fit_transform(df[feature_cols]) df_scaled pd.DataFrame(scaled_features, columnsfeature_cols, indexdf.index)注意pct_change、rolling等操作产生NaN值在归一化之前务必先填充。否则MinMaxScaler会把NaN传下去后面构建时间窗口时会制造一连串NaN影响训练。4.2 归一化的“时序”陷阱必须在训练集上fit这里要敲黑板了MinMaxScaler的fit操作只能在训练集上进行不能在整个数据集上进行。原因很简单如果你的整个数据集包含了未来半年的数据你拿未来半年的最大值和最小值来归一化训练集的数据那就等于泄漏了未来的信息。正确的做法是先把数据切成训练集、验证集、测试集然后在训练集上fit再把训练集、验证集、测试集都用同一个scaler做transform。# 正确做法先切分再归一化 train_size int(len(df_scaled) * 0.8) train_df df_scaled.iloc[:train_size] test_df df_scaled.iloc[train_size:] scaler MinMaxScaler(feature_range(0, 1)) train_scaled scaler.fit_transform(train_df) test_scaled scaler.transform(test_df)4.3 时间窗口构建lookback到底怎么选时间窗口也叫lookback是LSTM输入序列的长度。你要预测第t1天的价格就需要提供t、t-1、t-2……直到t-lookback1的数据给模型。lookback选多少直接决定了模型能“看到”多长的历史上下文。很多初学者胡乱选一个窗口比如30、60、100选完发现模型效果不好然后开始疯狂调参其实问题很可能出在窗口长度和数据的周期性不匹配上。我的经验是lookback的选择应该结合数据的周期性和交易频率来确定。A股市场有明显的5日周期一周交易日和20日周期一个月我会至少试5的倍数和10的倍数。操作上可以写一个简单的循环在验证集上比较不同lookback的效果。def create_sequences(data, lookback): X, y [], [] for i in range(lookback, len(data)): X.append(data[i-lookback:i]) y.append(data[i]) return np.array(X), np.array(y) lookback 20 # 先试20天 X_train, y_train create_sequences(train_scaled, lookback) X_test, y_test create_sequences(test_scaled, lookback) print(X_train.shape, y_train.shape) # (样本数, 20, 特征数), (样本数, 特征数)这里有一个细节很多人忽略如果y的构造是“用前20天预测第21天的close”那么X的最后一个时间步是第t-1天y是第t天。这个对齐关系如果搞错了模型的预测结果整体偏移一天看似很准其实是在预测过去。4.4 数据集切分的正确姿势按时间顺序严禁随机打乱传统机器学习的做法是把数据随机打乱然后划分训练集和测试集。但时间序列数据的核心特征是时间顺序的连续性一旦随机打乱模型学习到的所有时间依赖完全被破坏。所以LSTM的数据切分必须按时间顺序前80%做训练中间10%做验证最后10%做测试。很多人在这个环节想当然地用了train_test_split的默认参数结果模型在回测时候表现很好实际使用就拉胯——就是因为随机切分让模型偷看了未来数据。# 正确的时间序列切分 train_ratio 0.8 val_ratio 0.1 train_len int(len(data) * train_ratio) val_len int(len(data) * val_ratio) train_data data.iloc[:train_len] val_data data.iloc[train_len:train_lenval_len] test_data data.iloc[train_lenval_len:]5. 现场排坑数据预处理五个高频报错实录5.1 问题排查速查表现象可能原因排查思路训练Loss变成NaN数据未归一化或归一化范围不当检查特征数值范围改用MinMax归一化到(0,1)模型预测值整体偏移一天时间窗口对齐错误检查X和y的对应关系训练集和测试集之间性能差异巨大归一化时整个数据集fit了确认scaler只在训练集上fit输入维度报错构建X时lookback设置或数据维度不对检查create_sequences的输出维度验证集效果比训练集还好未来数据泄漏检查特征构建是否用到t1时刻的数据5.2 我在实际项目中踩过的三个“深坑”第一个坑是归一化Scaler的保存问题。训练好模型之后部署阶段需要对新输入的数据做同样的归一化处理。如果你只在训练时fit了scaler用完就丢了上线时又拿新的数据重新fit得到的归一化范围会完全跑偏。正确的做法是用joblib把scaler保存下来推理时直接load出来用。import joblib # 训练阶段保存scaler joblib.dump(scaler, scaler.pkl) # 推理阶段加载scaler scaler joblib.load(scaler.pkl) new_data_scaled scaler.transform(new_data)第二个坑是inverse_transform的维度问题。LSTM输出的是一个包含所有特征的归一化矩阵多元预测时候你想恢复成真实股价需要挑出close这一列的位置做一个逆变换才能回到原始价格。很多人直接对单列数据做inverse_transform结果报维度错误或者恢复出来的结果不对。# 假设feature_cols里close排在第三个位置下标2 close_idx feature_cols.index(close) predicted_close_scaled y_pred[:, close_idx].reshape(-1, 1) # 构造一个和原始特征维度相同的临时数组 temp_array np.zeros((len(predicted_close_scaled), len(feature_cols))) temp_array[:, close_idx] predicted_close_scaled[:, 0] # 逆变换 predicted_close scaler.inverse_transform(temp_array)[:, close_idx]第三个坑是样本量不足。LSTM是一个数据饥渴型模型如果你只有几百天的数据模型很难学到有效的规律。这种情况下即使预处理做到完美结果也不会太好。我一般建议至少要有三年以上的日线数据能覆盖多个完整的涨跌周期模型才勉强有可用性。5.3 一个验证预处理是否到位的方法做完整套预处理之后怎么判断数据是否达到了可直接训练的状态除了肉眼检查我习惯习惯跑一个最简版本的LSTM来“体检”数据集。只用一层LSTM、一个Dense输出层训练50轮看Loss曲线是否平稳下降。如果Loss曲线乱跳或者根本降不下来说明预处理里大概率有问题。这时先不着急调模型回头查数据。如果Loss能平稳下降且验证Loss在合理范围说明数据这关过了可以放心去迭代更复杂的模型结构。import tensorflow as tf from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout model Sequential() model.add(LSTM(64, input_shape(lookback, X_train.shape[2]), return_sequencesFalse)) model.add(Dense(1)) model.compile(optimizeradam, lossmse) history model.fit(X_train, y_train[:, close_idx], epochs50, batch_size32, validation_split0.1, verbose1)如果验证Loss不下降先别急着怪模型试试把学习率调低一点再跑如果还不下降大概率问题出在数据预处理上。结尾最后再分享一个小技巧算是我自己的一个习惯每次做完数据预处理我都会把特征数据的分布可视化一遍画个直方图或者箱线图确认每个特征的范围基本一致。这一步花不了几分钟但能让你及时发现那些数值范围异常的特征列省得后面训练到一半才发现数据有问题回头再改来回折腾。做LSTM股票预测数据预处理永远是项目成功的地基。这套流程我反反复复用了一年多自己团队的新人来了也是按这个套路走踩坑踩得少很多。数据靠谱了后面模型的事都好说。
返回列表