跳至主要内容

时间序列分解:读懂趋势、季节性与噪声

学习如何将时间序列数据拆解为趋势、季节性与残差等有意义的组件,掌握加性与乘性模型。了解经典与 STL 方法,探索季节性调整,并用分解模型实践预测。
· 8分钟 阅读

使用 AI 探索

分解是数据科学中颇具趣味的一类方法。其核心思想是把复杂事物拆解为更简单、更易解释的部分。这可以用于矩阵、模型,或信号。

由于这一理念十分通用,它有许多具体形式。在线性代数中,有QR 分解、特征分解、奇异值分解,以及 Cholesky 分解等。在其他领域,还有傅里叶变换、主成分分析,甚至用 LDA 等方法进行的主题建模。回归模型中的变异性可以分解为平方和组成部分:SST、SSR 和 SSE。我们甚至可以把决策树看作一种分解形式。

这里,我们将回顾我们的最爱之一:时间序列分解。如果读完之后您对此产生兴趣,欢迎参加由 Rob Hyndman 教授主讲的 Forecasting in R 课程。

什么是时间序列分解?

时间序列分解是一种将时间序列拆分为独立组成部分的方法,通常包括趋势或趋势-周期、季节性和残差。时间序列分解的目的在于更好地理解潜在模式,并提升预测的准确性。

时间序列有哪些组成部分?

我们来看看时间序列的各个组成部分。这可能是理解该概念的最佳方式。

趋势(Trend)组件

顾名思义,时间序列的趋势是长期走势。需要说明的是,趋势组件通常被理解为趋势-周期,其中的“周期”被看作是长期且不规则的模式。汽车销量过去被认为遵循七年一周期(不论原因如何),不过随着电动车的兴起,这一说法或许已不再适用。

人们从趋势中得到的关键信息通常是:从长期来看,数据是在上升、下降,还是保持稳定?过去数十年全球海洋温度上升就是一个明显(且不妙)的趋势。

季节性(Seasonal)组件

季节性组件需要更详细的说明,因为它稍微复杂一些。时间序列的季节性是指在固定间隔内重复出现的短期循环。比如按日、按月、按周、按季度。关键在于这些间隔必须相当规律。您或许会看到,如果人们周日更喜欢在家自己煮咖啡,那么咖啡店订单会呈现每周季节性。(我们将在下一节探讨这个咖啡店订单的问题。)

关于季节性,还有些复杂之处。想想一年中的周数并非整数(约 52.18)。再想想各个月份并非等长:二月有 28(或 29)天,但七月总是 31 天。

更进一步,由于每年略多于 52 周,而且月份也不等长,您无法将大多数月份整除为完整的周。这在看日历时很直观。但对数据分析师来说,不太直观的是:当您在按周粒度观察数据,再把它汇总到按月级别时,可能会看到不一致的现象,比如某一年的一月有四个周一,下一年的一月却有五个。

残差 / 不规则 / 余项组件

在去除趋势-周期和季节性组件之后剩下的一切,我们称之为“残差组件”,其中包括数据中的异常事件和随机噪声。与名称可能带给您的印象相反,残差组件并非无用的副产物。所有数据都存在无法解释的变异性,量化这种变异性的大小很重要。有趣的是,在表现良好的序列中,残差往往呈现为正态分布(高斯分布),在这种情况下,我们可能会用假设检验来评估“黑天鹅”事件。

时间序列分解模型

既然我们已经讨论了时间序列的构成部分,还需要说明的是,时间序列分解实际上有两类通用形式。我们先从加性模型开始。

加性分解

下面是加性分解的公式:

请注意公式中的加号。它的含义是:将趋势与季节性相加,再加上残差,就能还原原始时间序列。一般来说,当季节性波动在时间上基本保持恒定时,我们使用加性分解。

乘性分解

下面是乘性分解的公式。请注意该版本中的乘号。

当季节性波动会随趋势强度而变化时,我们使用乘性模型。也就是说,当您观察到季节性效应的幅度会随着整体序列的升降而变大或变小时。我们在这里描述的,实际上是一种异方差性。

时间序列分解技术

到目前为止,我们已经介绍了时间序列分解包含的要素,以及两种主要的通用类型。接下来,我们要讨论最常见的两种方法。每种方法都可以采用加性或乘性模型。

经典分解

经典分解更容易理解。它起源于 20 世纪 20 年代,但至今仍然适用。为了展示经典分解,我们用 Excel 演示。经典分解(与我们稍后要展示的 STL 一样)既可用于加性模型,也可用于乘性模型。本示例中,我们创建加性版本。

如果您想亲自尝试,可使用 Kaggle 上这个很有意思的咖啡销售数据集中的 index_1 CSV。供您参考,我们已按日聚合并汇总了销售额。

  • 首先,我们计算趋势-周期。我们识别出每周季节性,而一周有七天,因此季节周期为奇数,这意味着应使用对称窗口进行中心化移动平均,即 7 天移动平均。请务必注意 Excel 表中的对齐与中心化。

  • 接着,通过用原始序列减去趋势,得到去趋势后的序列。

  • 然后,为求季节性组件,我们对每个季节周期内的去趋势值取平均。一个重要补充:您会发现季节性组件的和很可能并不严格等于 0。请务必通过归一化进行调整:

    • 计算季节性组件的均值。

    • 用每个季节性取值减去该季节性均值。

  • 最后,对于余项(残差),我们用原始序列减去趋势与季节性组件。

classical additive time series decomposition in Excel

如果我们发现序列的季节性效应会随数据水平变化,就应考虑使用乘性分解。乘性分解的步骤大致相同,只是将减法替换为除法。具体而言,我们会通过用原始序列除以趋势-周期来得到去趋势序列。随后,为求余项,我们将原始序列除以趋势与季节性组件的乘积。(或者,我们也可以对原始序列取自然对数,再应用加性分解。两者在功能上等价。)

STL 分解

STL 分解指基于 LOESS 的季节-趋势分解,是一种更现代的分解方法。它更难解释,但通常比经典方法更灵活。STL 使用一种称为 LOESS 的方法:对数据的多个重叠小片段拟合简单回归模型,并在各个局部加权回归中分别提取趋势与季节性组件。

该方法解决了经典分解的特定缺点,即在经典方法中,首尾观测值无法得到趋势-周期或余项估计。您可以在上面的 Excel 空白单元格(第 3、4、5 行以及第 20、21、22 行)中看到这一点。

还需说明的是,经典分解所识别的趋势-周期会平滑快速的升降,因此可能掩盖序列中发生的重要变化。最后,经典分解默认存在固定的季节性模式,但在数据量足够大时,我们有时会看到季节性模式发生变化。

我们先为原始序列绘制时间图。与之前一样,第一步是对 Kaggle 数据集进行聚合与汇总,以得到我们关注的序列。

coffee_sales_data <- read.csv("/Users/x/Desktop/index_1.csv")
coffee_sales_data <- as.data.frame(coffee_sales_data)
library(dplyr)
library(ggplot2)

coffee_sales_data |>
  group_by(date) |>
  summarize(money = sum(money)) |>
  ggplot(aes(x = as.Date(date), y = money)) +
  geom_line(color = '#01ef63') +
  labs(title = "Coffee Sales") +
  labs(subtitle = "Time series")

接下来,我们将序列分解为趋势、季节性和余项。分解图顶部的框显示了原始序列,只是略显压缩。

coffee_sales_data |>
  group_by(date) |>
  summarize(money = sum(money)) |>
  mutate(date = as.Date(date)) |>
  as_tsibble() |>
  filter(date > '2024-02-28') |>
  filter(date < '2024-05-01') |>
  model(
      STL(money ~ season(window = "periodic"))
  ) |>
  components() |>
  autoplot(color = '#203147') +
  labs(title = "Coffee Sales") +
  labs(subtitle = "Classical additive decomposition")

coffee_sales_data |>
  group_by(date) |>
  summarize(money = sum(money)) |>
  mutate(date = as.Date(date)) |>
  as_tsibble() |>
  filter(date > '2024-02-28') |>
  filter(date < '2024-05-01') |>
  model(
      STL(money ~ season(window = "periodic"))
  ) |>
  components() -> ts_components

ts_components |>
  autoplot(color = '#203147') +
  labs(title = "Coffee Sales") +
  labs(subtitle = "Classical additive decomposition")

请注意,与经典分解不同,我们得到的数据框在起始或末尾没有缺口。

print(ts_components)

STL 分解也存在一些缺点,如果您感兴趣,我们已将相关信息放在常见问题中。

时间序列分解与季节性调整

很多时候,创建季节性调整版本的数据很有帮助。时间序列分解对此大有裨益。要创建季调版本,我们只需将趋势与余项相加;或者从原始序列中减去季节性组件;两种做法最终等价。

seasonally_adjusted <- components |>
  mutate(seasonally_adjusted = money - season_week)

season_adjust_data <- seasonally_adjusted |>
  select(date, money, season_adjust) |>
  pivot_longer(cols = c(money, season_adjust), names_to = "series", values_to = "value")

season_adjust_data |>
  autoplot() + facet_wrap(~series) +
  scale_color_manual(values = c(
    "money" = '#203147',          # blue
    "season_adjust" = '#01ef63'   # orange
  )) +
  facet_wrap(~series) +
  labs(title = "Coffee Sales: Original vs Seasonally Adjusted")

我们发现,在这个特定数据集中,季节性并不算特别强,这本身也很有意思。

季调数据的价值在于,它能帮助我们理解序列的某些特征。首先,我们可以判断潜在趋势是否由季节性驱动。其次,我们可以更仔细地审视数据中的异常事件。我们认为周五的促销之所以有效,是因为“周五”,还是因为“促销”?借助时间序列分解与季节性调整,我们可以检验这类问题。

时间序列分解与预测

您也许会惊讶于时间序列分解对预测同样有帮助。我们主要从两方面思考。其一,了解时间序列的组成部分,有助于我们选择合适的预测模型。如果季节性很强,我们可能会倾向于使用 Holt-Winters 指数平滑或 ETS 方法,这些方法专门用于处理趋势和季节性。

其二,我们甚至可以在预测中直接使用分解后的数据,从而获得更多选项。例如,我们可以对季调后的数据运行非季节性预测模型(稍后再把季节性组件加回去)。当我们想使用更灵活的模型(如标准 ARIMA 和 线性回归)时,季节性调整尤其有用。

下面,我们创建一个简单的 ARIMA 模型来预测季调后的数据。

season_adjust_data_filtered <- season_adjust_data |>
  filter(series == "season_adjust") |>
  as_tsibble(index = date)

seasonal_adjust_arima_model <- season_adjust_data_filtered |>
  model(
    ARIMA(value)
  )

seasonal_adjust_arima_model |>
  forecast(h = "14 days") |>  
  autoplot(season_adjust_data_filtered, color = '#01ef63') +
  labs(title = "Forecast of Seasonally Adjusted Coffee Sales",
       subtitle = "Simple ARIMA Model",
       y = "Money (Seasonally Adjusted)",
       x = "Date")

我们也可以先对季调序列进行预测,再把季节性组件加回去,从而得到带季节性的预测。所有这一切都得益于最初的分解。

# Build tagged historical and forecast data
historical_data <- components |>
  as_tibble() |>
  select(date, money) |>
  mutate(source = "historical")

forecast_data <- final_forecast |>
  as_tibble() |>
  transmute(date, money = money_forecast) |>
  mutate(source = "forecast")

# Bind and create tsibble
full_data <- bind_rows(historical_data, forecast_data) |>
  as_tsibble(index = date, validate = FALSE)

# Plot with color by source
full_data |>
  ggplot(aes(x = date, y = money, color = source)) +
  geom_line() +
  scale_color_manual(values = c(
    "historical" = "#203147",   # dark blue for real data
    "forecast" = "#01ef63"      # green for forecast
  )) +
  labs(
    title = "Coffee Sales: Actual and Forecast",
    subtitle = "Historical vs Forecast",
    y = "Money",
    x = "Date"
  )

结语

希望您喜欢这篇时间序列分解的文章!请记住,分解只是时间序列分析的一部分。若想进一步精进,欢迎报名由 Rob Hyndman 教授主讲的 Forecasting in R 课程,他也参与了我们用于可视化的 R 包的开发。如果您对本文的内容感兴趣,但更偏好 Python,我们也提供 Visualizing Time Series Data in Python 课程,非常适合此类任务。

Josef Waples

作者

Josef Waples

我是数据科学编辑,曾为科学期刊发表的研究论文撰稿。我的兴趣领域包括线性代数、统计学、R 等相关主题。

Laiba Siddiqui

作者

Laiba Siddiqui

我是内容策略专家,热衷于将复杂主题化繁为简。我曾帮助 Splunk、Hackernoon 和 Tiiny Host 等公司为其受众打造兼具吸引力与信息量的内容。

常见问题

什么是时间序列分解?

时间序列分解是将时间序列拆分为三个可解释组件的过程:趋势、季节性和残差(或不规则组件)。趋势展示长期演变,季节性捕捉重复的短期循环,残差反映随机噪声或意外事件。分解有助于分析师更好地理解数据结构,并通过分别建模各组件来提升预测效果。

为何经典分解在序列的开头和结尾会出现缺失值?

中心化移动平均需要点位两侧对称的数据。对于 7 天移动平均,无法为最前 3 天和最后 3 天计算中心化值。因此,经典分解无法在序列边缘给出估计,导致趋势、季节性与残差组件在边界处出现缺口。

何时应使用加性分解与乘性分解?
  • 当季节性波动大致恒定时,使用加性分解。
  • 当季节性组件随趋势增减、即随数据水平变化而缩放时,使用乘性分解。这常见于具有非恒定方差的异方差时间序列。

能否在加性与乘性分解之间转换?

可以。常见做法是对序列取对数,再应用加性分解。这在原始尺度上与乘性模型在数学上等价。它可以简化建模并稳定方差。

分解得到的残差是否总是服从高斯分布?

不能绝对如此。尽管STL 分解通常假设残差近似正态分布,但在真实数据中并非总是如此。残差可能偏斜、峰度较高,或受离群点与结构性突变影响。不过,如果分解拟合良好,余项往往近似于高斯噪声。

经典分解有哪些缺点?

经典分解有以下局限:

  • 边缘效应:如上所述,无法在序列开头或结尾估计趋势-周期或残差。

  • 固定季节性:假设季节性模式稳定,随时间不变。

  • 对离群值敏感:由于基于移动平均与简单平均,更易受极端值干扰而失真。

  • 灵活性有限:使用预设的平滑规则,难以很好地适应非常规模式。

为何 STL 分解优于经典分解?

STL 解决了许多经典分解的不足:

  • 可处理非常量季节性,即模式随时间变化。

  • 在边界处提供估计,避免边缘缺失值。

  • 允许用户自定义平滑参数,对各组件的平滑程度有更大控制。

  • 通过鲁棒拟合选项对离群值更稳健。

STL 分解是否存在局限?

是的。尽管 STL 更灵活,但也有一些注意点:

  • STL 只支持加性分解,不支持乘性。如果存在乘性行为,需要进行对数变换。

  • 需要对季节性与趋势窗口进行参数调优以获得最优结果。

  • 它假设周期性季节性,若季节期不规则或变化,不进行调整就可能难以捕捉。

STL 分解中的 LOESS 是什么?

LOESS(局部加权回归平滑)是一种局部回归,在 STL 分解中用于在数据子集上估计平滑曲线。它不是拟合单一的全局趋势,而是在重叠的窗口上拟合许多小而简单的回归,然后将这些局部拟合结果组合,形成趋势与季节性组件的平滑估计。这使 LOESS 尤其适合捕捉时间序列中渐变与不规则模式。

什么是时间序列分析中的移动平均?

**移动平均**是一种平滑技术,用于减少短期波动、突出长期趋势。它通过对固定窗口(如 7 天或 12 个月)内的数值求平均,并在序列上“滑动”该窗口来实现。例如,7 天移动平均通过将某一点与其前后各三天的数值取平均来平滑该点。移动平均常用于在经典分解中估计趋势组件。

什么是分解模型?

分解模型是一种将时间序列拆分为不同部分的方法,每部分解释数据中的一种模式。通常包括趋势(长期方向)、季节性(重复的短期循环)和残差(随机噪声或不规则事件)。分解模型主要有两种类型:加性模型(各部分相加)与乘性模型(各部分相乘)。二者的选择取决于季节性变化的幅度是保持恒定,还是随数据总体水平而扩大。

主题