时间序列之季节性

什么是季节性?

当一个时间序列的均值有规律的、周期性的变化时,我们就说这个时间序列表现出季节性。季节性的变化通常是遵循时间的——以一天、一周或一年为单位重复。

在这里插入图片描述

时间序列中的四种季节性模式

这里介绍两种季节性特征。一种适用于观测较少的季节,例如在一周中每日观测的序列;另一种是傅里叶特征,适用于观测较多的季节,例如在一年中每日观测的序列。


季节图和季节指标

类似于用移动平均图来发现时间序列的趋势,我们可以使用季节图来发现时间序列的季节模式。

季节性图将时间序列按固定周期分割成了片段,这些片段对应于一个相同的周期,这个周期就是我们想要观察的**“季节”**。下图为维基百科关于三角学文章的日浏览量的季节图,周期为一周。

在这里插入图片描述

该时间序列有一个明显的季节性模式,即工作日升高,周末较低。

季节指标是二元特征,表示时间序列水平的季节差异,将季节性时期视为分类特征并进行独热编码,就可以得到季节性指标。通过对一周中的天数进行独热编码,我们得到每周的季节性指标,即六个新的“虚拟特征”。(如果去掉其中一个指标,线性回归的效果会更好,这里剔除了星期一):

DateTuesdayWednesdayThursdayFridaySaturdaySunday
2016-01-040.00.00.00.00.00.0
2016-01-051.00.00.00.00.00.0
2016-01-060.01.00.00.00.00.0
2016-01-070.00.01.00.00.00.0
2016-01-080.00.00.01.00.00.0
2016-01-090.00.00.00.01.00.0
2016-01-100.00.00.00.00.01.0
2016-01-110.00.00.00.00.00.0

在训练集数据中加入季节性指标有助于模型区分季节期间的均值:

在这里插入图片描述

普通线性回归学习了季节中每个时间点的均值

傅里叶特征和周期图

傅里叶特征更适用于长季节的观测,它不会用独热编码的方式为每个日期创建一个特征,而是试图用几个特征来捕捉季节曲线的整体形状。

在这里插入图片描述

维基三角学序列的年度季节性

从图中我们可以发现三角学序列在不同频率上的重复:一年中三次长时间的上下运动,一年 52 次短时间的每周运动,也许还有其他频率。现在我们试图用傅里叶特征来捕捉一个季节内的这些频率,采用三角函数正弦和余弦的曲线。

傅里叶特征是一对正弦和余弦曲线,从最长的季节开始,每个潜在频率对应一对,模拟年度季节性的傅里叶对将具有的频率:每年一次、每年两次、每年三次,等等。

在这里插入图片描述

上图:每年一次的频率, 下图:每年两次的频率

如果我们将一组这样的正弦/余弦曲线添加到我们的训练数据中,线性回归算法将计算出适合目标序列中季节成分的权重。下图中给出了如何使用四个傅里叶对来模拟维基三角学序列中的年度季节性。

在这里插入图片描述

上图:四个傅里叶对的曲线,带回归系数的正弦和余弦之和, 下图:这些曲线的综合近似于季节模式

我们发现只需要 8 个特征(4 个正弦/余弦对)就可以很好地估计年度季节性,与此相比,季节性指标方法需要数百个特征(一年中的每一天特征)。通过傅里叶特征我们只模拟了季节性的“主效应”,通常需要在训练数据中添加更少的特征,这意味着减少了计算时间和降低了过拟合的风险。

用周期图选择傅里叶特征

应该在特征集中包含多少傅里叶对?通常我们用周期图来回答这个问题,周期图给出了时间序列中频率的强度,具体来说,下图中 y 轴上的值为 (a ** 2 + b ** 2) / 2,其中 ab 是该频率下的正弦和余弦的系数。

在这里插入图片描述

维基三角学序列的周期图

可以看出,从左到右,周期图在 Quarterly 之后下降,一年四次。这就是为什么我们选择了四个傅里叶对来模拟每年的季节,这里忽略了“每周”的频率,因为它更适合用独热编码的季节指标来建模。

计算傅里叶特征

尽管 statsmodel 库已经对傅里叶特征的计算进行了完美的封装,这里还是给出了逐步的计算过程,以便帮助我们加深对细节的了解,这里给出了如何从时间序列的索引中推到出一组傅里叶特征。

import numpy as np
import pandas as pd

def fourier_features(index, freq, order):
    time = np.arange(len(index), dtype=np.float32)
    k = 2 * np.pi * (1 / freq) * time
    features = {}
    for i in range(1, order+1):
        features.update({
            f"sin_{freq}_{i}": np.sin(i * k),
            f"cos_{freq}_{i}": np.cos(i * k),
        })
    return pd.DataFrame(features, index=index)

# 利用 4 个傅里叶特征对计算一年中每日观察的时间序列的季节性
# fourier_features(y, freq=365.25, order=4)

示例:隧道交通

这里定义了两个函数:seasonal_plotplot_periodogram

from pathlib import Path
from warnings import simplefilter

import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
from sklearn.linear_model import LinearRegression
from statsmodels.tsa.deterministic import CalendarFourier, DeterministicProcess

simplefilter("ignore")

# Configuration
simplefilter("ignore")

sns.set(style="whitegrid")
plt.rc("figure", autolayout=True, figsize=(11, 5))
plt.rc(
    "axes",
    labelweight="bold",
    labelsize="large",
    titleweight="bold",
    titlesize=14,
    titlepad=10,
)
plot_params = dict(
    color="0.75",
    style=".-",
    markeredgecolor="0.25",
    markerfacecolor="0.25",
    legend=False,
)
%config InlineBackend.figure_format = "retina"


def seasonal_plot(X, y, period, freq, ax=None):
    if ax is None:
        _, ax = plt.subplots()
    palette = sns.color_palette("husl", n_colors=X[period].nunique(),)
    ax = sns.lineplot(
        x=freq,
        y=y,
        hue=period,
        data=X,
        ci=False,
        ax=ax,
        palette=palette,
        legend=False,
    )
    ax.set_title(f"Seasonal Plot ({period}/{freq})")
    for line, name in zip(ax.lines, X[period].unique()):
        y_ = line.get_ydata()[-1]
        ax.annotate(
            name,
            xy=(1, y_),
            xytext = (6, 6),
            color=line.get_color(),
            xycoords=ax.get_yaxis_transform(),
            textcoords="offset points",
            size=14,
            va="center",
        )
    return ax


def plot_periodogram(ts, detrend='linear', ax=None):
    from scipy.signal import periodogram
    fs = pd.Timedelta('365D') / pd.Timedelta('1D')
    freqencis, spectrum = periodogram(
        ts,
        fs=fs,
        detrend=detrend,
        window='boxcar',
        scaling='spectrum',
    )
    if ax is None:
        _, ax = plt.subplots()
    ax.step(freqencis, spectrum, color='purple')
    ax.set_xscale('log')
    ax.set_xticks([1, 2, 4, 6, 12, 26, 52, 104])
    ax.set_xticklabels(
        [
            'Annual (1)',
            'Semiannual (2)',
            'Quarterly (4)',
            'Bimonthly (6)',
            'Monthly (12)',
            'Biweekly (26)',
            'Weekly (52)',
            'Semiweekly (104)',
        ],
        rotation=30
    )
    ax.ticklabel_format(axis='y', style='sci', scilimits=(0, 0))
    ax.set_ylabel('Variance')
    ax.set_title('Periodogram')
    return ax


data_dir = Path('data/ts-course-data')
tunnel = pd.read_csv(data_dir / 'tunnel.csv', parse_dates=['Day'])
tunnel = tunnel.set_index('Day').to_period('D')
X = tunnel.copy()

# days within a week
X['day'] = X.index.dayofweek   # the x-axis (freq)
X['week'] = X.index.week       # the seasonal period (period)

# days within a year
X['dayofyear'] = X.index.dayofyear
X['year'] = X.index.year

fig, (ax0, ax1) = plt.subplots(2, 1, figsize=(11, 6))
seasonal_plot(X, y='NumVehicles', period='week', freq='day', ax=ax0)
seasonal_plot(X, y='NumVehicles', period='year', freq='dayofyear', ax=ax1)
<Axes: title={'center': 'Seasonal Plot (year/dayofyear)'}, xlabel='dayofyear', ylabel='NumVehicles'>

在这里插入图片描述

# 看一下周期图
plot_periodogram(tunnel.NumVehicles)
<Axes: title={'center': 'Periodogram'}, ylabel='Variance'>

在这里插入图片描述

从周期图可以发现:周季较强,年季较弱。周季的建模我们采用季节指标,年季的建模采用傅里叶特征,周期图在双月 Bimonthly(6) 和 Monthly(12) 之间下降,所以我们使用 10 个傅里叶对。季节性特征的创建使用 DeterministicProcess

from statsmodels.tsa.deterministic import CalendarFourier, DeterministicProcess

fourier = CalendarFourier(freq='A', order=10)      # 10 sin/cos pairs for 'A'nnual seasonality

dp = DeterministicProcess(
    index=tunnel.index,                        
    constant=True,                             # dummy feature for bias (y-intercept)
    order=1,                                   # trend (order 1 means linear)
    seasonal=True,                             # weekly seasonality (indicators)
    additional_terms=[fourier],                # annual seasonality (fourier)
    drop=True,                                 # drop terms to avoid collinearity
)

X = dp.in_sample()
X.head()
consttrends(2,7)s(3,7)s(4,7)s(5,7)s(6,7)s(7,7)sin(1,freq=A-DEC)cos(1,freq=A-DEC)sin(2,freq=A-DEC)cos(2,freq=A-DEC)sin(3,freq=A-DEC)cos(3,freq=A-DEC)sin(4,freq=A-DEC)cos(4,freq=A-DEC)sin(5,freq=A-DEC)cos(5,freq=A-DEC)sin(6,freq=A-DEC)cos(6,freq=A-DEC)sin(7,freq=A-DEC)cos(7,freq=A-DEC)sin(8,freq=A-DEC)cos(8,freq=A-DEC)sin(9,freq=A-DEC)cos(9,freq=A-DEC)sin(10,freq=A-DEC)cos(10,freq=A-DEC)
Day
2003-11-011.01.00.00.00.00.00.00.0-0.8674560.497513-0.863142-0.5049610.008607-0.9999630.871706-0.4900290.8587640.512371-0.0172130.999852-0.8758920.482508-0.854322-0.5197440.025818-0.9996670.880012-0.474951
2003-11-021.02.01.00.00.00.00.00.0-0.8587640.512371-0.880012-0.474951-0.043022-0.9990740.835925-0.5488430.8996310.4366510.0859650.996298-0.8115390.584298-0.917584-0.397543-0.128748-0.9916770.785650-0.618671
2003-11-031.03.00.01.00.00.00.00.0-0.8498170.527078-0.895839-0.444378-0.094537-0.9955210.796183-0.6050560.9338370.3576980.1882270.982126-0.7354170.677615-0.963471-0.267814-0.280231-0.9599330.668064-0.744104
2003-11-041.04.00.00.01.00.00.00.0-0.8406180.541628-0.910605-0.413279-0.145799-0.9893140.752667-0.6584020.9611300.2760970.2884820.957485-0.6486300.761104-0.991114-0.133015-0.425000-0.9051930.530730-0.847541
2003-11-051.05.00.00.00.01.00.00.0-0.8311710.556017-0.924291-0.381689-0.196673-0.9804690.705584-0.7086270.9813060.1924520.3856630.922640-0.5524350.833556-0.9999910.004304-0.559589-0.8287700.377708-0.925925
# 进行 90 天的预测
y = tunnel['NumVehicles']

model = LinearRegression(fit_intercept=False)
_ = model.fit(X, y)

y_pred = pd.Series(model.predict(X), index=y.index)
X_fore = dp.out_of_sample(steps=90)
y_fore = pd.Series(model.predict(X_fore), index=X_fore.index)

ax = y.plot(color='0.25', style='.', title='Tunnel Traffic - Seasonal Forecast')
ax = y_pred.plot(ax=ax, label='Seasonal')
ax = y_fore.plot(ax=ax, label='Seasonal Forecast', color='C3')
_  = ax.legend()

在这里插入图片描述

  • 8
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值