ARTICLE DETAIL

资讯详情

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

时间序列预测实战:趋势与季节性分解的Python实现与避坑指南

时间序列预测实战:趋势与季节性分解的Python实现与避坑指南 简介这套实战资料面向具备一定Python基础、希望掌握趋势与季节性时间序列建模的气候预测学习者也适合作为数据分析课程的案例补充。内容从数据探索出发完整覆盖趋势提取、季节性分解、模型选择与评估、预测可视化等环节并以合成数据集和真实日最低气温数据集分别给出两套Notebook实践便于对比不同数据特性下的建模思路。压缩包共9个文件包含3个ipynb主程序、温度csv数据集以及xml项目配置和iml模块文件等整体仅2.95MB轻量易用下载后即可对照运行。已有528人学习下载。通过其中案例可系统学习Pandas滚动平均识别趋势、STL分解季节性成分、ARIMA/SARIMA建模与网格调优并借助MSE、RMSE、MAE等指标评估预测精度同时还能掌握时间序列训练集切分与可视化表达方法。对农业规划、能源调度、灾害预警等气候敏感领域这些代码和思路具备直接迁移与二次开发价值。1. 基于趋势和季节性的时间序列预测拆开再合上比直接上模型稳得多很多团队做销量或流量预测时第一反应就是把数据直接丢给 LSTM 或 XGBoost训练集上漂亮上线一两个月就翻车。回头补做基于趋势和季节性的时间序列预测把序列拆成趋势、季节、残差三块分别处理反而稳定得多。这个方案解决的是大多数业务预测的真实问题日活、销量、工单量这类数据往往同时带着明显走势和固定周期模型需要的不是记住历史而是把这两股力量拆清楚再各自外推。适合数据分析师、后端工程师以及所有要给周报月报里的 KPI 做预判的人。接下来我会把背后的原理、完整代码、参数设置和踩坑经历一次讲透。2. 趋势和季节性为什么要拆开预测分解原理与工具选型直接对原始序列建模时模型要同时识别两件事长期往哪走、周期怎么波动。数据量大、信号干净时它能学会一旦数据短或者噪声大这两股力量就会互相干扰。分解后再预测每个模型只处理一种结构难度大幅降低。这也是为什么 Holt-Winters、SARIMA 这类统计模型内部本质上都内置了趋势和季节两个组件只是黑匣子程度不同而已。2.1 趋势和季节性的定义加法模型与乘法模型怎么选时间序列里的趋势trend指长时间的方向性移动季节性seasonality指固定周期内的重复形态。这两者叠加方式不同分解的数学写法也不同。加法模型写为 y_t T_t S_t R_t意思是趋势和季节分别对原始值贡献一个绝对量乘法模型写为 y_t T_t × S_t × R_t季节贡献按比例缩放。判断方法不需要公式画图看季节波动的幅度是否随趋势变化就行。比如某产品销量从日均 100 涨到 10000如果每周六的峰值始终比平时高固定数值用加法如果峰值始终是平时的 1.5 倍就得用乘法。实际业务数据里乘法形态更常见。零售、电商、餐饮的旺季波动几乎都随体量放大绝对量预测在这种场景下偏差会越来越大。处理乘法模型有个取巧办法先对序列做 log 变换乘法关系就变成加法关系后面所有加法分解的代码直接复用。这是最快的落地路径也是我处理量级跨度大时的默认选择。模型季节幅度典型场景处理方式加法绝对量固定温度、水电用量直接分解乘法随水平缩放销量、流量、GMVlog 变换后按加法分解2.2 经典分解与 STL两个工具的优缺点对比statsmodels 里有两套现成分解工具seasonal_decompose 和 STL。经典分解的思路是先用移动平均把趋势抠出来从原序列里减掉趋势剩下季节加残差再按周期逐位置平均得到季节分量。实现简单但有两个明显短板移动平均在序列两端各丢一段数据季节分量对异常值非常敏感一个脉冲就能让某个周期位置的平均值偏掉。STL 用局部加权回归替代移动平均robustTrue 时异常值会自动降权季节形态也允许缓慢变化不需要每年强制一样。代价是参数变多period、seasonal、trend、low_pass 都要人工指定默认值不一定适合你的数据。我这里有个对比表方便按项目情况选维度经典分解STL核心方法移动平均 逐位置平均loess 局部加权回归异常值鲁棒性差一个毛刺带偏整年好robust 可降权端点缺失趋势两端各丢一半窗口趋势两端缺失季节完整季节形态固定不变允许缓慢漂移参数数量只需周期period、seasonal、trend 等我的经验是日常做探索性分析直接用 STL只有数据量很小、要严格复现经典分解流程时才退回 seasonal_decompose。处理日粒度数据时 trend 窗口没调好分解出来的趋势会跟着噪声一起跳比不分解还难用。2.3 趋势该用什么模型拟合线性、多项式与阻尼趋势分解出来趋势分量通常很平滑最常见做法是用线性回归拟合时间编号。但有三个细节值得注意。第一线性外推默认未来趋势斜率不变。业务指标大多有天花板某产品不可能永远每月涨 5%预测未来一两周还好预测半年以上就该加阻尼。阻尼趋势的做法是给每一步的斜率增量乘一个 0.8~0.98 的衰减系数趋势逐渐弯向水平线这是实践中长期外推最稳的方式。第二多项式看着拟合度高但外推极不稳定。二次项预测窗口稍长就剧烈弯曲三次以上基本没法用。看到有人拿高次多项式拟合趋势还引以为傲基本可以判断没吃过亏。第三拟合趋势前先处理 NaN。STL 的趋势分量在序列两端有一定长度缺失直接 fit 会报错或把 NaN 当有效数据。这种细节最容易浪费半小时。2.4 季节性预测最容易误用的场景多周期与稀疏序列季节性不一定只有一个周期。外卖数据同时有一天内和一周内两个周期零售数据同时有周周期和年周期。STL 一次只接受一个 period拿 period7 分解年季节还留在残差和趋势里拿 period365 分解周季节又会被平滑掉。常见做法是两级分解先用大周期分解把趋势和年季节拿掉再对剩余部分做小周期分解。周期复杂或数据量大时tbats 这类多季节性模型更省事但解释性不如拆开做。稀疏序列是另一个翻车点大量日期销量为 0比如只周末营业的门店。STL 的局部加权回归会把零值拉低季节均值分解出的季节分量整体偏下。解决思路是先把是否营业做成哑变量分离出去只对营业日的销量做分解预测最后再合成。零值占比超过三成时这条路几乎是必须走的。3. 用 Python 跑通趋势季节性预测分解、拟合与合成的完整代码下面这套流程假设你手里有一份日粒度数据 sales.csv两列date 和 y带明显周季节。目标是用趋势和季节性分解预测未来 14 天。整个过程分成四步分解、趋势建模、季节外推、合成结果。3.1 第一步读入数据并做 STL 分解import pandas as pd import numpy as np from statsmodels.tsa.seasonal import STL import matplotlib.pyplot as plt # 读入日粒度数据date 列解析成索引 df pd.read_csv(sales.csv, parse_dates[date], index_coldate) y df[y].asfreq(D) # 强制日频率缺失日期补 NaN # 业务周期是周period 设 7robust 提高异常值鲁棒性 stl STL(y, period7, robustTrue, seasonal13, trend21) res stl.fit() trend res.trend # 趋势分量 seasonal res.seasonal # 季节分量 resid res.resid # 残差 # 画出来检查分解是否合理 res.plot() plt.show()STL 的 period 是核心参数日数据带周季节就设 7月度数据带年周期设 12设错后面全歪。robustTrue 会让局部加权回归对异常值自动降权如果序列里有大促或故障导致的极端值这一个参数能救回整条季节曲线。seasonal 和 trend 是平滑窗口必须为奇数默认值也能跑但噪声大时把 trend 调大到 21 或 31趋势会更平滑后续拟合更稳。分解完成后先看三张子图趋势是否单调平滑季节是否呈现清晰的 7 天起伏残差是否在零附近随机分布。如果残差里还能看出周期痕迹说明 period 或窗口参数不对先调回去再往下走。3.2 第二步趋势分量建模与参数选择from sklearn.linear_model import LinearRegression # 构造时间编号特征从 0 开始递增 t np.arange(len(y)) mask trend.notna().values # 去掉 STL 前端的 NaN # 对趋势分量做线性回归不直接对原始 y 拟合 reg LinearRegression() reg.fit(t[mask].reshape(-1, 1), trend[mask]) # R^2 在 0.9 以上说明趋势被解释得比较干净 score reg.score(t[mask].reshape(-1, 1), trend[mask]) print(trend R^2:, score)特别注意这里拟合的对象是趋势分量本身不是原始 y。直接拿时间编号对原始数据回归季节成分会混进系数里预测自然带偏。判断要不要加二次项可以看趋势分量的差分序列对 trend 做一阶差分如果差分后还有明显递增或递减的斜率再考虑多项式或分段线性。趋势分量 R^2 过低时不要急着换模型先回头检查 STL 参数。R^2 低于 0.9 通常意味着 trend 窗口太小趋势分量把噪声也学进去了把 trend21 提到 31 或 41 再试。3.3 第三步季节性分量怎么预测下一周期的值季节分量在 STL 下被近似成周期性信号外推方式有两种直接用最近一个完整周期的季节值或者用近四周同一天的平均值。前一种保守后一种平滑常规先用最近周期数据噪声大换后者。period 7 seasonal_values seasonal.values # 方案 A直接取最近一个周期的季节值 last_season seasonal_values[-period:] # 方案 B取最近 4 周同一天的平均抑制单周噪声 seasonal_matrix seasonal_values.reshape(-1, period) recent seasonal_matrix[-4:, :] next_season_avg recent.mean(axis0) # 默认选 A如果季节分量单周抖动大改成 B season_next last_season注意 STL 的季节分量是全长的不像趋势两端有缺失reshape 时不会出问题。用方案 B 的前提是季节形态近几周没有明显漂移如果某周因为大促把季节值拉高取平均反而会把促销效应扩散到整个未来周期。这时候方案 A 更干净。3.4 第四步合成最终预测并可视化from datetime import datetime def forecast_trend_season(reg, season_next, n_history, horizon14, period7, resid_stdNone): # 趋势外推用回归方程预测未来时间编号 t_future np.arange(n_history, n_history horizon) trend_future reg.predict(t_future.reshape(-1, 1)) # 季节分量按周期往后铺满预测窗口 season_future np.tile(season_next, horizon // period 1)[:horizon] # 加法合成 y_future trend_future season_future # 给一个粗略 95% 区间正式上线前用滚动回测重算 if resid_std is not None: lower y_future - 1.96 * resid_std upper y_future 1.96 * resid_std return y_future, lower, upper return y_future resid_std resid.dropna().std() y_future, lower, upper forecast_trend_season( reg, season_next, len(y), horizon14, resid_stdresid_std ) # 生成未来日期序列并画图 future_dates pd.date_range(y.index[-1] pd.Timedelta(days1), periods14, freqD) plt.figure(figsize(12, 4)) plt.plot(y.index, y, labelactual) plt.plot(future_dates, y_future, labelforecast) plt.fill_between(future_dates, lower, upper, alpha0.2, label95% interval) plt.legend() plt.show()合成顺序是硬性的先算趋势未来值再叠加季节。顺序反了季节分量会被趋势回归误伤。1.96 乘残差标准差当区间只是经验做法残差不是正态分布时区间会偏窄真上线前用第 5 章的滚动回测重算区间宽度。这里还有一个容易被忽略的点如果最初对序列做过 log 变换y_future、lower、upper 三个数组都要整体做 expm1 还原不能只还原点预测否则区间在原始尺度上不对称。4. 趋势和季节性预测避坑清单5 个翻车现场与解决办法这套分解加合成的流程看着简单真正落地时坑都在细节里。以下是几类常见翻车情况按现象到原因再到解决的顺序写每一条都是实际项目里的血泪经验。4.1 周期参数设错分解结果全是锯齿现象STL 分解后的季节分量看不出固定周期残差大得离谱趋势分量跟着数据跳动。 原因period 与真实周期不符。日数据真实周期是 7填成 30STL 会在错误的间隔上强行平均季节分量自然是一团乱麻。 解决先用自相关图确认主周期不要拍脑袋。from statsmodels.graphics.tsaplots import plot_acf # ACF 在周期倍数处出现峰值lags 设到 60 够看两个周期 plot_acf(y.dropna(), lags60) plt.show()ACF 图上第一个明显峰值的横坐标就是候选周期。比如峰出现在 lag7 和 lag14周期就是 7。这个方法同样适用于判断有没有双周期周周期和年周期会同时出现两组峰。4.2 季节波动随趋势放大加法模型残差越来越大现象模型刚上线前几周误差正常后面 RMSE 越滚越大MAPE 却还在可接受范围。 原因序列本质是乘法季节性旺季绝对值是淡季好几倍加法模型用一个固定季节系数扛不住全部时段残差必然随时间放大。 解决对序列做 log1p 变换再做整套分解预测最后 expm1 还原。这个操作把乘法关系转成加法关系整个流程不用换。y_log np.log1p(y) # 用 y_log 替换前面的 y走完分解、拟合、合成 # 输出时统一还原注意区间也要还原 lower_orig np.expm1(lower_log) upper_orig np.expm1(upper_log)注意区间换算后不是对称的这是正常现象不要为了好看强行对称。log1p 对有小数的序列也能处理如果数据量级全为正且没有 0可以用 log 替代区别不大。4.3 趋势外推变成笔直一条线远期预测失真现象预测第 30 天以后还在线性增长业务侧看一眼就说不可能。 原因线性趋势外推没有衰减机制实际业务有竞品、有生命周期、有市场容量。 解决给趋势加阻尼或直接用带阻尼项的 ETS 模型做交叉验证。from statsmodels.tsa.exponential_smoothing.ets import ETSModel # error/trend/seasonal 都设 additiveperiod 按业务周期 ets ETSModel( y, erroradd, trendadd, seasonaladd, seasonal_periods7, damped_trendTrue ) fit ets.fit() ets_forecast fit.forecast(14)damped_trendTrue 会让趋势斜率逐步衰减预测曲线慢慢弯向水平而不是无限延伸。ETS 和前面的分解法结果应该接近如果差太多通常是分解法里趋势窗口太短导致斜率被高估。4.4 只给点预测不给区间业务侧没法用现象预测值报上去运营问误差范围是多少答不上来。 原因把单点预测当交付物忽略了预测天然有不确定性。 解决快速版用残差标准差构造经验区间严谨版用滚动回测收集未来 h 步的实际误差取 90% 分位数当区间边界。第 5 章会给滚动回测的代码这里给个更轻量的增量做法计算近 8 周滚动预测误差的标准差直接乘 1.645 当 90% 区间半径比用全体残差更贴合近期表现。4.5 节假日脉冲被当成季节性预测被带偏现象春节或双十一当天预测远低于实际节后几天又明显高于实际。 原因单次或日期每年漂移的脉冲事件会被 STL 的 loess 平滑吸收进趋势或季节分量导致节日被低估、节后惯性被漏掉。 解决把节假日单独建哑变量和趋势一起进回归模型。# holiday_dates 是已知节假日列表未来预测时也要有对应日期 df[is_holiday] df.index.isin(holiday_dates) # 趋势 节假日哑变量一起拟合 X pd.DataFrame({ t: np.arange(len(df)), is_holiday: df[is_holiday].astype(int) }) reg.fit(X, trend)节假日效应拆出来后季节分量不再被脉冲污染预测节日当天会明显抬升。注意哑变量要求预测窗口内也能拿到节假日日历否则只能假设未来无节日区间自然要放宽。5. 滚动回测与评估指标让趋势季节性方案经得起验证再好的分解流程不验证等于没做。一次性把数据切成训练集和测试集很容易被运气骗到我一般用滚动回测固定训练窗口每次往前滚一个周期预测下一个周期收集每一步误差。这样做能覆盖周内每天、月内每个阶段的分布。def my_forecast(train, period, horizon): # 把第 3 章的分解趋势拟合季节外推流程封装进来 pass def rolling_eval(df, period, horizon, n_splits): errors [] total len(df) # 最后 n_splits 个预测窗口依次滚动 for i in range(n_splits): end total - (n_splits - i) * horizon if end - horizon 0: break train df.iloc[:end] test df.iloc[end:end horizon] if len(test) horizon: break pred, _, _ my_forecast(train, period, horizon) actual test[y].values[:len(pred)] errors.append(np.mean(np.abs(actual - pred))) return errors errors rolling_eval(df, period7, horizon7, n_splits8) print(平均绝对误差:, np.mean(errors))滚动回测的结果也是调参依据。period、trend 窗口、季节外推用最近周期还是多周均值这些参数在回测误差面前都不再是玄学。指标方面MAPE 在分母接近 0 时会爆炸销量这类含零值的数据我改用 WMAPEdef wmape(y_true, y_pred): return np.sum(np.abs(y_true - y_pred)) / np.sum(np.abs(y_true))WMAPE 分母用真实值总和单点异常不会把指标拉飞。数据量足够且周期稳定时可以拿 LSTM 和这套分解法对比一轮。大多数业务场景下分解方案在数据量两三年以内时表现反而更稳LSTM 适合的是更长序列和更复杂的非线性依赖。我现在的固定流程是拿到序列先画图用 ACF 定周期STL 分解看三个分量是否干净滚动回测定参数最后再决定要不要升级到复杂模型。这套习惯帮我少踩了很多坑希望帮到你。本文还有配套的精品资源点击获取
返回列表