ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于趋势与季节性的时间序列预测:拆解、建模与实战

基于趋势与季节性的时间序列预测:拆解、建模与实战 简介面向气候预测与时间序列分析学习者这套Python实战资源包以趋势与季节性为主线聚焦含明显周期波动和长期走向数据的建模预测问题。压缩包共9个文件核心为3个Jupyter Notebook配套1个CSV日最低气温数据集并含若干XML配置与工程文件总大小仅2.95MB无需额外大体积环境即可直接运行。内容从Pandas滚动平均提取趋势开始逐步过渡到STL季节性分解、ARIMA/SARIMA模型搭建、网格搜索自动调参以及基于真实温度数据的训练集切分、MSE/RMSE/MAE误差评估和预测结果可视化形成完整闭环同时提供合成数据集版本便于对照理解模型行为。已有528人学习下载非常适合希望从零快速掌握气候预测中时间序列建模流程并能动手验证各步骤原理的数据分析师与气象爱好者。1. 基于趋势和季节性的时间序列预测先拆解再建模才是真正的实战时间序列预测最常翻车的点不是模型不够强而是很多人直接把原始序列丢给 LSTM 或 ARIMA忽略了一个关键前提序列里的趋势和季节性决定了你要用哪种建模方式、要设什么参数、预测结果能达到什么精度。趋势是长期缓慢的上升或下降季节性则是固定周期内反复出现的波动。把这两者先拆开、单独建模、再合回预测值通常比一股脑端给黑箱模型更稳也更容易解释。这套思路适合电商销量、服务器流量、工厂能耗这类既有日/周/月周期又有长期增长或衰减的业务数据。你不需要跑一次实验就换一种模型而是先做好分解这一步后续选器、调参、上线监控才有章可循。实战包里那些“跑不出来”“预测永远是一条直线”的问题多半就出在把趋势和季节性搅在一起处理。2. 趋势与季节性拆解的三层做法从统计分解到业务特征2.1 STL 分解先看数据里到底有什么成分拿到序列后我的习惯是先用 STL 做一次分解把原始序列拆成趋势项、季节项和残差项。这一步不是为了直接拿残差去做预测而是为了摸清数据的脾气看趋势是单调递增还是有拐点季节性是否稳定残差里是否还藏着周内效应或节假日脉冲。常见做法是用 statsmodels 里的 STL 方法核心参数有两个period 和 seasonal。seasonal 决定季节项的光滑程度设置太大会把趋势吃掉period 必须符合业务周期不能拍脑袋写 7。比如日粒度电商数据周期是 7 天小时粒度服务器流量周期是 24。import pandas as pd import matplotlib.pyplot as plt from statsmodels.tsa.seasonal import STL df pd.read_csv(orders.csv, parse_dates[date], index_coldate) df[orders] df[orders].astype(float) stl STL(df[orders], period7, seasonal13, robustTrue) result stl.fit() fig result.plot() fig.set_size_inches(12, 8) plt.show() trend result.trend seasonal result.seasonal resid result.resid逻辑说明STL 通过局部加权回归反复迭代把趋势和季节性交替估计出来。robustTrue 会在迭代中给极端值降权避免双十一这种异常销量把季节项拉变形。代码里 period7 对齐业务周周期seasonal13 表示季节项平滑窗口宽度为 13 个观测点窗口越宽季节曲线越平缓。参数说明如果你的数据是月度粒度seasonal 一般取 7 到 15 之间如果是日粒度周周期取 11 到 15 比较安全。seasonal 太小会出现季节项里混入噪声太大又会把趋势的拐弯处吸走。判断标准很简单画完图后看 trend 是否还能保留转折seasonal 是否周期性清晰可见。2.2 可解释性更强的做法分离趋势作为特征而不是当噪声STL 分解适合分析和可视化但直接拿 trend 和 seasonal 当训练特征时会遇到一个时序预测里的通病分解组件在预测区间上不存在必须外推。很多实战包里会直接 fit 一个 STL 到全量数据把 fitted 的 trend 当特征喂给模型到了预测阶段才发现 trend 没有未来值只能用线性外推硬补结果一塌糊涂。我一般会做两步转换。第一步是显式构造趋势特征用时间戳自带的连续变量比如从起点到当前日期的天数或者序号。第二步是把季节性编码成周期性特征用正弦和余弦函数对周期索引做傅里叶展开这样模型能学到“第几小时”或“第几周”的循环平滑关系。import numpy as np df[t] np.arange(len(df)) df[week_sin] np.sin(2 * np.pi * df.index.dayofweek / 7) df[week_cos] np.cos(2 * np.pi * df.index.dayofweek / 7) df[month_sin] np.sin(2 * np.pi * df.index.month / 12) df[month_cos] np.cos(2 * np.pi * df.index.month / 12)逻辑说明序号变量 t 让模型学到单调变化趋势傅里叶特征让模型学到周期性相似度。判断一个周期特征编码是否合理看预测曲线上周期衔接处是否平滑即可。sin 和 cos 的组合能把 dayofweek0 和 dayofweek6 之间的距离拉近避免把周一和周日当成完全不相关的两个类别。参数说明7 对应星期周期12 对应月份周期这是最常见的两组。如果你做小时级流量预测周期是 24那就把 24 换成 7 和 12 对应位置的除数即可。如果序列同时存在日周期和年周期也可以叠加多组傅里叶特征但要注意列数不要超过训练样本数的十分之一否则模型容易过拟合。2.3 趋势和季节性的常见误用直接把差分当处理完不少人在实战里会用差分来“去掉趋势和季节性”一阶差分去趋势七阶差分去周季节性。这个思路在处理非平稳序列做平稳性检验时没问题但放在预测建模里有个副作用差分后的序列会放大噪声预测误差会在逆差分时被逐级累积放大。我倾向的做法是保留原始数值把趋势和季节性作为显式特征建模让模型自己学习如何组合它们。这样预测结果天然保持在原始量纲上评估指标和业务口径都是一一对应的。只有在用 ARIMA 这类需要平稳性的模型时差分才是必要的。from statsmodels.tsa.stattools import adfuller result adfuller(df[orders].dropna()) print(fADF p-value: {result[1]:.4f}) result_diff adfuller(df[orders].diff().dropna()) print(fADF p-value (diff): {result_diff[1]:.4f})逻辑说明ADF 检验的零假设是序列存在单位根p 值小于 0.05 就认为序列平稳。上面第一句是在检验原始序列第二句是在检验一阶差分后的序列。如果你需要给 ARIMA 定阶这个结果能确认差分数目如果你走的是特征工程路线这个结果只是告诉你是否需要额外处理趋势。参数说明diff() 不传参数就是一阶差分。如果你发现一阶差分后序列仍有年度趋势再考虑 add 一个周期差分层或者把时间序号特征的高阶项平方、三次加进去。切忌在一阶差分不平稳时盲目叠加差分阶数差分的阶数每加一预测误差的方差会显著膨胀。3. 构建预测模型的三个落地方案从基线到深度学习3.1 基线与快速验证用回归模型搭一套可解释的强基准做趋势和季节性的预测我建议不要一上来就调 LSTM。先用线性回归或者 LightGBM 这类特征模型跑一版既能快速验证趋势特征和周期特征是否抓准也能当后续复杂模型的对比基线。很多时候这个基线的效果就已经能支撑业务使用了没必要求大求全。这里提一个实战里的铁律时间序列预测不能使用普通 K 折交叉验证否则未来信息会泄漏到训练集里验证指标虚高上线后直接翻车。需要用时间顺序切分训练集永远在验证集之前。from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_absolute_error df[lag_1] df[orders].shift(1) df[lag_7] df[orders].shift(7) df[rolling_mean_7] df[orders].shift(7).rolling(7).mean() train df.iloc[:-14] test df.iloc[-14:] features [t, week_sin, week_cos, month_sin, month_cos, lag_1, lag_7, rolling_mean_7] model LinearRegression() model.fit(train[features], train[orders]) pred model.predict(test[features]) mae mean_absolute_error(test[orders], pred) print(fMAE on test: {mae:.2f})逻辑说明lag_1 和 lag_7 捕捉短期自相关和上周同一天的水平rolling_mean_7 做了平滑处理降低单日波动对预测的干扰。线性回归虽然简单但这里的关键是特征构造是否贴合趋势和季节性的业务含义模型本身只是做一个线性组合。预测结果出来后如果 MAE 比均值预测还差那问题一定出在趋势和季节特征构造或者滞后项对齐上。参数说明train 用 iloc[:-14] 把最后 14 天留给测试是因为后续模型要预测两周的数据。lag_1 和 lag_7 同时放入时要注意共线性如果效果不升反降可以先去掉 lag_1 只保留 lag_7。rolling_mean_7 的窗口要和周期一致不然只是多引入了一个噪声特征。3.2 用 Prophet 做趋势和季节性的联合建模业务参数怎么设如果你不想手搓傅里叶特征又希望模型自动处理趋势突变和节假日效应Prophet 是当前落地效率比较高的选择。它本身就把趋势项和季节项分开建模趋势用分段线性或逻辑斯蒂增长拟合季节项用傅里叶级数逼近这正是标题所指向的“基于趋势和季节性”的核心思路。Prophet 的参数里最值得花时间调的是 changepoint_prior_scale 和 seasonality_prior_scale。前者控制趋势拐点的灵活度数值越大模型越容易拟合突发增长或断崖下降后者控制季节项的平滑程度数值过大会把季节项拉平过小则会把噪声当作周期。from prophet import Prophet df_prophet df.reset_index() df_prophet df_prophet.rename(columns{date: ds, orders: y}) model Prophet( yearly_seasonalityFalse, weekly_seasonalityTrue, daily_seasonalityFalse, changepoint_prior_scale0.05, seasonality_prior_scale10, ) model.add_seasonality(namemonthly, period30.4, fourier_order5) model.fit(df_prophet) future model.make_future_dataframe(periods14, freqD) forecast model.predict(future) model.plot(forecast)逻辑说明weekly_seasonalityTrue 会默认用傅里叶级数拟合 7 天周期add_seasonality 再补充月度周期用于捕捉类似月末冲量或账单周期类规律。changepoint_prior_scale0.05 的意思是允许趋势在历史数据中出现拐点默认值 0.05 适用于大多数业务场景seasonality_prior_scale10 表示对季节项的约束比较宽松允许季节形状变化。参数说明period30.4 是平均月长天数fourier_order5 控制月度季节项的复杂度值越大拟合越精细但泛化能力会下降。如果你的数据里存在大促这类强事件可以将 changepoint_prior_scale 调到 0.1 到 0.3 之间试探效果。注意 Prophe t输出的 yhat 是原始量纲的预测值yhat_lower 和 yhat_upper 是不确定性区间业务汇报时一般建议直接用 yhat不要过度解读区间范围。3.3 用 LSTM 做序列预测数据处理和训练要点LSTM 在这类任务里的真实定位是当你发现线性模型和 Prophet 的误差停留在某个平台且残差里仍存在明显的非线性模式时才值得引入。LSTM 的优势是能直接消费原始序列做端到端预测但这恰恰也是最容易出错的地方很多人把序列长度设置成 7 或者 30但这往往只是照抄经验值不是从数据里探出来的。我一般会在进入 LSTM 之前先对序列做窗口扫描分别测试 input_window 在 7、14、28 下的验证集误差选定窗口后再构建样本。另外必须做归一化而且归一化的 fit 只能用在训练集上防止验证集信息提前进入 scaler。import numpy as np from sklearn.preprocessing import MinMaxScaler from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense values df[orders].values.reshape(-1, 1) scaler MinMaxScaler(feature_range(0, 1)) scaled scaler.fit_transform(values[: -14]) window 14 X, y [], [] for i in range(window, len(scaled)): X.append(scaled[i - window: i]) y.append(scaled[i]) X np.array(X) y np.array(y) split int(len(X) * 0.8) X_train, X_val X[:split], X[split:] y_train, y_val y[:split], y[split:] model Sequential() model.add(LSTM(32, activationtanh, input_shape(window, 1))) model.add(Dense(1)) model.compile(optimizeradam, lossmse) model.fit(X_train, y_train, validation_data(X_val, y_val), epochs30, batch_size64, verbose1)逻辑说明每个训练样本是一个 window 长度的时间窗口形状是 (window, 1)1 表示单变量特征。LSTM 层用 32 个隐单元输入维度与窗口一致输出层直接输出归一化后的下一个值。训练时把最后 14 天的原始数据排除在 scaling 之外防止 MinMaxScaler 因为见到未来最大值而把历史数据压扁。参数说明window14 表示用过去两周预测下一天适合周周期明显的日粒度数据。batch_size64 控制每次参数更新的样本量数据量小时可以降到 16。epochs30 是一个保守起点观察 val_loss 连续 5 轮不下降就回调提前终止。激活函数默认 tanh 用于 LSTM 内部状态更新Dense 层不配激活函数是因为这是回归任务。预测时把最后一个窗口的真实值作为输入每次预测出的新值追加到窗口末尾滚动预测 14 天。这里有个血泪经验不要用预测出来的值反复作为下一轮的输入超过 7 步误差会按指数叠加超过 7 步后预测曲线通常会退化成一条平线。如果你确实需要长周期预测建议用迭代预测的中间结果做一次残差校正或者直接切换到 Prophet 这类全局模型。4. 参数取舍与调优路径从数据尺度到训练策略的选择4.1 趋势跨度与季节性周期的参数联动在趋势和季节性拆解类实战中参数不是孤立的。趋势跨度影响你要用多少历史数据来拟合季节性周期则决定傅里叶特征或滞后项的间隔。最常见的翻车就是把两者各调各的趋势选 90 天季节周期却写 7最后模型对近期拐点反应迟钝。我的原则是模型训练窗口至少包含 3 个完整季节性周期。日粒度周周期训练集至少 21 天月粒度年周期至少 3 年。短了季节性模式学不完整长了趋势项会把遥远的旧模式强行带入拖慢对最近业务变化的响应。df[week_of_year] df.index.isocalendar().week print(df.groupby(week_of_year)[orders].std().sort_values(ascendingFalse).head(10))逻辑说明这行代码用来量化季节性强度。按周序号分组计算标准差如果不同周之间的标准差差异很大说明季节性在大幅波动如果差异很小说明季节性趋于稳定建模时不需要给季节项太高自由度。实战中建议保守一点先看数据的内在周期个数再决定训练长度和季节特征阶数而不是直接套一个全网通用的 window。参数说明isocalendar().week 是 pandas 1.1 以后的标准用法返回 ISO 周序号跨年时不会出现 0 和 53 的混淆。这个检查适合周季节性显式的数据月粒度数据就换成 df.index.month。4.2 调节平滑参数和鲁棒性参数的方式无论是 STL 系数的平滑窗口还是 LSTM 训练周期背后都存在一个权衡参数调得越大曲线越平滑对近期变化的敏感度越低调得越小拟合越激进验证集误差可能先降后升。具体调参时不要追求验证集 MAE 无限低要同时观察训练集和验证集的误差gap。实战中我会跑一个简单网格对比几个候选参数下的验证集误差然后选择误差相对稳定、预测曲线不抖动的那个。不推荐单独依赖自动化调参工具一次跑完因为时间序列中连续参数之间存在非线性耦合。import itertools seasonal_candidates [7, 11, 13, 15] best_params None best_mae float(inf) for seasonal in seasonal_candidates: stl STL(df[orders], period7, seasonalseasonal, robustTrue) result stl.fit() maes [] for i in range(7, 14): pred result.trend[-1] result.seasonal[-i] actual df[orders].iloc[-i] maes.append(abs(pred - actual)) avg_mae np.mean(maes) if avg_mae best_mae: best_mae avg_mae best_params seasonal print(fBest seasonal: {best_params}, repeated MAE: {best_mae:.2f})逻辑说明这里用一个朴素策略评估不同 seasonal 参数下分解结果对最近一周回看的预测能力。预测值用最后一个趋势值加对应时序的季节项虽然粗糙但足以反映分解参数是否让季节项稳定到可以用来预测的程度。真正的建模阶段会用完整训练集重拟合但参数选型阶段这个做法足够快速有效。参数说明maes 循环从 7 到 13 共 7 个点对应一周回看长度实际上是用历史最后 7 天检验分解质量。这种方式不适合在线更新参数只是给你一个参数选择的数理依据避免纯凭感觉拍参。4.3 模型训练中三个值得优先调试的数据处理参数时间序列实战里模型结构往往不是瓶颈数据处理参数才是。第一个是滞后阶数偏自相关函数会在哪个 lag 上截尾第二个是归一化范围MinMax 的上下界是否被离群点拉爆第三个是训练验证切分比例验证集是否包含完整季节性周期。from statsmodels.tsa.stattools import pacf pacf_values pacf(df[orders].dropna(), nlags20) for lag, value in enumerate(pacf_values[1:], start1): if abs(value) 0.2: print(fLag {lag}: {value:.3f})逻辑说明PACF 用于检测序列在滞后阶数上的直接相关性超过阈值的 lag 就是值得放入特征的滞后边。比如 lag_7 的系数显著说明上周同一天对今天有独立预测力应当保留。lag 1 显著则是典型的短期惯性。超过 0.2 阈值的 lag 越多说明序列依赖更多历史信息LSTM 这类模型的价值就越大。参数说明nlags20 表示计算 20 阶偏自相关日粒度周周期数据一般 20 阶足够月粒度数据建议设置 nlags36。阈值 0.2 不是硬标准如果数据量少可以放宽到 0.15但不要低于 0.1否则会把纯噪声当趋势带进模型。5. 趋势和季节性预测的避坑清单五个现象、原因与解法5.1 预测期末端出现“断崖或尖刺”现象预测曲线在最后几天突然掉头向下或直线冲高。原因通常是趋势项和季节项在预测区间边界上相位错位或者 LSTM 滚动预测时误差累积触发了极端输出。解决检查训练集末尾是否包含异常值比如大促或补货造成的单日脉冲把序列末尾三天的数据用中位数平滑再进模型预测出结果后也不要直接交付先做平滑后处理。df[orders_smoothed] df[orders].rolling(3, centerTrue, min_periods1).median()逻辑说明rolling median 比均值更抗离群点centerTrue 保证平滑值时序不偏移。预测前用平滑序列替代原始序列建模能显著降低点尾效应。注意在最终评估时还是要用原始值计算误差平滑只是为了稳定模型训练。5.2 验证集误差低、上线误差高现象离线测试 MAE 很好看一上线就偏差很大。原因多数是验证集切分不合理测试区间正好落在季节性高点或低点而模型在训练阶段没见过类似位置。解决用滚动时间序列验证多次切分取平均不要只凭一次留出法下结论。验证集应至少包含两个完整周期或者采用扩展窗口训练、滚动预测的方式模拟线上行为。5.3 差分后预测无法还原现象用了差分平稳化逆差分后误差被放大到不可接受。原因在于误差在差分的累积反转过程中会多次放大尤其在多步预测时更严重。如果你不是在做 ARIMA 定阶研究建议优先把趋势和季节项作为特征建模而不是对原始序列做差分。这属于模型选型问题不是调参能解决的。5.4 季节性周期设定错误现象分解结果看不出固定周期或季节项和趋势项纠缠不清。原因通常是 period 设定与实际业务周期不一致。比如日粒度数据有月度冲量你就不能只设定 period7要增加 period30 的月度季节项小时级数据同时存在 24 小时和 168 小时周期只保留其中一个残差里就会出现明显的条纹状噪声。解决方式是先做频谱分析确认主周期再回头修正 period。周期识别错误会让后面所有特征工程失去方向这也是先把数据拆开看的原因。5.5 LSTM 预测逐渐退化成均值线现象迭代预测超过一定步数后输出趋向于训练集的均值周期细节完全消失。原因在于多步递归时误差不断叠加模型最终只能输出一个最保守的中间值。解决方法是改用直接多步预测即输出层用多个神经元一次预测未来 N 天或者在 LSTM 后面接一个线性输出层分步预测再拼接。如果业务只关心未来一天则保持单步预测即可不要盲目追求预测长度。6. 验证预测质量的三个手段残差分析、反向测试与对比基线预测模型上线前只看 MAE 是不够的。我一般会额外跑三个验证手段。第一残差分析把预测残差对时间序号作图如果残差里还有明显的周期性条纹说明季节项没提取干净回去调整傅里叶阶数或 STL 周期。第二反向测试用训练集末端的某个历史节点做起点模拟当时的模型输入并预测“未来”然后和真实历史对比这样能验证模型对拐点和周期变化的真实响应能力。第三基线对比始终在报告里留一行“用上一周期实际值直接当预测”的误差如果复杂模型连这个都赢不了就不该上线。import matplotlib.pyplot as plt resid test[orders] - pred plt.figure(figsize(10, 4)) plt.plot(resid) plt.axhline(y0, colorr, linestyle--) plt.title(Residual over time) plt.show()逻辑说明残差图中如果出现与周期等距的锯齿状波动说明模型没有完整捕捉到季节性。比如 7 天周期的数据残差每隔 7 点出现一个峰意味着周季节模式的幅度或形状在训练集和测试集之间发生了偏移。此时优先检查傅里叶阶数是否过低或者 STL 的 seasonal 参数是否过小。第二个反向测试的具体做法是从训练集倒数第 14 天处截断只用更早的数据重新训练然后用同样的时间顺序预测后面 14 天与真实值对比。这个流程模拟的是“站在过去预测现在”的场景比单纯对保留集预测更能暴露数据泄漏和滞后特征带来的乐观偏差。注意反向测试和正式验证使用同一套参数不要调两次参。参数层面还有两个实际经验值得记录预测输出的整数化处理不要把小数直接四舍五入对于销量类数据业务上通常期望预测值带一位小数方便看趋势但对于容量规划类场景则要向上取整保证资源充足。另一个是数据异常检测不能只看预测偏差还要关注真实值的采集稳定性尤其当上游数据链路出现延迟补报时预测模型会把缺失当成真实下跌来学习。最后每轮模型迭代后我会把自己调过的参数和验证结论记在一张表里包括训练窗口、周期设定、seasonal 参数、滞后阶数和验证误差。这张表比代码本身更宝贵。没有它三个月后再看同一个实战包你只记得当初调参调得很痛苦却不知道哪个参数救了你。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进