
简介基于趋势和季节性的时间序列预测实战资源包聚焦Python环境下对含趋势项与季节项数据的建模流程适合具备一定Python基础、希望进入气候预测或时序分析领域的读者可直接对照Notebook动手实践。压缩包共9个文件包含3个Jupyter Notebook交互式案例、1个真实气温数据集daily-minimum-temperatures.csv及配套的项目配置与说明文件总大小仅2.95MB轻量易上手。案例以ARIMA、SARIMA和STL分解为主线完整演示了滚动均值提取趋势、季节项分解、模型训练与调参、误差指标评估以及预测结果可视化等关键环节同时提供可运行的建模代码和输出展示便于读者直接复现、改造并迁移到自己的数据上。已有528人学习下载适合作为从理论到实战的过渡材料也可为温度、降雨等气候指标预测提供一套可扩展的参考流程。1. 把趋势和季节性拆开预测的难度先降一半做过销量预测、流量预测或运维监控阈值的人应该都有过这种体会直接拿原始序列丢给 LSTM 或 XGBoost调参调到怀疑人生结果换一个测试集就翻车。问题往往不是模型不够强而是数据里藏着两股力量一股叫趋势一股叫季节性。趋势让序列长期向上或向下季节性让序列在固定周期内上下起伏。两股力量叠在一起模型很难同时学明白而「基于趋势和季节性的时间序列预测」这个方向核心做法就是先把序列分解成趋势、季节和剩余三个部分再分而治之。这篇实战笔记会从分解原理讲到可复现的代码、参数选择和踩坑记录适合手里有历史数据、想用 Python 快速做出一版能用的预测结果的分析师或开发。目标很具体让你今天读完明天能在自己数据上跑出一张分解图和一份预测曲线。2. 趋势和季节性为什么必须分开处理三个场景与一个判别方法2.1 三种让你误判序列特征的场景先看第一个常见场景。一家电商公司的周销量序列过去两年整体增长了 40%但每年 11 月前后都有一个高峰。如果你直接对整个序列拟合一个线性模型模型会把每年的高峰当成噪声把整体上涨当成唯一规律预测结果在旺季会明显低估。这就是趋势压过了季节性的情况。第二个场景相反。一个机房服务器的每日请求量一年四季基本平稳但每周一到周五明显高于周末。这时候趋势几乎是一条平线季节性占绝对主导。如果非要用 ARIMA 去拟合阶数选择容易被短期的周内波动带偏模型会花大量参数去描述周内形态却对整体水平的判断失真。第三个场景最棘手趋势和季节性同时都在变。比如一款新产品的用户增长曲线早期陡增、中期放缓同时每周还有使用高峰。线性趋势假设直接失效固定幅度的季节性假设也失效因为季节性波动的幅度会随着整体量级增长而放大。这种序列如果直接建模残差里会残留大量结构性信息导致预测区间宽到没有实用价值。这三种场景的共同点在于趋势和季节性不是独立起作用的但它们对预测的贡献方式完全不同。趋势决定序列长期走向季节性决定短期节奏。一个模型同时拟合两者等于让同一个参数集去表达两种不同频率的行为效果往往顾此失彼。我一般会先做一次快速判别再决定是否走分解路线。2.2 判别方法画图 自相关检验不要靠猜判别趋势和季节性是否显著不需要一开始就上复杂统计检验。我的习惯是两步走。第一步画原始序列折线图和自相关图。折线图直接看是否呈现单调增减或明显周期起伏自相关图更客观如果延迟 7 阶或 12 阶的自相关系数显著高于周边说明存在对应周期的季节性。注意看图时不能只看相关系数大小要看它相对邻近阶数的突出程度。第二步用一个快速数值指标辅助确认。对序列做一阶差分再对差分后的序列做单位根检验比如 ADF 检验。如果原序列 ADF 统计量不显著、差分后显著说明趋势成分存在。对季节性可以按候选周期切分子序列比较各子序列的均值差异是否显著用简单的方差分析就能搞定。import pandas as pd from statsmodels.tsa.stattools import adfuller, acf import numpy as np def check_trend_seasonality(series, seasonal_period): # ADF 检验原序列平稳性p 0.05 通常意味着存在趋势成分 adf_orig adfuller(series.dropna())[1] adf_diff adfuller(series.diff().dropna())[1] print(f原序列 ADF p-value: {adf_orig:.4f}) print(f一阶差分后 ADF p-value: {adf_diff:.4f}) # 自相关检查季节性看指定周期附近是否有显著峰值 acf_vals acf(series.dropna(), nlagsseasonal_period * 2) peak acf_vals[seasonal_period] print(f延迟 {seasonal_period} 阶自相关系数: {peak:.4f}) return adf_orig, adf_diff, peak这段代码的作用是给出两个数字ADF 的 p 值和指定周期处的自相关系数。p 值大于 0.05 说明原序列非平稳差分后显著低于 0.05 则说明趋势成分存在周期处的自相关系数明显高于两侧说明季节性成立。我一般把周期处的自相关系数超过 0.3 视为季节性显著的粗略阈值具体还要结合序列长度和业务周期判断这个后面避坑部分会细说。3. STL 分解实操用 statsmodels 把趋势和季节拆出来3.1 STL 分解的最小可用代码确定序列存在趋势和季节性后下一步就是分解。常见做法是使用 STLSeasonal-Trend decomposition using LOESS它是目前实践中最稳的分解方法。相比经典加法分解STL 能处理非线性的趋势和非对称的季节形态而且对异常值不敏感在电商、运维、金融数据上都适用。statsmodels 库提供了现成实现直接用即可。import pandas as pd import matplotlib.pyplot as plt from statsmodels.tsa.seasonal import STL def stl_decompose(series, period): stl STL(series, periodperiod, robustTrue, seasonal13, trendNone, low_passNone) result stl.fit() fig result.plot() fig.set_size_inches(12, 8) plt.show() return result调用方式很简单传入等间隔的序列数据和周期长度period7 表示周季节性period12 或 365 分别对应月度或年度周期。robustTrue 是很关键的一个开关它让 LOESS 在拟合时降低异常值权重避免某天的突发事件把趋势曲线拉偏。seasonal13 控制季节项平滑窗口的长度数值越大季节项越平滑越能抓住稳定的周期形态数据越长这个值可以适当调大。分解结果包含三部分trend 趋势项、seasonal 季节项、resid 残差项。趋势项展示长期走向季节项展示固定周期的起伏残差项则是剔除前两者后的随机波动。拿到这三部分后预测策略就很清晰了对趋势项用线性或多项式模型外推对季节项直接取历史同期均值或最后一年形态对残差项按白噪声处理。三部分预测相加就是最终结果。这套思路下你不再需要用一个黑匣子模型去硬学序列的复杂形态而是分开建模。3.2 预测时趋势项与季节项各自怎么用趋势项外推是分解预测中最容易出错的一环。用线性模型外推趋势短期效果尚可但如果趋势明显非线性且外推距离较长线性假设会让预测结果系统性偏离。我一般会先看趋势曲线形状如果接近直线用线性回归如果呈指数或对数形态先做变换再回归如果形态复杂直接用最后一段趋势加二阶多项式回归但要控制外推距离不要超过训练集长度的五分之一。季节项的预测通常有两种做法。一种是对每个季节位置取历史同期均值简单且稳适合季节形态稳定的序列另一种是使用最后一次完整周期的季节项作为未来周期的季节形态适合季节形态随时间缓慢演变的序列。两种做法我都倾向使用前者因为均值能平滑掉单次周期的异常波动。残差项在预测中通常不做外推只用于生成预测区间——用残差的标准差作为预测区间的宽度参考简单有效。import numpy as np from sklearn.linear_model import LinearRegression def forecast_with_decomposition(result, period, horizon): trend result.trend seasonal result.seasonal resid result.resid # 1. 趋势外推用最后 30 个趋势点拟合线性模型 x np.arange(len(trend))[-30:].reshape(-1, 1) y trend[-30:].values model LinearRegression().fit(x, y) future_x np.arange(len(trend), len(trend) horizon).reshape(-1, 1) trend_forecast model.predict(future_x) # 2. 季节项取历史同期均值 n_cycles len(seasonal) // period seasonal_matrix seasonal[-n_cycles * period:].values.reshape(n_cycles, period) seasonal_forecast np.tile(seasonal_matrix.mean(axis0), horizon // period 1)[:horizon] # 3. 残差用于估计预测区间 resid_std np.nanstd(resid) pred trend_forecast seasonal_forecast lower pred - 1.96 * resid_std upper pred 1.96 * resid_std return pred, lower, upper这段逻辑里趋势外推只用了最后 30 个点训练线性模型而不是整个趋势段理由是很久以前的趋势信息对近期走向的参考价值有限强行纳入会让模型对历史平均形态敏感反而看不清最近的变化方向。季节项用了全量历史周期求均值数值上更稳定。预测区间用 1.96 倍残差标准差对应 95% 的覆盖概率但前提是残差近似正态分布如果后面验证时发现残差存在明显的自相关这个区间就要打折扣。4. 参数选择的边界period、seasonal、trend 三个参数怎么调4.1 period 的决定因素不是数据频率而是业务周期STL 分解中period 是最不能拍脑袋的参数。数据是日频不代表周期就是 7。一个面向政企客户的软件系统日活数据可能没有周末低谷反而月底和季末冲高真正的周期是 30 天或 90 天。另一个典型错误是月频数据一律用 12 做年季节性但如果业务有寒暑假效应6 月和 9 月明显不同于其他月份12 这个周期就无法表达这种非连续的季节形态。我的建议是先做业务判断再用自相关图验证。画出延迟 1 到 60 阶的自相关图找出几个显著的峰值再看这些峰值对应的延迟是否能从业务角度解释。不要只依赖 ACF 数值因为 ACF 对趋势敏感趋势强的时候所有延迟的自相关都偏高这时候要把趋势先去掉再看。4.2 seasonal 与 trend 参数越大越平滑但会吃掉信号seasonal 参数控制季节项估计时的平滑窗口大小。窗口过小季节项会跟着单次周期的随机波动走产生伪季节形态窗口过大真实的季节变化会被抹平。经验上seasonal 设置为不小于 period 的奇数且不超过一个完整周期长度的两倍。period7 时用 13 或 15 都常被采用period12 时用 15 或 21 做参照。trend 参数如果不显式指定STL 会自动设置。手动指定时trend 需要大于 seasonal 才能保证趋势项的平滑度否则趋势项和季节项会出现高频的相互抢夺。low_pass 参数是趋势项做低通滤波时的窗口一般用户不需要手动调保持 None 让库自动推断即可。这三个参数我只动前两个low_pass 几乎不碰。参数作用调大后的效果常用区间period季节性周期长度周期辨识变长可能错过短周期7 / 12 / 24 / 365按业务定seasonal季节项平滑窗口季节形态更平滑细节被抹period 到 2 × period 之间的奇数trend趋势项平滑窗口趋势线更直转折滞后1.5 × seasonal 到 2 × seasonalrobust是否抗异常值True 时受离群点影响更小数据有脉冲时设 True参数调整后如何判断是否合适看两个信号。一个是残差项是否近似随机如果残差中仍能看到明显的周期起伏说明季节项没有完全提取另一个是趋势项是否保留了足够的转折点如果趋势被拉成直线且残差不再平稳说明 trend 过大。实际操作中我会对比两到三组参数下的残差自相关图选残差自相关系数整体最接近零的那组参数而不是选趋势最平滑的。5. 避坑时间序列分解预测的五个典型翻车现场5.1 趋势外推的曲率失控短期好用长期离谱现象预测未来 30 天前 7 天看起来合理越往后越夸张甚至出现负值或指数级飙升。原因趋势项外推用了二阶多项式曲率在数据末端被放大或者训练集末端恰好处于波动高点或低点模型误把局部波动当成长期趋势方向。解决限制外推距离在训练集长度的五分之一以内改用带曲率约束的拟合方法比如在回归中加入趋势项的斜率变化惩罚或者干脆用最后一段趋势的中位数作为未来趋势常数项。我常用的做法是同时输出线性外推和常数外推两个版本让业务方自己权衡。5.2 季节周期长度数错周数据做出了年季节性现象季节项呈现出奇怪的锯齿状预测曲线明显偏离历史形态。原因周频数据只有 104 个点有人硬是设 period52希望提取年季节性。但 STL 做局部回归时一个周期内只有 2 个点可用平滑窗口形同虚设季节项实际上在拟合随机波动。解决数据无法支撑周期时不要硬做分解。周频数据连续 3 年以上再考虑年季节性不够 3 年时用月度虚拟变量回归方法代替 STL 分解或者退一步只做趋势加节假日修正。鉴别方法很简单分解完成后看季节项曲线如果出现了明显的、没有业务解释的高频抖动大概率是周期长度设置不合理。5.3 节假日效应被当成残差噪声现象残差项中每年春节、双十一对应位置出现大幅度正负波动预测区间因此被撑到不可用。原因STL 分解只识别固定周期节假日对应日期在公历中并不固定且它们的影响强度和持续时间也不固定。STL 无法把这些效应归入季节项只能把它们留给残差导致残差方差被高估。解决做两步建模。先把节假日因素作为虚拟变量单独回归得到节假日效应因子再把去除节假日影响的序列交给 STL 分解。预测时同样分两步STL 预测基础值然后叠加节假日因子。这个方案在电商数据上效果非常明显残差方差通常能下降 30% 到 50%。5.4 分解对异常值过于敏感一条脏数据带崩全局现象某天接口故障导致数据为零接下来好几天的预测值都异常偏低。原因STL 的 LOESS 回归使用局部加权异常值虽然只影响局部窗口但趋势项的平滑会让这个影响顺着时间方向扩散尤其是在异常点靠近序列末端时。解决数据进入分解前先做离群点检测把超过 5 倍残差标准差的点替换为插值。statsmodels 的 STL 虽然提供了 robustTrue但它降低的是异常点的权重不是完全剔除极端情况下仍会污染邻近点的估计。我习惯的做法是先用一个简单的 3-sigma 规则初步清洗再做分解判断清洗是否合理的标准是分解后的残差项不再出现孤立的巨大尖峰。5.5 分解结果是动态的线上预测不断回头修改现象用 1 月到 10 月的数据分解并预测 11 月结果合理到了 11 月重新做全量分解发现 10 月的趋势值和季节值与之前完全不同导致模型经常自我否定。原因STL 分解是基于全局数据的后端数据的更新会影响前端的趋势估计。这不代表算法出错而是动态分解的正常性质。但如果你用前一次分解的结论指导下一次预测会陷入不断改口的循环。解决确立「冻结」原则。每次预测只使用截至预测日前一天的数据做分解预测完成后把趋势方程和季节均值存档。后续预测中如果数据没有发生明显的结构性变化不重新拟合参数只更新残差统计量。这是一个工程约定不是统计问题但能避免大量的重复沟通成本。6. 验证预测结果的另一种思路滚动回测与预测区间覆盖率分解预测有没有效果不是看训练集上的拟合误差而是看连续滚动预测中的表现。具体做法是把数据按时间顺序分为训练段和测试段从训练段末尾开始每次只向前预测一步或一个周期随后把真实值并入训练集再次预测如此反复直到测试段结束。这样得到的多步预测误差比单次预测更有参考价值因为它还原了真实业务中不断新增数据点后的预测场景。实现时注意保持分解参数固定不要每步都重新用自相关图判断周期否则回测结果中混杂了人为调参的影响无法对比不同方法的真实水平。评估指标用 RMSE 和 MAPE 各看一个RMSE 对大偏差敏感MAPE 看相对水平两者结合不容易被单点异常误导。另外一定记录预测区间的覆盖率即真实值落在区间内的比例目标是接近 95% 且不高于 98%。覆盖率太低说明区间过窄太高说明残差方差被高估都会影响实际使用。def rolling_forecast_eval(series, period, horizon, steps): from statsmodels.tsa.seasonal import STL import numpy as np errors [] coverage [] n len(series) for i in range(steps): train series.iloc[: n - steps i] test series.iloc[n - steps i : n - steps i horizon] stl STL(train, periodperiod, robustTrue).fit() # 简化处理趋势外推进阶版本 x np.arange(len(train) 1, len(train) 1 horizon) trend_fit np.polyfit(np.arange(len(train)), stl.trend, deg1) trend_pred np.polyval(trend_fit, x) seasonal_mean stl.seasonal[-period:].values tiled_seasonal np.tile(seasonal_mean, horizon // period 1)[:horizon] pred trend_pred tiled_seasonal resid_std np.nanstd(stl.resid) errors.extend(np.abs(pred - test.values)) coverage.append( np.mean((test.values pred - 1.96 * resid_std) (test.values pred 1.96 * resid_std)) ) return np.mean(errors), np.mean(coverage)这段回测的骨架基本可以直接抄但有三个点需要按数据情况调整。第一趋势外推用的是全局一次多项式拟合如果你的趋势明显非线性要替换为分段线性或样条。第二seasonal_mean 取的是最后一个周期的季节项如果你的季节形态稳定度一般可以退化到取历史同期均值。第三回测步数越多早期预测面临的趋势不确定性越大误差会被拉高这是正常现象不要因此误判方法失效。当我用这套流程跑完一个项目后通常最后一步是画一张「预测值与真实值」的对比图重点看最后一个周期内的偏差方向是否系统性的偏大或偏小。经验是系统性偏差几乎都能追溯到趋势外推的假设错误而不是季节项的问题。这也是为什么我始终建议先看图调整趋势外推方式再回头打磨模型细节。我个人的习惯是把分解图和预测对比图打印出来贴在工位旁边每天看一遍比任何指标都更能提前嗅到问题的气味。希望这篇笔记能帮你少走几段弯路。本文还有配套的精品资源点击获取