4. 时间序列分解:趋势、季节性与残差成分的概念与可视化

时间序列分解,说白了就是把一条乱糟糟的曲线拆成几块有规律的东西。我刚开始做环境数据分析那会儿,拿到PM2.5的监测数据,第一反应就是直接建模预测。结果呢?模型跑出来一塌糊涂。后来老前辈跟我说:你先拆开看看。嗯,从那以后,分解成了我每次分析的第一步。

4.1 为什么要分解?

你想想看,一条时间序列里其实藏着好几层信息。比如北京的AQI数据,既有每年冬天变差的规律(季节性),又有逐年改善的大趋势(趋势),还有一些突发污染事件(残差)。如果不拆开,这些信息混在一起,模型根本学不明白。

核心思想:任何时间序列 Y(t) 都可以表示为三个成分的叠加或乘积:

加法模型:Y(t) = Trend(t) + Seasonal(t) + Residual(t)

乘法模型:Y(t) = Trend(t) × Seasonal(t) × Residual(t)

我个人习惯先试加法模型。为什么呢?因为直观。如果数据波动幅度随时间变化不大,加法模型就够了。如果波动幅度越来越大,比如夏季用电量逐年攀升,那就要用乘法模型。

4.2 三个成分到底长什么样?

咱们一个一个说清楚。

4.2.1 趋势成分(Trend)

趋势就是长期的变化方向。上升、下降、或者持平。我在项目中遇到过监测某湖泊的溶解氧数据,十年间缓慢下降。这就是趋势——它告诉你,这个湖的生态可能在恶化。

4.2.2 季节性成分(Seasonal)

季节性就是固定周期内的重复模式。周期可以是年、季度、月、周。比如河流的流量,每年汛期涨水、枯水期落水。这个规律非常稳定。我曾经处理过某水库的日入库流量数据,季节性周期是365天,但实际做分解时我用了365.25天——因为闰年。这个小细节,很多人会忽略。

4.2.3 残差成分(Residual)

残差就是去掉趋势和季节性之后剩下的东西。说白了就是「说不清楚的部分」。它可能是随机噪声,也可能是突发事件。比如某天工厂突然排污导致水质异常,这个信号就会出现在残差里。

注意:残差不等于无用信息。我曾经通过分析残差,发现了一个传感器故障的规律——残差在每天凌晨3点突然跳变,后来排查发现是设备定时自检导致的。所以残差里往往藏着故事。

4.3 怎么分解?用Python实现

Python里最常用的工具是 statsmodels 库的 seasonal_decompose 函数。我直接上代码,你跟着跑一遍就明白了。

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from statsmodels.tsa.seasonal import seasonal_decompose

# 生成模拟数据:趋势 + 季节性 + 噪声
np.random.seed(42)
dates = pd.date_range('2020-01-01', periods=365*3, freq='D')
trend = np.linspace(20, 25, len(dates))  # 缓慢上升
seasonal = 5 * np.sin(2 * np.pi * np.arange(len(dates)) / 365)  # 年周期
noise = np.random.normal(0, 1, len(dates))
data = trend + seasonal + noise

# 创建时间序列
ts = pd.Series(data, index=dates)

# 分解
result = seasonal_decompose(ts, model='additive', period=365)

# 可视化
fig, axes = plt.subplots(4, 1, figsize=(12, 10), sharex=True)
ts.plot(ax=axes[0], title='原始序列', color='#2E86AB')
result.trend.plot(ax=axes[1], title='趋势成分', color='#A23B72')
result.seasonal.plot(ax=axes[2], title='季节性成分', color='#F18F01')
result.resid.plot(ax=axes[3], title='残差成分', color='#C73E1D')
plt.tight_layout()
plt.show()

我的经验:period参数一定要设对。如果是日数据,年周期就是365;如果是月数据,年周期就是12。设错了,分解出来的季节性会完全乱掉。我曾经帮一个团队排查问题,他们用日数据但设了period=12,结果季节性成分看起来像锯齿——其实就是周期设错了。

4.4 加法模型 vs 乘法模型:怎么选?

我整理了一个简单的判断表,你直接对照着看:

特征 加法模型 乘法模型
季节性波动幅度 基本不变 随趋势变化
典型场景 气温、水位 用电量、销售额
数据中是否有零或负值 可以 不行(乘法要求正数)
我的建议 先试这个 加法效果不好再试

举个例子:某河流的COD浓度,季节性波动在丰水期和枯水期差不多大,用加法模型就够了。但如果是某工厂的月产值,随着工厂扩建产值翻倍,季节性波动也跟着变大,这时候乘法模型更合适。

4.5 可视化:一眼看出问题

分解之后一定要画图。我习惯把四个子图叠在一起看,这样能快速发现异常。

比如有一次,我看到残差成分里出现了一个巨大的尖峰。排查后发现,那天附近发生了山体滑坡,导致水质突变。这个信息如果只看原始序列,很容易被季节性波动掩盖掉。

还有一次,趋势成分突然出现一个台阶式的下降。我一开始以为是算法问题,后来发现是那个月更换了监测仪器,导致数据整体偏低。嗯,这就是数据质量的问题了。

可视化要点:

  • 原始序列和趋势用同一种颜色系,方便对比
  • 季节性成分单独用亮色,突出周期性
  • 残差用警示色,一眼就能看到异常点
  • 四个子图共享x轴,方便对齐时间

4.6 避坑指南

我做时间序列分解这些年,踩过不少坑。挑几个典型的跟你说说:

  • 周期长度搞错:我曾经用周数据,但设了period=7。看起来没问题,但实际业务周期是14天(两周一报)。结果季节性成分完全没抓到规律。
  • 缺失值处理:分解函数遇到NaN会报错。我习惯先用插值法填充,或者直接删除缺失段。但要注意,如果缺失太多,插值会引入偏差。
  • 边界效应:分解时,序列开头和结尾的趋势成分会不准确。因为算法需要前后数据来平滑。我一般会忽略前几个和后几个数据点。
  • 多重季节性:有些数据有多个周期,比如小时数据既有日周期又有周周期。这时候简单的seasonal_decompose搞不定,需要用更高级的方法,比如STL分解。

特别提醒:不要盲目相信分解结果。我见过有人把随机噪声硬生生分解出「趋势」和「季节性」。怎么判断?看残差——如果残差里还有明显的模式,说明分解不彻底,需要调整参数或换模型。

4.7 本章小结

时间序列分解就是把复杂问题简单化。趋势告诉你方向,季节性告诉你规律,残差告诉你意外。这三块搞清楚了,后面的预测建模就轻松多了。

我个人习惯,拿到任何时间序列数据,第一件事就是分解。哪怕最后不用这个结果,也能帮你快速了解数据的脾气。嗯,就像交朋友,先了解性格,再谈合作。


公众号:蓝海资料掘金营,微信deep3321