12.2 趋势、季节与残差分解¶
小率把奶茶店半年的销量折线拿给均哥看。折线一会儿上升,一会儿每周重复波动,还有几天突然很高。他想知道:到底是店越来越火,还是只是周末更忙?
一条线里混了这么多东西,我应该先解释哪一个?
先拆开。趋势看长期方向,季节看固定周期,残差看剩下的异常和噪声。
12.2.1 分解是把混合声音分轨¶
时间序列分解 (Time Series Decomposition) 的目标,是把原序列拆成更容易解释的部分。
加法分解适合季节波动幅度大致稳定的序列:
\[
y_t = T_t + S_t + R_t
\]
乘法分解适合“水平越高,波动越大”的序列:
\[
y_t = T_t \times S_t \times R_t
\]
实际分析时,常把乘法关系转成对数后的加法关系:
\[
\log y_t = \log T_t + \log S_t + \log R_t
\]
12.2.2 季节调整让长期变化更清楚¶
如果只看原始销量,周末高峰会遮住长期趋势。季节调整 (Seasonal Adjustment) 会先去掉重复周期:
\[
\text{季节调整序列} = y_t - S_t
\]
这样再看趋势,就更容易回答“这家店是不是慢慢变火”。
什么时候先做季节调整
想比较不同月份、不同星期的长期水平时,先季节调整。
想预测真实未来销量时,不能只保留季节调整后的序列,还要把季节项加回去。
12.2.3 STL:更稳健的现代分解¶
STL (Seasonal-Trend decomposition using Loess) 是常用的分解方法。它用局部平滑估计趋势和季节,对异常点也比较稳健。
import numpy as np
import pandas as pd
from statsmodels.tsa.seasonal import STL
rng = np.random.default_rng(12)
dates = pd.date_range("2022-01-01", periods=60, freq="MS")
t = np.arange(len(dates))
sales = pd.Series(
180 + 2.5 * t + 24 * np.sin(2 * np.pi * t / 12) + rng.normal(0, 8, len(t)),
index=dates,
name="月销量",
)
result = STL(sales, period=12, robust=True).fit()
parts = pd.DataFrame({
"原序列": sales,
"趋势": result.trend,
"季节": result.seasonal,
"残差": result.resid,
})
print(parts.tail().round(1))
完整脚本见:
12.2.4 残差不是垃圾桶¶
分解之后,残差应该像“没有明显结构的波动”。如果残差里还看得出趋势、周期或连续偏高偏低,说明分解还没把结构解释完。
残差里还有图案,就别急着预测
残差不是随便剩下什么都可以。如果残差仍然有自相关,后续模型还需要继续解释这些时间结构。
小率的笔记本
分解不是为了把图画得漂亮,而是为了分清问题。
趋势回答“长期变没变”,季节回答“固定周期怎么重复”,残差回答“剩下的异常和噪声”。
解释和预测之前,先把这三件事分开看。

