2026/10/9 13:27:58

随机森林时间序列预测实战:从滑动窗口到特征工程的完整指南

随机森林时间序列预测实战:从滑动窗口到特征工程的完整指南 简介这是一份面向时间序列预测场景的Python完整实现以RF算法为核心附带训练与测试所需的CSV数据集适合计算机、电子信息、数学等专业学生在课程设计、期末大作业或毕业设计中直接参考。代码在Anaconda和PyCharm环境下运行包含全流程可执行脚本采用参数化编程关键参数方便调整并配有保姆级逐行注释便于新手快速理解特征构建、模型训练与预测评估的完整逻辑。压缩包共3个文件以py脚本为主、2个csv数据文件为辅整体仅46KB轻量易用。目前已有194人学习下载。通过这套源码读者能直接获得可复现的RF时间序列预测方案理清从数据读取到结果输出的代码脉络也能在此基础上替换数据或调整参数快速迁移到自己的实验与项目中或作为后续算法对比的基线实现。1. RF时间序列预测为什么随机森林能处理时序以及它和LSTM真正的区别如果你手里有一列历史数据想预测未来几天的走势第一反应往往是LSTM、Prophet或者ARIMA。但真实业务里很多做流量监控、库存补货、设备告警预测的工程师最后落地的方案却是随机森林Random ForestRF做时间序列预测。原因很直接RF不要求数据平稳、不要求提前估计趋势项把时间序列改造成监督学习的形态后它能自动处理非线性关系和多个外部特征而且训练速度比深度学习快一个数量级。这篇文章讲的RF时间序列预测本质上是一个思路转换把“按时间顺序排列的一列数”重新组织成“用过去一段窗口预测未来一个点”的表格结构然后交给回归森林去拟合。适合的场景是单变量或多变量时序、样本量在几千到几十万之间、特征里有明显的滞后相关以及你希望快速上线一个可解释的基线模型。在动手之前先把它的边界说清楚RF不具备天然的序列记忆能力它学的是窗口内的特征与目标之间的映射所以窗口怎么构造、数据怎么切分、预测时怎么滚动直接决定这个方案是能用还是翻车。2. 把时间序列改成监督学习滑动窗口与特征工程的三种建法2.1 滑动窗口原理滞后特征是RF唯一能理解的“时间”随机森林不认“时间”这个概念它只知道特征X和目标y。要让时序数据被RF消费唯一可靠的做法是把过去p个时刻的值作为特征当前时刻的值作为目标。这个p就是窗口大小也叫滞后阶数。窗口滑过整条序列后原来的1000个时间点就变成了1000-p1个样本每个样本有p个特征。这里有个容易被新手忽略的点窗口宽度决定了模型能看到多远的“历史记忆”。如果窗口设成3模型只能看到t-1、t-2、t-3三个时刻的值更早的信息全部丢失。而RF又不像LSTM那样有隐状态传递它每次预测都是独立的所以窗口基本等于它的全部记忆容量。实操里我一般先看数据的自相关图ACF找到自相关系数衰减到不显著的那个滞后阶数再以它为中心尝试2~3个候选窗口而不是拍脑袋定一个数。2.2 三种特征构造方式单变量滞后、多变量外生、日期时间特征特征工程是RF时序预测里比模型选择更重要的环节。常见做法是至少构造下面三类特征按业务场景取舍。单变量滞后特征是基础就是在目标变量自身的历史值上做平移。比如预测明天销量就把昨天、前天、上周同一天的值都作为特征列。特别注意“上周同一天”这类周期性滞后它能帮RF学到周度节奏效果往往比单纯拉大窗口更明显。多变量外生特征是指那些影响目标但本身也需要被记录的变量比如天气温度、促销标记、节假日标记。RF对这类特征的处理非常友好不需要标准化不需要做非线性变换它能自动找到外生特征和目标之间的阈值切分。唯一要注意的是这类特征在预测未来时必须是已知的或可预估的否则训练时用得到、预测时拿不到就会出“特征缺失”的尴尬。日期时间特征是把时间戳拆成小时、星期几、是否是月初月末、第几周等成分。对带有明显周期性的业务数据星期几这个特征几乎必加。我做过一个模拟项目X的流量预测只加一个星期几特征测试集RMSE就下降了15%左右比调任何模型参数都管用。原因是RF通过树的分裂能自动组合“星期几滞后值”的交互关系而这种组合在纯数值的滞后特征里是表达不出来的。2.3 数据集划分的讲究不能随机打散要按时间切分这是新手最容易踩爆的雷之一。做普通分类回归时train_test_split随机打乱数据没问题因为样本之间独立。但时间序列里t时刻的样本和t1时刻的样本共享了重叠的滞后窗口本质上是强相关的。如果随机打散测试集里会出现“用未来数据训练、拿过去数据测试”的情况测试指标会异常好看但上线后立刻打回原形。正确做法是按时间顺序切分前面80%的连续数据段做训练后面20%做测试。更严谨一点可以留出一段gap比如训练集截止到某个时间点测试集从更晚的时间点开始中间空出若干个周期避免训练集末尾的数据通过滞后窗口“泄漏”到测试集开头。我在用Python实现时一般这样切import numpy as np import pandas as pd from sklearn.ensemble import RandomForestRegressor # 假设 df 是单列时间序列按时间升序排列 values df[value].values # 定义滑动窗口函数 def create_window(data, window_size): X, y [], [] for i in range(len(data) - window_size): X.append(data[i:i window_size]) # 过去 window_size 个点 y.append(data[i window_size]) # 未来一个点 return np.array(X), np.array(y) window_size 7 # 用过去7天预测第8天 X, y create_window(values, window_size) # 按时间顺序切分前80%训练后20%测试 train_size int(len(X) * 0.8) X_train, X_test X[:train_size], X[train_size:] y_train, y_test y[:train_size], y[train_size:] model RandomForestRegressor(n_estimators300, random_state42) model.fit(X_train, y_train)这段代码里create_window函数把一维序列变成二维特征矩阵window_size7意味着模型每次看一周的历史。切分处用的是纯切片没有shuffle保证训练集全部在测试集的时间之前。random_state42是为了让树的抽样过程可复现调试阶段务必固定它否则每次跑出来的结果都不一样你会分不清是数据问题还是随机性的问题。3. 用Python跑通RF时间序列预测完整代码与数据说明3.1 模拟数据生成先跑通流程再换真实数据既然标题强调“完整源码和数据”我先给一段可以零依赖跑通的模拟数据生成代码。虽然真实业务数据各有各的形态但先用带趋势和周期性的模拟数据把流程跑通再替换成自己的数据是最不容易卡壳的路径。模拟数据我一般会叠加三部分线性趋势、正弦周期、随机噪声这样既能模拟真实业务数据的形态又能验证模型是否学到了趋势和周期性。import numpy as np import pandas as pd np.random.seed(0) n_points 1000 time np.arange(n_points) # 趋势成分 季度周期成分 噪声 trend time * 0.05 seasonal 10 * np.sin(2 * np.pi * time / 30) noise np.random.normal(0, 1, n_points) values trend seasonal noise df pd.DataFrame({ds: pd.date_range(2023-01-01, periodsn_points, freqD), value: values}) df.to_csv(simulated_series.csv, indexFalse) print(df.head())生成的数据共1000个点前800个做训练后200个做测试。正弦周期设成了30天模拟月度周期趋势项每天增长0.05噪声标准差约1信噪比接近101在这个强度下RF能学到大部分结构但不会拟合得完美适合用来验证预测流程。3.2 核心训练代码RF回归加滚动预测直接预测一个点相对简单但实际业务里我们通常要预测未来多步。多步预测有两种常见策略递归预测和直接预测。递归预测用已预测出来的值继续作为输入去预测下一步代码简单但误差会随步长累积直接预测是训练多个模型每个模型负责预测一个固定的未来时刻误差不累积但训练成本翻倍。下面这段代码先把单步预测的基础流程写清楚再手动实现递归多步预测改装成直接预测的思路也一并说明。import numpy as np import pandas as pd from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_squared_error, mean_absolute_error # 读取上一小节生成的模拟数据 df pd.read_csv(simulated_series.csv) values df[value].values def create_window(data, window_size): X, y [], [] for i in range(len(data) - window_size): X.append(data[i:i window_size]) y.append(data[i window_size]) return np.array(X), np.array(y) window_size 15 X, y create_window(values, window_size) # 时间顺序切分 train_size int(len(X) * 0.8) X_train, X_test X[:train_size], X[train_size:] y_train, y_test y[:train_size], y[train_size:] # 训练RF回归模型 rf RandomForestRegressor( n_estimators400, max_depth10, min_samples_leaf3, random_state42, n_jobs-1 ) rf.fit(X_train, y_train) # 单步预测评估 y_pred_one rf.predict(X_test) print(单步预测 RMSE:, round(mean_squared_error(y_test, y_pred_one, squaredFalse), 4)) print(单步预测 MAE:, round(mean_absolute_error(y_test, y_pred_one), 4)) # 递归多步预测预测未来 horizon 个点 def recursive_forecast(model, last_window, horizon): current_window list(last_window) forecasts [] for _ in range(horizon): pred model.predict(np.array([current_window]))[0] forecasts.append(pred) # 把预测值追加到窗口末尾并丢掉窗口最前面的旧值 current_window.append(pred) current_window.pop(0) return np.array(forecasts) last_window X_test[-1] # 以测试集最后一段窗口为起点预测未来30天 multi_step_forecast recursive_forecast(rf, last_window, horizon30) print(未来30天预测值前5个:, multi_step_forecast[:5].round(2))逻辑上recursive_forecast做的事情是先把测试集最后一组窗口取出来预测出下一个点然后把这个预测值塞进窗口末尾同时把最老的一个值丢掉窗口长度保持不变再继续预测下一个点。这样连续30步后得到的就是未来30天的预测曲线。参数说明n_estimators400给了足够多的树来稳定预测max_depth10限制了单棵树的深度防止树无限分裂而记住训练集噪声min_samples_leaf3保证每个叶子节点至少3个样本降低过拟合。n_jobs-1让所有CPU核心并行训练。这三组参数搭配是我在类似数据规模下的起步配置不是最优配置但足够给你一个能看的基线。3.3 预测结果可视化训练集拟合vs测试集泛化模型训练完成以后光看RMSE还不够必须画出曲线来观察预测值和真实值的贴合度。时序预测里最常见的视觉问题是“预测曲线比真实曲线滞后一拍”这在RF里尤其容易发生——当窗口内的lag_1特征权重太高时模型本质上是在复制上一个观测值。用图能快速发现这类问题下面这段代码同时画出训练集拟合、测试集预测以及未来30天的递归预测曲线import matplotlib.pyplot as plt plt.figure(figsize(12, 5)) # 测试集预测对比 test_indices np.arange(len(y_test)) plt.plot(test_indices, y_test, label真实值, colorblack, linewidth1.5) plt.plot(test_indices, y_pred_one, label单步预测, colorblue, alpha0.7) # 未来30天递归预测 future_indices np.arange(len(y_test), len(y_test) 30) plt.plot(future_indices, multi_step_forecast, label递归预测30天, colorred, linestyle--) plt.legend() plt.title(RF时间序列预测结果) plt.xlabel(时间相对索引) plt.ylabel(数值) plt.grid(alpha0.3) plt.show()画图时留意两个部位一是测试集前半段单步预测是否紧跟真实值如果紧跟但延迟一拍说明lag_1主导了预测二是递归预测那一段误差扩散的速度有多快曲线是否快速发散或收敛成一条平线。如果递归预测趋于平线说明模型学到的窗口内“动量”不足预测越远越回归均值这时优先检查窗口大小和周期性滞后特征而不是盲目调参。4. RF时间序列预测常见问题与避坑5个必踩的坑4.1 坑一数据泄漏——特征缩放器提前fit了全量数据现象训练集RMSE低得离谱但测试集预测曲线几乎是一条横线泛化性能完全不行。原因有人习惯先做标准化再切分数据于是scaler.fit()用了全量数据的均值和方差。测试集的信息在训练前就被模型间接看到了这叫数据泄漏。虽然RF本身不依赖特征缩放但如果你和数据标准化流程一起用就容易无意中把测试集信息泄漏进训练流程。解决任何需要fit的操作——标准化、PCA、缺失值填充的统计量——都必须在切分之后、只用训练集的部分去fit然后transform训练集和测试集。时序数据里更稳妥的做法是先按时间切分再做任何预处理。4.2 坑二随机打散训练集导致时间顺序错乱现象测试集上单步预测准确率很高RMSE比预期小很多但换到递归预测误差急剧膨胀。原因训练时用train_test_split默认的shuffleTrue打散了样本顺序。窗口样本之间高度重叠随机打散等于把大量信息重复暴露给模型测试集里混进了与训练样本几乎相同的序列片段。网络上的教程代码不少这么写照抄就会莫名其妙地得到一个“完美模型”上线后立刻崩塌。解决切分必须按原始时间顺序。手动切片或用TimeSeriesSplit交叉验证任何场景下都不要对时序数据做随机shuffle。4.3 坑三窗口大小设成默认值或拍脑袋预测变成“延迟复制”现象预测曲线形状和真实曲线几乎重合但整体向右平移了一天或一个周期预测值等于上一个真实值。原因窗口设得太小比如只用过去1~3个点lag_1特征在训练中占绝对主导地位树的所有分裂都在围绕lag_1转。预测时模型学到的本质是“最有用的信息就是上一个点的值”于是输出近似等于输入序列的延迟复制。解决窗口至少包含一个完整的业务周期比如日粒度数据至少取7天周粒度数据至少取4~5周。同时可以加入滞后7、滞后14、滞后30这类季节性滞后特征。判断方法很简单画出预测和真实值的曲线如果预测看起来像真实值向右平移了1格就是窗口太小或滞后特征太单一了。4.4 坑四递归预测时把未来的真实值当历史值喂进去现象多步预测前几步很准但随后误差越来越大甚至发散到完全脱离数据范围。原因递归预测每一步都在用上一步的预测值作为输入。如果代码里写的是用真实值去更新窗口——比如训练时用y_test里已有的真实值做下一步的输入——那你在测试阶段“偷看”了未来数据前几步当然准。但线上环境只有预测值可用误差就暴露了。解决多步预测必须坚持一个原则——进入窗口的每一步都是模型自己预测出来的值。检查代码里recursive_forecast的调用方式确保循环里更新窗口用的是pred而不是真实值。4.5 坑五只信feature_importances_这个黑匣子指标现象特征重要性排名显示某些滞后特征权重很高删掉它们之后模型效果反而变好或者加了几个噪声特征后排名乱跳。原因sklearn的feature_importances_基于不纯度下降impurity decrease计算这个指标偏好取值更多的连续特征而且高相关特征会互相分摊重要性。在时序数据里滞后特征之间天然高度相关这个指标容易给出误导性的排序。解决额外做置换重要性permutation importance它衡量的是随机打乱某个特征后模型误差的上升程度更贴近真实预测贡献。在时序数据里做置换重要性时注意保持时间顺序不要用默认的随机交叉验证建议用TimeSeriesSplit。同时保留一个朴素基线做对比——比如“用上一个周期的值作为预测”RF如果跑不过这个基线说明它不是这个问题的最佳方案。5. 参数调优让RF从“能跑”到“预测得稳”的关键参数与搜索策略5.1 三个必调参数n_estimators、max_depth、min_samples_leafRF的参数比深度学习少很多但对时序预测而言真正值得花时间调的通常是这三个。n_estimators决定树的数量。默认100起步但时序数据噪声大、样本重叠多单棵树的方差会比较大我一般设到300~500。判断收敛的方法固定其他参数把n_estimators从50按100的间隔往上加观察测试集误差曲线。误差还在明显下降就继续加趋于平台期就停下来。不用过度追求几千棵树边际收益会趋近于零而且训练时间线性增长。max_depth限制树的深度。默认None不限制说明树可以一直分裂到叶子纯净为止这在时序预测里几乎必然过拟合。模拟数据或小样本业务数据上max_depth在5~15之间通常就够了。判断标准是看训练集和测试集的误差差——如果训练集RMSE明显小于测试集优先降低max_depth或增大min_samples_leaf。min_samples_leaf控制叶子节点的最小样本数。它比min_samples_split更直接地控制过拟合。时序数据里我常用min_samples_leaf3~10样本量低于几千时用小的几万以上时调到10~30。这个参数没有定式但一个可供参考的口令是“宁可让预测偏保守也不要让模型记住单次噪声”。5.2 网格搜索与滚动交叉验证的正确结合直接对时序数据做GridSearchCV是个常见错误因为默认的KFold随机切分会造成数据泄漏。正确做法是用TimeSeriesSplit替代KFold它按时间前后来划分多折数据每一折的训练集都在测试集之前。可以在sklearn里直接调用from sklearn.model_selection import TimeSeriesSplit, GridSearchCV tscv TimeSeriesSplit(n_splits3) param_grid { n_estimators: [200, 400], max_depth: [5, 10, 15], min_samples_leaf: [2, 5, 10] } grid GridSearchCV( RandomForestRegressor(random_state42, n_jobs-1), param_grid, cvtscv, scoringneg_mean_squared_error, verbose1 ) grid.fit(X_train, y_train) print(最优参数:, grid.best_params_)这里用neg_mean_squared_error作为评分指标因为GridSearchCV默认的R²比较粗糙通常不够灵敏。TimeSeriesSplit的n_splits不建议设太大时序数据相邻折之间重叠度高3~5折比较可靠超过5折容易让某些折的训练集过短。网格搜索跑完还会暴露另一个问题最优参数有时候在数据边界附近比如max_depth搜到上限15而15又是表现最好的。这种情况说明真正的更优值可能更大需要扩范围重搜一次而不是直接采用网格边界值。搜索结束后把最优参数在全部训练数据上重新fit一次再用测试集做最终评估。5.3 用置换重要性做特征筛选删掉噪声特征的判断标准特征筛选的目的是减少树的噪声来源、缩短训练时间有时还能小幅提升泛化效果。判断一个特征是否该删不看importance的绝对值而看它被随机打乱之后误差上升了多少。如果置换后测试误差几乎不变说明这个特征对预测没有实质贡献。在时序场景里滞后阶数很高的特征——比如窗口开到30天后最后那几列——往往贡献很小却会分散树对早期关键时刻的注意力。手动做法是每次删掉贡献最低的1~2个特征重新训练并对比验证集误差。如果误差不升反降说明删掉的确实是有害特征。一般执行两三轮之后就会到一个阈值期再往下删误差开始明显上升那个点就是特征数量的合理位置。from sklearn.inspection import permutation_importance # 基于已训练模型计算置换重要性 perm_result permutation_importance( rf, X_test, y_test, n_repeats10, random_state42, scoringneg_mean_squared_error ) feature_names [flag_{i1} for i in range(window_size)] importance_df pd.DataFrame({ feature: feature_names, importance_mean: perm_result.importances_mean, importance_std: perm_result.importances_std }).sort_values(importance_mean, ascendingFalse) print(importance_df.head(10))置换重要性给的是一个排序依据真正的删特征决策还得和业务可解释性结合。比如lag_1重要性最高完全合理但lag_14如果是周期性的关键点即便重要性排名靠后只要业务逻辑里“上周同期”有意义就值得保留。建模是手段业务规律才是底线。6. 验证模型的硬指标滚动回测与残差分析的实战习惯模型训练完了、参数也调过一轮接下来最关键的一步是验证。这里说的验证不是看测试集指标而是模拟线上真实使用方式做滚动回测。我常用的模式是从训练集末尾处开始每预测完一个点把这个点的真实观测值加入历史窗口窗口向前滚动一步再预测下一个点。逐个滚动完整个测试期最后汇总误差。这个过程模拟的是“昨天之前的数据我都知道我只需要预测明天”的实际线上状态。def walk_forward_forecast(model, history, test_values, window_size): current_history list(history[-window_size:]) predictions [] for actual in test_values: pred model.predict(np.array([current_history]))[0] predictions.append(pred) # 滚动更新加入真实值丢掉最早一个点 current_history.append(actual) current_history.pop(0) return np.array(predictions) history X_train[-1] # 训练集最后一段窗口 walk_pred walk_forward_forecast(rf, history, y_test, window_size) print(滚动回测 RMSE:, round(mean_squared_error(y_test, walk_pred, squaredFalse), 4))滚动回测的误差会比一次性预测更接近线上真实的误差水平因为每一步都在用真实历史做更新误差不会像递归预测那样逐级累积。判断模型好坏时我会拿这个滚动回测的RMSE除以目标变量的标准差得到一个类似“相对误差”的指标。小于0.5说明预测比直接用均值预测强很多0.5~0.8说明有改进空间大于1基本说明模型还没有业务可用性。最后养成的习惯是画残差图。把预测残差画出来看是否还有明显的周期性或趋势性。如果残差里还残留着形状说明模型漏掉了一些结构性信息——通常是周期性特征没构造够或者窗口没覆盖完整周期。如果残差近似白噪声就可以大胆把方案推向实际业务。我在第一个项目里就是漏了周周期特征残差图上一眼能看到7天的波浪形补上滞后7特征后模型才真正可用。这个经验让我每次换数据集都先看残差图再动参数。希望帮到你。本文还有配套的精品资源点击获取