尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

机器学习时序预测:滞后特征构造与多步预测实战指南

机器学习时序预测:滞后特征构造与多步预测实战指南 先说我为什么想写这个“DAY 60”。前两篇把差分自回归滑动平均模型、指数平滑、Prophet这些统计路线的东西过了一遍理论上已经足够应付一波趋势加季节的数据。但真到实战里你会发现光靠统计模型有一个绕不开的坎它对“外生变量”的接纳能力非常有限。比如促销日、天气突变、某天突然上了热搜带来的流量脉冲这些信息很难塞进统计模型的框架里硬塞进去效果也非常勉强。所以做到第60天我把视线正式转向了另一条经典路线——用机器学习模型做时序预测。这一篇不会讲深度学习也不碰那些大而重的Transformer架构纯粹聊经典机器学习模型怎么用在时序预测上。包括为什么滞后特征是这样构造的、多步预测到底该用递归还是直接策略、树模型和线性模型在时序问题上各自的脾气以及我实际跑数据时踩过的几个坑。适合刚刚从统计模型往机器学习方向过渡、准备用自己的业务数据做预测的读者。1. 序列规律提取的三种方式经典模型到底在算什么很多从机器学习转过来的人第一次拿到时间序列数据时最困惑的一件事是我的模型输入到底是什么因为常见机器学习任务里每一行样本都有明确的特征和标签但时间序列就一串数字既没有特征也没有标签。这时候第一步要转变思路——把“序列”转换成“监督学习数据集”。1.1 时间结构特征让模型看见“过去”最基础也最常用的一种特征构造方式就是把前几个时刻的值作为特征。假设我们要预测第 t 天的销量那可以构造出这样的样本样本序号特征1t-3特征2t-2特征3t-1标签t110012011013021201101301253110130125140这就是常说的滞后特征lag feature。窗口大小取多少决定了模型能看到多远的过去。窗口太小模型只能捕捉短期波动窗口太大一方面引入噪声另一方面会让训练集的行数大幅减少。实际项目中我一般会同时加入多个不同大小的滞后窗口比如 1、3、7、14、30让模型自己决定哪个窗口的信息更关键。树模型对这种冗余并不敏感线性模型则需要对特征做筛选。1.2 统计特征滑窗里藏着的规律除了直接取历史值还可以在滑动窗口内计算统计量。比如过去7天的均值、过去7天的标准差、最大值、最小值、中位数甚至是一阶差分后的均值。这类特征的用意在于给模型提供“最近这段时期的总体水平”和“波动程度”的感知——很多时候预测目标本身不好直接回归但它的滑动平均却非常有规律。举个例子如果某天销量突然从100跳到1000单看滞后值模型会以为第二天还是1000附近但如果加上了过去7天均值这个特征模型就能判断出这是异常脉冲还是真实水平抬升因为均值特征会把这个脉冲拉回很多。这种“缓冲”效应对树模型尤其重要树模型对极端值本来就敏感。1.3 日历与周期特征时间本身也是信号除了序列内在的滞后结构第t天这个下标本身也携带大量信息。星期几、是几号、是不是月初、是不是月末、是不是节假日这些特征虽然看起来和“序列值”无关但对很多业务场景有极强的解释力。比如零售行业周末销量天然比工作日高外卖行业雨天订单暴涨SaaS行业月初的活跃度往往高于月末。这类特征编码方式很简单星期几用0-6的整数或者one-hot节假日用一个二值特征月份用1-12。如果你有农历数据像春节这种移动假日的影响也可以做成一个“距离春节还有多少天”的特征。这些在经典模型里非常管用因为树模型可以自动捕捉到“星期六且逢节假日”这类高阶组合关系。2. 特征工程做完之后为什么树模型比线性模型更省心同样一份带滞后特征的数据集扔给线性回归和扔给随机森林表现差异往往非常明显。核心原因在于时间序列里“特征与标签的关系”很少是线性的。2.1 非线性关系的天然优势线性回归假设特征每增加一个单位标签固定增加某个值这在真实序列里很难成立。比如滞后1天的销量从100涨到200对当天销量可能只增加50但从500涨到600可能增加120。这种边际效应递增或递减的模式线性模型无法表达。而决策树通过不断切分特征空间天然就能拟合这种分段式的关系——它本质上是在把特征空间切成若干个矩形区域每个区域给一个输出值所以无论关系多曲折只要有足够多的树和足够深的深度就能逼近。2.2 无需标准化与缺失值容忍树模型对特征的尺度不敏感。销量这个特征数值是万级别、促销标记是0/1线性模型必须做标准化否则数值大的特征会主导梯度更新方向树模型则完全不在乎它只关心在哪里切分。另外很多时序数据里会有缺失值线性模型面对缺失值要么删除样本要么插补树模型尤其是LightGBM和XGBoost原生支持缺失值处理训练时会自动学习缺失值应该走向哪个分支。2.3 但线性模型也没有完全出局看到这里别急着把所有数据都扔给树模型。在线性模型仍有一个难以替代的优势——可解释性和外推能力。当你要预测的未来时间段出现了训练集里没有见过的数值范围比如销量突然增长到历史最高点的2倍树模型只能给出训练集见过的平均值而线性模型的 extrapolation 能力虽然也不完美但至少能沿着斜率继续向上延伸。另外如果你的任务需要向非技术领导解释“为什么预测结果是这个数”线性模型的系数比树模型的特征重要性直观得多。所以我的习惯是先跑一个线性模型当baseline再上树模型两者对比看差异而不是直接抛弃某一个。3. 多步预测的三种策略递归、直接与多输出时序预测和普通回归任务的另一个关键区别是我们通常不止预测下一个时刻而是要预测未来7天、未来30天。这时候就面临一个问题——模型训练时用的是“基于t-3到t-1预测t”的样本但到了真正预测未来时t1之后的历史值还没有发生特征里的滞后项从哪来3.1 递归策略用预测值代替真实值递归预测的思路很直接。先用模型预测出 t1 的值把它拼接到历史序列末尾构造出 t2 的特征再用同一个模型预测 t2如此迭代下去直到得到整个预测区间。这个策略最大的优点是训练成本低——只需要一个模型。但缺点也很明显误差会随着预测步数增加而累积。第一步预测偏了5%第二步基于这个偏掉的输入再预测可能就偏了10%到第30步可能已经完全偏离真实轨迹。我见过很多初学者踩这个坑单步预测的验证集上表现不错一换到多步输出就崩原因就是误差累积。缓解的办法包括训练时给特征加入少量噪声模拟预测误差的传播或者做多模型集成不同步数用不同模型但代价是训练成本成倍上升。3.2 直接策略每个步长单独建模直接预测的意思是为每一步训练一个独立的模型。模型1负责预测t1模型2负责预测t2以此类推。每个模型在训练时都只使用真实的历史值作为特征不依赖任何预测值因此不会有误差累积的问题。缺点是训练成本高——预测30天就要训30个模型。但在我的实际经验里当预测步长超过7天时直接策略的精度优势非常明显尤其是数据噪声比较大的场景。做滚动训练每过一天重新训练或微调时直接策略也更方便维护——某一步的模型效果不行只需要单独调那一步不会影响其他步长。3.3 多输出策略从一行样本出多个标签第三种方式是在构建训练集时让每行样本对应多个标签给定 t-3到t-1的特征同时输出 t、t1、t2……t6 七个值。像LightGBM这种树模型本身不直接支持多输出需要包装一层MultiOutputRegressor随机森林倒是原生支持。这种方式的好处是模型能学到各步长之间的相关性坏处是如果各步长的分布差异很大模型需要更大的容量才能拟合好而且可解释性比较差。从我的实践看并没有一个绝对的赢家。一般经验是预测步数短1-3天优先递归步数适中7-14天优先直接步数长且各步之间关联性强时尝试多输出。更重要的是把三种策略都跑一遍用验证集对比而不是靠直觉选。4. 一次完整的多步预测实战从数据清洗到评估理论说完用一个实际例子串一遍整个流程。为了好复现我用的是一份模拟的日销量数据生成逻辑包含趋势项、季节项和噪声这样代码可以直接跑结论也接近真实场景。4.1 数据集构造与划分原则import numpy as np import pandas as pd from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_absolute_error np.random.seed(42) date_rng pd.date_range(start2022-01-01, end2024-12-31, freqD) n len(date_rng) trend np.linspace(50, 200, n) seasonal 20 * np.sin(2 * np.pi * np.arange(n) / 7) 10 * np.sin(2 * np.pi * np.arange(n) / 365) noise np.random.normal(0, 5, n) sales trend seasonal noise df pd.DataFrame({date: date_rng, sales: sales})这里有个非常重要的原则切分训练集和测试集时不能随机打乱。时间序列必须按时间顺序切分——用前80%的数据做训练后20%做测试而且测试集一旦参与特征构造就不能再回传信息给训练集否则就是典型的数据泄漏。我习惯的做法是把特征构造分两步先在训练集上计算统计量比如均值、标准差再把这些统计量应用到测试集而不是把两组数据拼接后统一算。4.2 滞后特征与滚动统计的构造def make_features(df, lag_days[1, 3, 7, 14], win_days[7, 30]): df df.copy() for lag in lag_days: df[flag_{lag}] df[sales].shift(lag) for win in win_days: df[froll_mean_{win}] df[sales].rolling(win).mean() df[froll_std_{win}] df[sales].rolling(win).std() df[dayofweek] df[date].dt.dayofweek df[is_weekend] (df[dayofweek] 5).astype(int) df[month] df[date].dt.month df[dayofyear] df[date].dt.dayofyear return df特征工程做完后会有一个副作用前14行因为rolling窗口没填满会出现NaN。这部分数据不能直接用来训练需要丢弃。但要注意丢弃的时间段是最早的14天不是随机丢弃这个顺序感很重要。另外dayofyear配合sin/cos变换可以告诉模型“一年中的第几天”这个周期位置——直接用一个整数其实也可以树模型能自己切分但如果想让线性模型也吃这套特征最好再做一次周期编码。4.3 模型训练一步到位还是递归滚动拿递归策略举例。先只训练一个单步模型然后在测试集上滚动预测# 构造特征 df_feat make_features(df) train df_feat.dropna().iloc[:-30].copy() test df_feat.dropna().iloc[-30:].copy() feature_cols [c for c in train.columns if c not in [date, sales]] model RandomForestRegressor(n_estimators400, random_state42) model.fit(train[feature_cols], train[sales]) # 递归预测未来30天 history train[sales].tolist() predictions [] for step in range(30): data { lag_1: history[-1], lag_3: history[-3], lag_7: history[-7], lag_14: history[-14], } # 窗口统计 data[roll_mean_7] np.mean(history[-7:]) data[roll_std_7] np.std(history[-7:]) data[roll_mean_30] np.mean(history[-30:]) data[roll_std_30] np.std(history[-30:]) # 日历特征 date test[date].iloc[step] data[dayofweek] date.dayofweek data[is_weekend] 1 if date.dayofweek 5 else 0 data[month] date.month data[dayofyear] date.dayofyear pred model.predict(pd.DataFrame([data])[feature_cols])[0] predictions.append(pred) history.append(pred) # 关键预测值回填这段代码的递归逻辑是每预测出一天就把预测值追加到history末尾用于构造下一天的特征。第30天的时候history里其实已经有29个值是模型自己生成的。这也是误差累积的根源所在。4.4 评估阶段MAE不是唯一标准最终输出的predictions和test[sales]可以直接算MAE、RMSE。但只给一个数值指标远远不够。我强烈建议把预测值和真实值画在一起肉眼看一眼趋势跟随情况。很多时候MAE看着还行但预测曲线比真实曲线整体滞后了两三天这种情况在滞后特征较多的模型里非常常见——模型学会了“拿昨天的值当今天的预测”因为序列自相关太高时这招对MAE的优化效果非常显著。所以需要额外看两个指标方向准确率预测上涨时真实是否上涨和峰谷时刻误差预测的最高峰比真实晚了几天。这两个指标一开始可能不明显但对业务决策非常重要——如果你是做库存管理的峰值晚预测两天就意味着补货晚了48小时。5. 实战中比调参更值得花时间的几件事模型跑通只是起点真正让预测精度有质的提升靠的往往不是调参而是下面这些容易被忽略的细节。5.1 滞后阶数的选择不能靠拍脑袋滞后窗口取多少行业里有一个经典做法先看自相关函数ACF和偏自相关函数PACF图。ACF图能告诉你“多少天前的值对今天还有显著影响”如果第14天突然又出现一个高峰那很可能存在月度或双周周期。我见过有人滞后期取到90天结果模型严重过拟合而且推理时为了让前90天数据齐全预测起点被硬生生拖后了三个月业务上根本不可接受。更务实的做法是滞后窗口覆盖业务周期即可。日维度数据至少覆盖一周7天周维度数据覆盖一年52周月维度数据覆盖一年12个月。再多加几个2倍周期、3倍周期的窗口然后让模型自己筛。5.2 异常值处理先看业务再动手时序数据里的异常值处理比普通回归更敏感。普通回归里你可以用三倍标准差直接剔除但时序里一个异常点可能就是某次大促的真实记录如果直接剔除模型永远学不会“大促来了销量会暴涨”。我的经验是遇到异常值不要急着删先标记它们是哪类事件大促、系统故障、数据上报错误。再用一个“是否大促日”的0/1特征把这些信息显式传给模型。这样一来模型既不会把日常的异常噪声当成规律也能在大促再次到来时给出合理的反应。相比之下直接把异常值拉平或删掉等于亲手扔掉了最有业务价值的样本。5.3 训练/预测分布漂移模型失效的隐形杀手时序模型有一个躲不掉的痛训练时用的分布和预测时的分布往往不一样。比如训练数据是2022到2024年的销量但2025年业务增长策略变了用户规模翻了一倍销量整体上了一个台阶。这时候不管你用多厉害的模型预测值都会系统性地偏低。缓解办法之一是给模型加“趋势外推”的能力——把“天数序号”也作为一个特征传入模型让模型学到线性增长的部分。更务实的办法是用近期数据做增量训练。比如固定窗口训练只用最近180天的数据训练模型每个星期更新一次。虽然牺牲了一部分历史规律的学习但换来的是对近期变化的敏感性。5.4 不要迷信交叉验证很多从普通机器学习过来的人会在时序问题上直接用K折交叉验证这是一个非常隐蔽的错误。K折交叉验证要求样本独立同分布但时间序列的样本之间存在强自相关。随机打乱后的训练集里出现了未来数据验证集里出现了过去数据得到的分数会虚高而且对真实泛化能力完全没有参考意义。正确做法是使用时序交叉验证TimeSeriesSplit第一次用前50%训练、后10%验证第二次用前60%训练、后10%验证依次推进。这种方式保证每次训练集的时间都在验证集之前得到的误差分布才是真实预测场景下的误差分布。6. 踩坑笔记经典时序机器学习里最常犯的几个错这一节把我过去实际踩过的、以及帮读者排查问题时反复看到的错误整理出来每一条都对应过真实的“预测崩了”时刻。6.1 先差分还是先构造滞后特征方向反了很多人会先把原始序列做差分变成平稳序列再构造滞后特征。逻辑上没有错但有一个细节树模型根本不需要你差分。树模型对非平稳性不敏感差分了反而会丢失水平信息。比如销量从日均100涨到日均150差分后的损失了“100和150”这个水平差异模型只能看到最近几天的增量一旦趋势停止增长预测值会立刻掉回原地。如果你是线性模型差分还是有必要的如果是树模型我强烈建议直接对原始值建模然后让滞后特征自己表达变化。树模型能用切分捕捉到“相比昨天涨了多少”的含义不用你手动算。6.2 预测结果整体滞后最隐蔽的“假优秀”这是一个让很多新手困惑的问题MAE不高看起来预测也不错但就是感觉曲线比真实晚了一拍。原因在于如果序列的自相关系数非常高比如前一天150当天151历史预测值一直比真实低一点点模型最优策略就是把滞后1天的值直接作为预测值这样MAE会非常小。但如果业务需要预测的是“拐点”这种模型就完全失效了——因为拐点出现的第1天真实值突然从150跳到200而模型还在用150预测。诊断方法计算预测序列与真实序列的互相关函数cross-correlation如果峰值出现在滞后1-2天说明预测曲线整体滞后。缓解手段删除lag_1这个特征强制模型不能只靠“昨天复读”或者把滞后特征的权重降低。6.3 特征泄露你比想象中更频繁地犯特征泄露在时序里是这样的你在t时刻构造了一个统计特征用的是“整个测试期的均值”。比如你想加一个“2024年平均销量”的特征当时你觉得这是已知信息实际这个特征在预测期开始之前根本不存在模型提前看到了未来——这就是一种隐蔽但致命的泄露。再举一个非常常见的对时序数据做缺失值填充时用了全局均值填充。如果缺失值发生在测试期你等于用未来信息填了过去。正确做法是只用训练期数据计算填充值。6.4 随机种子不固定调参白折腾树模型里随机性很强尤其随机森林和梯度提升树每次跑结果可能都不一样。如果你在评估特征重要性或对比模型时没有固定随机种子看到的效果差异可能纯粹来自随机波动而不是你改的那一行参数。我习惯在每次实验开始时设定np.random.seed(42) random.seed(42)并且在代码注释里写明本轮实验用的种子值方便回溯对比。这在做多步预测实验对比时尤其重要。7. 从单一模型到集成的小步迭代到现在为止聊的都是单模型方案。真实业务里单一模型总有不擅长的部分线性模型学不到非线性树模型学不到趋势外推。最稳妥的做法是在单模型跑通之后再做一个简单集成。7.1 按预测步长选模型再融合我常用的一个方案是步长1-3天用随机森林对近期变化敏感步长4-14天用XGBoost或LightGBM对中长期规律更稳定步长15-30天用线性回归外推能力相对好。然后把这三段拼接起来。虽然接口处会有一些跳跃但整体效果往往比单一模型硬扛30天要好很多。加一个简单的平滑处理比如接口前后3天取平均就能让曲线更自然。7.2 多模型加权平均另一种做法是所有步长都用同一个模型但多个不同模型并行训练最后按验证集误差加权平均。举个例子随机森林在验证集上MAE是12XGBoost是13线性回归是18那最终的权重可以按误差倒数归一化随机森林权重0.42XGBoost 0.39线性回归 0.19。这样既保留了各模型的优势又避免过度依赖单一模型。7.3 调整方向比堆模型重要其实做了几十天时序预测实验后最强烈的感受是模型并不是瓶颈特征和对业务的理解才是。每次精度大幅度提升几乎都是因为一个新特征上线——比如补上节假日、补上天气、补上促销计划。模型层面从随机森林换成LightGBM往往只提升几个百分点的误差。所以如果你时间有限先把70%的精力放在特征和数据清洗上剩下30%再来折腾模型。这也就是为什么在第60天这个节点上我依然在用经典模型而不是直奔深度学习——不是深度学习不好而是经典模型在特征可解释、训练成本、部署难度上仍然优势明显。对大多数业务场景来说先把手上的滞后特征、日历特征用足效果超过深度学习模型并不是什么稀罕事。
返回列表