时间序列预测的目标变换
在上一章中,我们深入探讨了如何通过特征工程技术进行时间嵌入和时延嵌入。但这只是回归方程的一侧——特征。我们常常发现,方程的另一侧——目标——并不能按照我们的期望表现。换句话说,目标不具备一些理想特性,这些特性本可以使预测更加容易。该领域的一个主要问题是平稳性——更确切地说,是缺乏平稳性。这会与我们开发机器学习(ML)/统计模型时所做假设产生冲突。在本章中,我们将探讨一些处理此类目标问题的技术。
在本章中,我们将涵盖以下主题:
- 处理时间序列中的非平稳性
- 检测和纠正单位根
- 检测和校正趋势
- 检测与校正季节性
- 检测和修正异方差性
- 目标变换的 AutoML 方法
技术要求
你需要按照本书前言中的说明设置Anaconda环境,以获得一个包含本书代码所需所有库和数据集的可用环境。任何额外的库将在运行notebook时安装。
在使用本章代码之前,你需要运行以下notebooks:
02-Preprocessing_London_Smart_Meter_Dataset.ipynb位于Chapter02文件夹中01-Setting_up_Experiment_Harness.ipynb位于Chapter04文件夹中01-Feature_Engineering.ipynb位于Chapter06文件夹中
本章的配套代码可在https://github.com/PacktPublishing/Modern-Time-Series-Forecasting-with-Python-/tree/main/notebooks/Chapter07找到。
检测时间序列的非平稳性
平稳性是大多数计量经济学模型中一个普遍的假设,是一个严谨的数学概念。但不用深究太多数学,我们可以直观地将平稳性理解为:时间序列所采样的分布的统计特性随时间保持恒定。这在时间序列回归中也很重要,因为我们在跨时间估计一个单一的预测函数。如果时间序列的行为随时间变化,那么我们估计的单一函数可能并不始终相关。例如,将一天中附近公园的访客数量视为一个时间序列,我们知道这些模式在疫情前和疫情后会有很大不同。在机器学习领域,这种现象被称为概念漂移。
直观上,我们可以理解预测平稳序列比预测非平稳序列更容易。但关键点在于:在现实世界中,几乎所有的时间序列都不满足平稳性假设——更具体地说,不满足严格平稳性假设。严格平稳性是指所有统计特性(如均值、方差、偏度等)都不随时间变化。很多时候,这种严格平稳性假设被放宽为弱平稳性,我们只要求时间序列的均值和方差不随时间变化。
我们可以问自己以下四个主要问题来检查时间序列是否平稳:
- 均值是否随时间变化?换句话说,时间序列中是否存在趋势?
- 方差是否随时间变化?换句话说,时间序列是否具有异方差性?
- 时间序列的均值是否表现出周期性变化?换句话说,时间序列中是否存在季节性?
- 时间序列是否具有单位根?
在这些问题中,前三个可以通过简单的视觉检查来确定。单位根更难理解。我们很快会更深入地研究单位根。让我们看几个时间序列,检查我们是否可以通过视觉判断它们是否平稳(你可以记下你的答案):

图7.1:测试你对平稳性的理解
现在,检查你的回答,看看你猜对了多少。如果你在六个中至少答对了四个,那么你对平稳性的直觉相当不错:
- 时间序列1是平稳的,因为它是一个白噪声过程,根据定义,具有零均值和恒定方差。它通过了我们前三个问题的检查清单。
- 时间序列2是非平稳的,因为它有明显的向下线性趋势。这意味着序列开始时的均值与末尾时的均值不同。因此,它未通过我们清单中的第一个问题。
- 时间序列3乍看可能是平稳的,因为它基本上围绕0振荡,但随着时间推移,振荡幅度变大。这意味着它具有递增的方差——换句话说,它是异方差的。因此,虽然这回答了我们的第一个问题,但它没有通过恒定方差的第二个检查。所以,它是非平稳的。
- 现在,我们来看看问题孩子——时间序列4。乍一看,我们可能认为它是平稳的,因为尽管开始时有一个趋势,但它又逆转了趋势,使得均值几乎恒定。而且方差也没有明显的大幅变化。但这是一个具有单位根的时间序列(我们稍后将在单位根部分详细讨论),通常,单位根时间序列很难通过视觉判断。
- 时间序列5回答了前两个问题——恒定均值和恒定方差——但它有一个非常明显的季节性模式,因此是非平稳的。
- 时间序列6是另一个白噪声过程,只是为了迷惑你。它同样是平稳的。
当我们有成百上千甚至数百万个时间序列时,实际上无法通过目视检查来确定它们是否平稳。因此,现在我们来看几种使用统计检验检测这些关键属性的方法,以及如何尝试纠正它们。
尽管我们讨论的是纠正或使时间序列平稳,但在机器学习范式中并不总是必须这样做,因为其中一些问题可以通过在模型中使用正确的特征来处理。是否使序列平稳是我们必须在实验技术后做出的决定。这是因为,正如你将看到的,虽然使序列平稳有优点,但使用某些技术也有缺点,我们将在详细讨论每种变换时看到这些。
笔记本提示:
要跟随完整代码,请使用Chapter06文件夹中的02-Dealing_with_Non-Stationarity.ipynb笔记本。
检测和纠正单位根
首先讨论单位根,因为这是最常检验平稳性的概念。时间序列分析源于计量经济学和统计学,单位根是直接来自这些领域的概念。
单位根
要完全理解单位根非常复杂,但为了培养一些直觉,我们可以看一个简化版本。考虑一阶自回归模型(AR(1)模型):
$y_t = \phi y_{t-1} + \epsilon_t$,其中$\epsilon_t$是白噪声,$\phi$是AR系数。
如果我们考虑方程中$\phi$的不同值,可以得出三种情景(图7.2):
- $|\phi| \gt 1$:当$|\phi|$大于1时,时间序列中的每个后续值都乘以一个大于1的数,这意味着序列将具有强烈且快速增加/减少的趋势,从而成为非平稳的。
- $|\phi| \lt 1$:当 $|\phi|$ 小于 1 时,时间序列中的每个后续值都乘以一个小于 1 的数,这意味着长期来看,序列的均值趋近于零并围绕其振荡。因此,它是平稳的。
- $|\phi| = 1$:当 $|\phi|$ 等于 1 时,情况变得更棘手。当 $|\phi| = 1$ 时,对于
AR(1)模型,这被称为具有单位根,方程变为 $y_t = y_{t-1} + \epsilon_t$。这在计量经济学中称为随机游走,是金融和经济领域中非常流行的一种时间序列。从数学上可以证明,此类序列具有常数均值但方差非常数:

图7.2:具有不同 $\phi$ 参数的自回归时间序列。上:情景1,phi <1;中:情景2,phi = 1;下:情景3,phi>1
虽然我们讨论了 AR(1) 过程中的单位根,但可以将同样的直觉扩展到多个滞后或 AR(p) 模型。计算和检验单位根在那里更复杂,但仍然可行。
那么,现在我们知道了什么是单位根,如何用统计方法检验它?这就是 Dickey-Fuller 检验的用武之地。
增广 Dickey-Fuller (ADF) 检验
该检验的原假设是时间序列的 AR(1) 模型中的 $\phi$ 等于 1,因此是非平稳的。备择假设是 AR(1) 模型中的 $\phi$ 小于 1。ADF 检验在 Dickey-Fuller 检验基础上扩展到 AR(p) 模型,因为大多数时间序列不只是由单个滞后定义的。这是检验单位根的标准且最流行的统计检验。其核心是对时间序列的滞后进行回归,并计算残差方差的统计量。
让我们看看如何在 Python 中使用 statsmodels 实现:
from statsmodels.tsa.stattools import adfuller
result = adfuller(y)
result 来自 adfuller 是一个元组,包含 检验统计量、p 值和不同置信水平下的 临界值。这里我们最关心的是 p 值,它是一种简单实用的方法来判断是否拒绝原假设。如果 p < 0.05,则有 95% 的概率该序列没有单位根;从单位根角度看,该序列是平稳的。
为了使这一过程更简单,我们在 src.transforms.stationary_utils 中包含了一个名为 check_unit_root 的方法,它可以为你进行推断(将返回的概率与置信度比较,并拒绝或接受 null 假设),并返回一个 namedtuple,其中包含一个名为 stationary 的布尔属性,以及来自 statsmodels 的完整结果(在 results 中):
from src.transforms.stationary_utils import check_unit_root
# We pass the time series along with the confidence with which we need the results
check_unit_root(y, confidence=0.05)
既然我们已经学会了如何检查一个序列是否具有单位根,那如何使其平稳呢?让我们看看几种有助于实现这一点的变换。
差分变换
差分变换是一种非常流行的变换,用于使时间序列平稳,或至少消除单位根。其概念很简单:我们将时间从观测域变换到观测变化域。差分变换将后续观测值相减:
zt = yt – – yt-1
差分有助于稳定时间序列的均值,从而减少或消除趋势和季节性。让我们看看差分如何使序列平稳。
设所讨论的时间序列为 $y_t = \beta_0 + \beta_1 t + \epsilon_t$ ,其中 $\beta_0$ 和 $\beta_1$ 是系数, $\epsilon$ 是白噪声。从这个方程中,我们可以看到时间 t 是方程的一部分,这使得 yt 成为一个带有趋势的时间序列。因此,差分后的时间序列 z 如下:
$$z_t = y_t = y_{t-1}$$
$$= (\beta_0 + \beta_1 t + \epsilon_t) - (\beta_0 + \beta_1 (t - 1) + \epsilon_{t-1})$$
$$= \beta_1 + (\epsilon_t - \epsilon_{t-1})$$
我们需要在这个新方程中寻找的是,没有提到 t。这意味着对 t 的依赖(它创造了趋势)已被移除,现在时间序列在任何时间点都具有常数均值和方差。
差分不能消除所有类型的非平稳性,但对大多数时间序列有效。不过,这种方法也有一些缺点。其中之一是我们在建模时会丢失时间序列的尺度。很多时候,时间序列的尺度包含一些对预测有用的信息。例如,在供应链中,销量较高的SKU与销量较低的SKU表现出的模式不同,而当我们进行差分时,这种区分的相关信息就丢失了。
另一个缺点更多是从操作角度而言。当我们使用差分进行预测时,在从模型获得差分输出后,还需要对变换进行逆变换。这是一个需要管理的额外复杂层面。一种方法是将最近一次观测值保存在内存中,并不断加上差值以逆变换。另一种方法是针对每个需要逆变换的 t,准备好 yt-1,并不断将差值加到 yt-1 上。
我们已在本书GitHub仓库的 src.transforms.target_transformations.py 中实现了后一种方法,使用日期时间索引作为键来对齐和获取 yt-1 观测值。让我们看看如何使用它:
from src.transforms.target_transformations import AdditiveDifferencingTransformer
diff_transformer = AdditiveDifferencingTransformer()
# [1:] because differencing reduces the length of the time series by one
y_diff = diff_transformer.fit_transform(y, freq="1D")[1:]
y_diff 将得到变换后的序列。要还原回原始时间序列,我们可以调用 inverse_transform 使用 diff_transformer。随附的笔记本包含了示例和图表,展示了差分如何改变时间序列。
这里,我们将差分视为减去时间序列中后续值的过程。但也可以使用其他算子进行差分,例如除法(yt / yt-1),这在 src.transforms.target_transformations.py 文件中以 MultiplicativeDifferencingTransformer 实现。我们还可以尝试这些变换,以检查哪种最适合你的数据集。
尽管差分解决了大多数平稳性问题,但它不能保证处理所有类型的趋势(非线性或分段趋势)、季节性等。有时,我们可能不想差分序列,但仍需处理趋势和季节性。因此,让我们看看如何检测和去除时间序列中的趋势。
检测和校正趋势
在第5章《时间序列预测作为回归》中,我们谈到预测是一个难题,因为其本质上是外推问题。趋势是预测成为外推问题的主要因素之一。如果时间序列呈上升趋势,任何试图预测它的模型都需要外推到训练期间所见值范围之外。ARIMA 通过自回归处理此问题,而指数平滑则通过显式建模趋势来处理。但标准回归可能天生不适合外推。然而,通过适当的特征(如滞后),它可以开始做到这一点。
但如果我们能可靠地估计并提取时间序列中的趋势,就可以通过去趋势来简化需要应用回归的问题。
但在继续之前,值得了解两种主要趋势类型。
确定性趋势与随机趋势
让我们再次使用之前看到的简单 AR(1) 模型来建立直觉。之前,我们看到在 AR(1) 模型中包含 $\phi \gt 1$ 会导致时间序列出现趋势。但另一种思考趋势时间序列的方式是,将时间作为序数变量纳入定义时间序列的方程中。例如,考虑两个时间序列:
时间序列1:
$$y_t = \phi y_{t-1} + \epsilon_t; \epsilon_t \sim \mathcal{N}(0, \sigma^2)$$
时间序列2:
$$y_t = \beta_0 + \beta_1 t + \epsilon_t; \epsilon_t \sim \mathcal{N}(0, \sigma^2)$$
这些可以在以下图中看到:

图7.3:上方:随机趋势;下方:确定性趋势
我们之前已经见过这两个方程;时间序列1是AR(1)模型,而时间序列2是我们选择用来演示差分的方程。我们已经知道对于$\phi \gt 1$,时间序列1和时间序列2都有趋势。但这两个趋势之间存在差异。
在时间序列2中,趋势是恒定的,并且可以被完美建模。在这种情况下,仅用线性拟合就能完美解释趋势。但在时间序列1中,趋势无法用简单的线性拟合来解释。它本质上依赖于时间序列的先前值,该值有$\epsilon_{t-1}$,因此是随机的。因此,时间序列2具有确定性趋势,而时间序列1具有随机趋势。
我们可以使用本章之前介绍的ADF检验来检查时间序列是否具有确定性或随机趋势。在不深入统计检验的数学细节的情况下,我们知道它通过将AR(p)模型拟合到时间序列来检验单位根。该检验有几个变体,我们可以通过statsmodels实现中的regression参数来指定。该参数接受以下值:
c:这意味着我们在AR(p)模型中包含一个常数截距。实际上,这意味着即使序列不是围绕零,我们也会将其视为平稳。这是statsmodels中的默认设置。n:这意味着我们在AR(p)模型中甚至不包含常数截距。ct:如果我们提供此选项,AR(p)模型将包含常数截距和线性确定性趋势分量。这意味着即使时间序列中存在确定性趋势,它也会被忽略,并且该序列将被检验为平稳。ctt:这是当我们包含常数截距——即线性二次趋势时。
因此,如果我们使用regression="c"运行ADF检验,结果将是非平稳的。现在,如果我们使用regression="ct"运行ADF检验,结果将是平稳的。这意味着当我们从时间序列中移除确定性趋势后,它变得平稳。正是这个检验可以用来判断我们在时间序列中观察到的趋势是确定性的还是随机性的。在延伸阅读部分,我们提供了一个链接,指向Fabian Kostadinov的一篇博客文章,他在文章中通过几个时间序列实验来清晰区分ADF检验的不同变体。
我们已在 src.transforms.stationary_utils 中将此检验实现为 check_deterministic_trend,它为您进行推断并返回一个带有布尔属性 deterministic_trend 的 namedtuple。namedtuple 还包含我们在 adf_res 和 adf_ct_res 下进行的两个 adfuller 检验的原始结果,以便您进一步研究。让我们看看如何使用此检验:
check_deterministic_trend(y, confidence=0.05)
这将告诉我们趋势是平稳的还是确定性的。现在,让我们看看几种识别和统计检验时间序列中趋势的方法(无论其是否为确定性)。
Kendall's Tau
Kendall's Tau 是一种相关性度量,但在数据的秩上执行。Kendall's Tau 是一种非参数检验,因此不对数据做出假设。相关系数 Tau 返回介于 -1 和 1 之间的值,其中 0 表示无关系,1 或 -1 表示完美关系。我们不会深入探讨 Kendall's Tau 的计算细节或显著性检验的方法,因为这超出了本书的范围。在延伸阅读部分中有一个链接对此进行了详细解释。
在本节中,我们将看到如何使用 Kendall's Tau 来度量时间序列中的趋势。如前所述,Kendall's Tau 计算两个变量之间的秩相关。如果我们将其中一个变量选为时间序列,并将另一个变量设为时间的序数表示,那么得到的 Kendall's Tau 将代表时间序列中的趋势。另一个好处是,Kendall's Tau 的值越高,我们预期趋势越强。
scipy 提供了 Kendall's Tau 的实现,我们可以如下使用:
import scipy.stats as stats
tau, p_value = stats.kendalltau(y, np.arange(len(y)))
我们可以将返回的 p 值与所需的置信度(通常为 0.05)进行比较,并说明如果 p_value < confidence,则我们得出结论,该趋势在统计上显著。tau 的符号告诉我们这是递增趋势还是递减趋势。
我们在 src.transforms.stationary_utils 中将 Kendall's Tau 实现为 check_trend,它为您检查趋势的存在。我们只需要提供以下参数:
y:要检查的时间序列confidence:用于检查结果 p 值的置信水平
还有几个其他参数,但这些是针对 Mann-Kendall(M-K)检验的,将在后面解释。
让我们看看如何使用这个检验:
check_trend(y, confidence=0.05)
该方法还会检查已识别的趋势是确定性的还是随机的,并计算趋势的方向。结果以namedtuple的形式返回,包含以下参数:
trend:这是一个布尔标志,表示是否存在趋势。direction:这将为increasing或decreasing。slope:这是估计趋势线的斜率。对于Kendall's Tau,它将为Tau。p:这是统计检验的p值。deterministic:这是一个布尔标志,表示确定性趋势。
现在,让我们来看Mann-Kendall检验。
Mann-Kendall检验(M-K检验)
Mann-Kendall检验用于检查是否存在单调上升或下降的趋势。由于M-K检验是一种非参数检验,与Kendall's Tau类似,因此不需要正态性或线性假设。该检验通过分析时间序列中连续点之间的符号来进行。检验的核心思想是,在存在趋势的情况下,符号值如果求和,会持续增加或减少。
尽管是非参数的,原始检验中仍有一些假设:
- 时间序列中不存在自相关性
- 时间序列中不存在季节性
多年来,为了应对这些问题,原始检验进行了大量修改,其中许多修改与原始检验一起已在 https://github.com/mmhs013/pyMannKendall 中实现。它们在 pypi 中作为 pymannkendall 提供。
预白化是一种用于去除时间序列中自相关的常用技术。简而言之,其思路如下:
- 用 AR(1) 模型识别 $\phi$,以
- $y_t^{prewhiten} = y_t - \phi * y_{t-1}$
M. Bayazit 和 B. Önöz (2007) 建议,如果样本量大于 50 且趋势足够强 (slope>0.01),则在进行 M-K 检验前不要使用预白化。对于季节性数据,M-K 检验的季节性变体也在 pymannkendall 中实现了。
参考文献核实:
M. Bayazit 和 B. Önöz 的研究论文在参考文献1中引用。
我们之前讨论过的方法 check_trend 也实现了 M-K 检验,可以通过设置 mann_kendall=True 来启用。但需要注意,M-K 检验比 Kendall's Tau 慢得多,尤其是对于长时间序列。M-K 检验还有一些特定参数:
seasonal_period:默认值是None。但如果存在季节性,我们可以在此处提供seasonal_period,然后就会使用 M-K 检验的季节性变体。prewhiten:这是一个布尔标志,用于在应用 M-K 检验前对时间序列进行预白化。默认值是None。在这种情况下,使用我们之前讨论的条件 (N>50), 来决定是否预白化。如果我们在此处明确传递True或False,则会遵循该设置。
让我们看看如何使用这个检验:
check_trend(y, confidence=0.05, mann_kendall=True)
结果以 namedtuple 的形式返回,包含以下参数:
trend:这是一个布尔标志,表示趋势的存在。direction:这将是increasing或decreasing。slope:这是估计趋势线的斜率。对于M-K检验,它是使用Theil-Sen估计器估计的斜率。p:这是统计检验的p值。deterministic:这是一个布尔标志,表示确定性趋势。
让我们来看一个例子,我们将这两个测试应用于一个时间序列(完整代码见02-Dealing_with_Non-Stationarity.ipynb):
# y_unit_root is the a synthetic unit root timeseries
y_unit_root.plot()
plt.show()

图7.4:M-K检验
kendall_tau_res = check_trend(y_unit_root, confidence=0.05)
mann_kendall_res = check_trend(y_unit_root, confidence=0.05, mann_kendall=True)
print(f"Kendalls Tau: Trend: {kendall_tau_res.trend} | Direction: {kendall_tau_res.direction} | Deterministic: {kendall_tau_res.deterministic}")
print(f"Mann-Kendalls: Trend: {mann_kendall_res.trend} | Direction: {mann_kendall_res.direction} | Deterministic: {mann_kendall_res.deterministic}")
## Output
>> Kendalls Tau: Trend: True | Direction: decreasing | Deterministic: False
>> Mann-Kendalls: Trend: True | Direction: decreasing | Deterministic: False
你最好能生成一些时间序列,或者挑选一些你遇到过的时间序列,并使用这些函数来了解它们如何工作以及结果如何帮助你。附带的笔记本提供了一些示例供你入门。你可以观察到不同类型趋势的方向和斜率有何不同。
既然我们知道了如何检测趋势,下面来看看去趋势化。
去趋势化变换
如果趋势是确定性的,去除趋势将对建模过程有所帮助。在第3章《分析与可视化时间序列数据》中,我们讨论了去趋势化,因为它是我们进行分解的一个组成部分。但移动平均或LOESS回归等技术有一个缺点——它们无法外推。但如果我们考虑的是确定性线性(甚至多项式)趋势,则可以通过线性回归轻松估计。这里额外的好处是,识别出的趋势可以很容易地外推。
步骤很简单:我们将时间序列对时间的序数表示进行回归,并提取参数。一旦有了这些参数,就可以使用日期将趋势外推到未来的任何时间点。Python中的核心逻辑如下所示:
# y is the time series we are detrending
x = np.arange(len(y))
# degree is the degree of trend we are estimating. Linear, or polynomial
# Fitting a regression on y using a linearly increasing x
linear_params = np.polyfit(x=x, y=y, deg=degree)
# Extract trend using fitted parameters
trend = get_trend(y)
# Now this extracted trend can be removed from y
detrended = y - trend
我们已经将这一去趋势器作为转换器实现于 src.transforms.target_transformations.py 和 DetrendingTransformer 中。你可以在 GitHub 仓库中查看其实现方式。现在,来看看如何使用它:
from src.transforms.target_transformations import DetrendingTransformer
detrending_transformer = DetrendingTransformer(degree=1)
y_detrended = detrending_transformer.fit_transform(y, freq="1D")
y_detrended 将包含去趋势后的序列。要恢复原始时间序列,我们可以调用 inverse_transform 并使用 detrending_transformer。随附的笔记本包含示例和图形,用于展示去趋势如何改变时间序列。
最佳实践:
我们必须谨慎对待趋势假设,尤其是在进行长期预测时。即使是线性趋势假设也可能导致不切实际的预测,因为在现实世界中趋势不会永远保持同一方向。通常建议用一个因子 $\phi$ 来阻尼趋势,以便在趋势外推时更加保守。这种阻尼可以简单表示为 $f_{t+h}^{damped} = f_{t+h} \times \phi^h, \text{ where } \phi \lt 1$。
导致时间序列非平稳的另一个关键因素是季节性。我们来看看如何识别并移除季节性。
检测与校正季节性
绝大多数现实世界的时间序列都具有季节性,例如零售销售额、能源消耗等。通常,季节性的存在与否是领域知识的一部分。但当我们处理时间序列数据集时,领域知识会变得有些模糊。大多数时间序列可能表现出季节性,但这并不意味着数据集中的每个时间序列都是季节性的。例如,在零售数据集中,有些商品是季节性的,而有些则不是。因此,在
检测季节性
除了直观观察外,有两种流行的方法可以检查季节性:自相关和快速傅里叶变换。这两种方法都能自动识别季节性周期。在我们的讨论中,将介绍自相关方法,并研究如何利用它来确定季节性。
如 第3章《分析与可视化时间序列数据》所述,自相关是时间序列与其滞后值之间的相关性。通常,我们预期在较近的滞后(lag 1、lag 2等)处相关性较高,并随着向后推移逐渐减弱。但对于具有季节性的时间序列,我们还会在季节性周期处看到一个尖峰。
通过一个例子来理解这一点。考虑一个合成时间序列,它由白噪声和周期为25的正弦信号组成(与我们在图7.1中看到的季节性时间序列相同):):
#WhiteNoise + Seasonal
y_random = pd.Series(np.random.randn(length), index=index)
t = np.arange(len(y_random))
y_seasonal = (y_random+1.9*np.cos((2*np.pi*t)/(length/4)))
如果绘制该时间序列的自相关函数(ACF),结果如下(计算和绘制的代码可在 02-Dealing_with_Non-Stationarity.ipynb 笔记本中找到):笔记本):

图7.5:季节周期为25的合成时间序列的自相关图
我们可以看到,除了前几个滞后之外,自相关随着接近季节周期而增加,并在精确的季节周期处达到峰值。我们可以利用ACF的这一特性来检测季节性。darts是一个时间序列预测库,它实现了这种识别季节性的技术。但由于它是为darts的时间序列数据结构设计的,我们已将相同的逻辑适配到常规的pandas系列上,并在src.transforms.stationary_utils.py中以check_seasonality命名。该实现可以进行两种季节性检查。它可以接受一个seasonality_period作为输入,并验证数据中是否存在对应于该seasonality_period的季节性。如果我们没有提前给出seasonality_period,它将返回最短的统计显著的seasonality_period。
该过程在高层次上执行以下操作:
- 它计算ACF。
- 它找出ACF中的所有相对最大值。相对最大值是函数从递增变为递减的点。
- 它检查提供的
seasonal_period是否为相对最大值。如果不是,我们得出结论,没有与seasonality_period相关的季节性。 - 现在,我们假设ACF服从正态分布,并计算指定置信水平的上限。上限由下式给出:
$$UB = z_{1-\frac{\alpha}{2}} \times SE(r_h)$$
其中rh是滞后h的估计自相关,SE是标准误差,$z_{1-\frac{\alpha}{2}}$是基于所需置信度$\alpha$的正态分布分位数。SE使用Bartlett公式进行近似(关于背后的数学原理,请参阅进一步阅读部分)。
- 每个
seasonality_period的候选值都对照此上限进行检查,超过此上限的被认为统计显著。
除了时间序列本身之外,此函数只有三个参数:
max_lag:这指定了在ACF及后续季节搜索中应包含的最大滞后。此值应至少比预期的季节周期大1。seasonal_period:在这里,我们根据领域知识给出对季节周期的直觉判断,函数会为我们验证这一假设。confidence:这是标准的统计置信水平。默认值为0.05。
让我们看看如何在图7.4(季节周期为25)的相同数据上使用此函数。这将返回一个namedtuple,包含seasonal(一个布尔标志,指示是否存在季节性)、seasonal_periods(具有显著季节性的季节周期)作为参数。
# Running the function without specifying seasonal period to identify the seasonality
seasonality_res = check_seasonality(y_seasonal, max_lag=60, confidence=0.05)
print(f"Seasonality identified for: {seasonality_res.seasonal_periods}")
## Output
>> Seasonality identified for: 25
This function can also be used to verify if your assumption about the seasonality is right.
# Running the function specifying seasonal period to verify
seasonality_res = check_seasonality(y_seasonal, max_lag=30, seasonal_period=25, confidence=0.05)print(f"Seasonality Test for 25th lag: {seasonality_res.seasonal}")
## Output
>> Seasonality Test for 25th lag: True
现在我们已经知道如何识别和检验季节性,接下来讨论去季节性。
去季节性变换
在第3章《分析与可视化时间序列数据》中,我们回顾了季节分解技术。此处同样可以使用这些技术,但需做一点调整。之前,我们并不关心将季节性投射到未来。但在预测中使用去季节性时,必须能够将其投射到未来。幸运的是,将季节周期向前投射是简单的。这是因为我们面对的是一个固定的季节轮廓,它会在季节循环中不断重复。例如,如果我们识别了一年中12个月的季节轮廓(每月频率数据的年度季节性),那么这12个月提取的季节性将每12个月重复一次。
利用这一性质,我们在src.transforms.target_transformations.py中实现了一个变换器,即DeseasonalizingTransformer。需要注意以下几个参数和属性:
seasonality_extraction:此变换器支持两种提取季节性的方式——"period_averages"(通过季节平均估计季节轮廓)和"fourier_terms"(通过傅里叶项回归提取季节性)。seasonality_period:根据用于季节性提取的技术,此参数可以是整数或字符串。若为"period_averages",则此参数表示季节周期重复的周期数。若为"fourier_terms",则表示从日期时间索引中提取的季节性。可以使用pandas datetime属性(如week_of_day、month等)来指定最显著的季节性。类似于之前看到的FourierDecomposition,我们也可以省略此参数,并在fit/transform方法的实现中提供自定义季节性。n_fourier_terms:此参数指定回归中包含的傅里叶项数。增加此参数会使拟合的季节性更复杂。- 此实现中没有去趋势处理,因为我们已经看到了
DetrendingTransformer。此实现期望在使用fit函数之前移除任何趋势。
让我们看看如何使用它:
from src.transforms.target_transformations import DeseasonalizingTransformer
deseasonalizing_transformer = DeseasonalizingTransformer(seasonality_extraction="period_averages",seasonal_period=25)
y_deseasonalized = deseasonalizing_transformer.fit_transform(y, freq="1D")
y_deseasonalized将包含去季节化的时间序列。要回到原始时间序列,我们可以使用inverse_transform函数。通常,这可用于在做出预测后重新添加季节性。
最佳实践:
季节性建模可以像这里讨论的那样单独进行,也可以使用我们在本章前面讨论的季节性特征。虽然最终哪种方法效果更好必须针对每个数据集通过经验来确定,但我们可以有一些经验法则/指导方针来决定优先级。
当我们有足够的数据时,让模型将季节性作为主要预测问题的一部分来学习似乎效果更好。但在数据不那么丰富的情况下,在将数据输入机器学习模型之前单独提取季节性效果很好。
当数据集具有不同的季节性(不同时间序列有不同的季节性周期)时,应相应处理。要么分别对每个时间序列进行去季节化,要么将全局机器学习模型拆分为不同的局部模型,每个模型都有自己的季节性模式。
我们之前讨论的最后一个方面是异方差性。让我们也快速看一下。方面我们之前讨论的
检测和修正异方差性
尽管名字吓人,但异方差性是一个足够简单的概念。它源自古希腊语,其中hetero表示不同,skedasis表示离散。顾名思义,异方差性定义为变量的变异性在不同变量之间不同。在时间序列的背景下,我们说时间序列是异方差的,当时间序列的变异性或离散程度随时间变化时。例如,考虑一个家庭多年来的支出。在这些年里,这个家庭从贫困到中产阶级,最后到中上层阶级。当家庭贫困时,支出较少且仅用于必需品,因此支出的变异性较小。但随着他们接近中上层阶级,家庭能够负担奢侈品,这在时间序列中产生了峰值,从而提高了变异性。如果我们回顾图7.1,我们可以看到异方差时间序列的样子。
但除了目视检查外,如果我们能进行自动化的统计检验来确定异方差性,那就太好了。
检测异方差性
检测异方差性的方法有很多,但我们将使用最流行的技术之一,即由 Halbert White 于 1980 年提出的 White 检验。White 检验使用辅助回归任务来检查恒定方差。我们使用一些协变量进行初始回归,并计算该回归的残差。然后,我们拟合另一个回归模型,以这些残差为目标,使用第一次回归中使用的协变量及其平方和交叉乘积。最终统计量通过该辅助回归的 R2 值估计。关于检验的详细说明,请参阅 “进一步阅读” 部分;关于严格的数学过程,研究论文在 “参考文献” 部分中引用。
参考文献核实:
要了解White检验的严格数学过程,请参阅参考文献部分引用的参考文献2中的研究论文。
在时间序列的背景下,我们通过使用确定性趋势模型来调整这一公式。初始回归使用时间作为序数变量进行,残差用于执行White检验。White检验在statsmodels of het_white中有实现,我们将使用它来进行该检验。het_white检验返回两个统计量和p值——Lagrangian Multiplier和F-Statistic。Lagrangian Multiplier检验残差方差与回归模型中自变量之间是否存在关系。F-Statistic将原始模型的拟合度与允许误差方差变化的模型进行比较。任何一个检验的p值小于置信水平都表明存在异方差性。但为保守起见,我们也可以同时使用两个检验,仅当两个p值都小于置信水平时才标记为异方差。
我们将所有这些封装在src.transforms.stationary_utils的一个实用函数中,即check_heteroscedasticity,它只有一个额外参数——confidence。让我们看看该方法在Python中的核心实现:
import statsmodels.api as sm
# Fitting a linear trend regression
x = np.arange(len(y))
x = sm.add_constant(x)
model = sm.OLS(y,x)
results = model.fit()
# Using the het_white test on residuals
lm_stat, lm_p_value, f_stat, f_p_value = het_white(results.resid, x)
# Checking if both p values are less than confidence
if lm_p_value<confidence and f_p_value < confidence:
hetero = True
else:
hetero = False
现在,让我们看看如何使用这个函数:
from src.transforms.stationary_utils import check_heteroscedastisticity
check_heteroscedastisticity(y, confidence=0.05)
这返回一个namedtuple,包含以下参数:
Heteroscedastic:一个布尔标志,指示是否存在异方差性lm_statistic:Lagrangian Multiplier(LM)统计量lm_p_value:与LM统计量相关的p值
最佳实践:
我们进行的异方差性检验仅考虑回归中的趋势,因此在存在季节性的情况下可能效果不佳。建议在应用该函数之前对数据进行去季节化处理。
检测异方差性是较容易的部分。有几种变换试图消除异方差性,但各有优缺点。让我们来看看其中几种变换。
对数变换
对数变换,顾名思义,就是对时间序列取对数。对数变换有两个主要特性——方差稳定化和减少偏度——从而使数据分布更接近正态。其中,我们更关注第一个特性,因为它能对抗异方差性。
对数变换通常被认为能降低数据的方差,从而消除数据中的异方差性。直观上,我们可以将对数变换视为一种将直方图右侧的极端值向内拉,同时将左侧的极低值向外拉伸的操作。
但已有研究表明,对数变换并不总能稳定方差。除此之外,对数变换在机器学习中还带来了另一个挑战。损失优化现在发生在对数尺度上。由于对数变换对数值范围低端的压缩比高端更厉害,学习到的模型对低端误差的敏感度可能低于高端。另一个关键缺点是,对数变换只能应用于严格正的数据。如果你的数据中有零或负数,那么你需要将整个分布平移一个常数M,然后再应用变换。这也会在数据中造成一些扰动,可能产生不利影响。
关键是,我们在应用对数变换时应谨慎。我们在src.transforms.target_transformations.py中实现了一个变换器,即LogTransformer,只有一个参数add_one,它在变换前加一,逆变换后减一。Python中的关键逻辑简单到只需在变换中应用np.log1p或np.log函数,并分别用np.expm1或np.exp进行逆变换。
# Transform
np.log1p(y) if self.add_one else np.log(y)
# Inverse Transform
np.expm1(y) if self.add_one else np.exp(y)y_log = log_transformer.fit_transform(y)
我们所做的只是将其封装成一个简洁易用的变换器。下面来看看如何使用它:
from src.transforms.target_transformations import LogTransformer
log_transformer = LogTransformer(add_one=True)
y_log = log_transformer.fit_transform(y)
y_log是对数变换后的时间序列。我们可以调用inverse_transform恢复原始时间序列。
Box-Cox变换
对数变换虽然有效且常见,但非常强烈。但对数并不是我们唯一可以使用的单调变换。还有许多其他变换,如y2,$\frac{1}{y}$,$\frac{1}{\sqrt{y}}$等等,它们都属于幂变换族。该族中非常著名且广泛使用的一组变换是Box-Cox变换:
$$y(\lambda) = \frac{y^\lambda - 1}{\lambda}, \quad \text{if } \lambda \neq 0$$
以及 $\log(y), \quad \text{if } \lambda = 0$
参考文献核实:
Box和Cox的原始研究论文在参考文献部分的参考文献3中引用。
直观上看,Box-Cox变换是一种广义的对数变换。对数变换只是Box-Cox变换的一个特例(当$\lambda = 0$时)。在$\lambda$的不同取值下,它近似于其他变换,例如当$\lambda = 2$时的y2、$\frac{1}{\sqrt{y}}$(当$\lambda = -0.5$时)、$\sqrt{y}$(当$\lambda = 0.5$时)等等。当$\lambda = 1$时,没有显著的变换。
我们之前提到的对数变换的许多缺点在这里同样存在,但这些影响的程度有所不同,并且我们有一个参数$\lambda$来帮助决定这些影响的合适水平。与对数变换类似,Box-Cox变换也只使用严格正的数据。同样,需要添加一个常数来偏移数据分布。这个参数的另一个方面是,多了一个需要调整的超参数。
有几种自动方法可以找到任何数据分布的最优$\lambda$。其中之一是通过最小化数据分布的对数似然,假设正态性。因此,本质上我们要做的是找到使数据分布最正态的最优$\lambda$。这种优化已经在流行的实现中实现,例如scipy中scipy.special模块中的boxcox函数。
另一种找到最优$\lambda$的方法是使用Guerrero的方法,这特别适用于时间序列。在该方法中,我们不是试图使数据分布符合正态分布,而是试图最小化时间序列中不同同质子序列之间的变异性。这个子序列的定义有些主观,但通常我们可以假设子序列长度为季节长度。因此,我们要最小化的是时间序列在不同季节周期之间的变异性。
参考文献核实:
提出Guerrero方法的研究论文在参考文献部分的参考文献4中引用。
这两种优化方法的工作方式存在显著差异,使用时需要小心。如果我们的主要关注点是消除时间序列的异方差行为,那么可以使用Guerrero的方法。
我们在src.transforms.target_transformations.py中提供了一个名为BoxCoxTransformer的转换器。我们需要了解几个参数和属性:
box_cox_lambda:这是用于Box-Cox变换的$\lambda$参数。如果设置为None,实现将找到最优的$\lambda$。optimization:这可以是guerrero(默认设置)或loglikelihood。这决定了如何估计$\lambda$参数。seasonal_period:这是使用Guerrero方法寻找最优$\lambda$参数的输入。技术上,这是子序列的长度,通常取季节周期。bounds:这是另一个使用 Guerrero 方法控制优化的参数。它是一个元组,包含在搜索最优 $\lambda$ 参数时的下界和上界。add_one:这是一个标志,用于在对序列应用对数变换之前加1,以避免取 log 0。
Transformer 中实现的核心逻辑如下:
## Fit Process
# Add one if needed
y = self._add_one(y)
# Find optimum box cox lamda if optimization is Guerrero
self.boxcox_lambda = self._optimize_lambda(y)
## Transform Process
boxcox(y.values, lmbda=self.boxcox_lambda)
## Inverse Transform
self._subtract_one(inv_boxcox(y.values, self.boxcox_lambda))
现在,让我们看看如何使用它:
from src.transforms.target_transformations import BoxCoxTransformer
boxcox_transformer = BoxCoxTransformer()
y_boxcox = boxcox _transformer.fit_transform(y)
y_boxcox 将包含 Box-Cox 变换后的时间序列。要恢复到原始时间序列,我们可以使用 inverse_transform 函数。
Box-Cox 变换和对数变换都可用于修正异方差性。但是,如前所述,对数变换是一种强变换,而 Box-Cox 变换给我们提供了另一个杠杆来调整和微调变换以适应数据。我们可以将 Box-Cox 变换视为一种灵活的对数变换,可以调整以对数据做出正确的变换。请查看笔记本,您可以在其中查看并尝试这些不同的变换,感受它们对数据的影响。
当我们大规模处理预测问题时,在预测之前需要分析成百上千甚至数百万个时间序列。在这种情况下,需要采用 AutoML 方法才能实用。
目标变换的 AutoML 方法
到目前为止,我们已经讨论了许多使序列更平稳的方法(这里我们用“平稳”一词的非数学含义),例如去趋势、去季节化、差分和单调变换。我们还研究了统计检验来检查时间序列中是否存在趋势、季节性等。因此,自然的下一步是将所有这些方法结合起来,以自动化的方式执行这些变换,同时尽可能选择好的默认值。这正是我们在 AutoStationaryTransformer 中实现并集成到 src.transforms.target_transformations 中的内容。
下面的流程图解释了这种自动化逻辑:

图7.6:AutoStationaryTransformer 的流程图
我们出于两个原因在此实现中排除了差分:
- 在预测的背景下,差分带来了相当沉重的技术债务。如果你进行差分,你本质上会使多步预测变得困难。这是可能的,但更加困难且灵活性更低。
- 差分可以被视为完成我们这里所做事情的一种不同方式。这是因为差分会去除线性趋势,而季节性差分也会去除季节性。因此,对于自回归时间序列,差分可以发挥很大作用,值得作为一种独立的变换。
现在,让我们看看可以使用哪些参数来调整 AutoStationaryTransformer:
confidence:统计检验的置信水平,默认值为0.05。seasonal_period:季节性周期重复自身的周期数。如果设置为None,则从数据中推断,默认值为None。seasonality_max_lags:仅当未给出seasonality_period时使用,用于设置搜索季节性的最大滞后,默认值为None。trend_check_params:传递给趋势统计检验的参数;check_trend的默认值为{"mann_kendall": False}。detrender_params:传递给DetrendingTransformer的参数,默认值为{"degree":1}。deseasonalizer_params:传递给DeseasonalizingTransformer的参数;seasonality_extraction固定为period_averages。box_cox_params:传递给BoxCoxTransformer的参数,默认值为{"optimization": "guerrero"}。
让我们将此将此 AutoStationaryTransformer 应用于一个应用于一个人工时间序列,看看效果如何(完整代码在相关笔记本中):
from src.transforms.target_transformations import AutoStationaryTransformer
auto_stationary = AutoStationaryTransformer(seasonal_period=25)
y_stat = auto_stationary.fit_transform(y_final)

图7.7:AutoStationaryTransformer——变换前后
我们可以看到 AutoStationaryTransformer 已经对时间序列进行了去季节化和去趋势处理。在这个特定示例中,AutoStationaryTransformer 应用了去趋势、去季节化和 Box-Cox 变换。
现在,让我们将这个自动变换应用于我们一直在处理的数据集:
train_df = pd.read_parquet(preprocessed/"selected_blocks_train_missing_imputed_feature_engg.parquet")
transformer_pipelines = {}
for _id in tqdm(train_df["LCLid"].unique()):
#Initialize the AutoStationaryTransformer with a seasonality period of 48*7
auto_stationary = AutoStationaryTransformer(seasonal_period=48*7)
#Creating the timeseries with datetime index
y = train_df.loc[train_df["LCLid"]==_id, ["energy_consumption","timestamp"]].set_index("timestamp")
#Fitting and transforming the train
y_stat = auto_stationary.fit_transform(y, freq="30min")
# Setting the transformed series back to the dataframe
train_df.loc[train_df["LCLid"]==_id, "energy_consumption"] = y_stat.values
#Saving the pipeline
transformer_pipelines[_id] = auto_stationary
执行此操作的代码分为两个笔记本,分别名为 02-Dealing_with_Non-Stationarity.ipynb 和 02a-Dealing_with_Non-Stationarity-Train+Val.ipynb,位于 Chapter06 文件夹中。前者对训练数据进行自动平稳变换,而后者对训练数据和验证数据的合并数据进行变换。这是为了模拟我们如何对验证数据(仅使用训练数据进行训练)和测试数据(使用训练和验证数据进行训练)进行预测。
这个过程稍微耗时。我建议你运行笔记本,去吃点午餐或零食,然后回来。完成后,02-Dealing_with_Non-Stationarity.ipynb 笔记本将保存几个文件:
selected_blocks_train_auto_stat_target.parquet:一个 DataFrame,其索引为LCLid和timestamp,以及变换后的目标变量auto_transformer_pipelines_train.pkl:一个 Python 字典,包含每个AutoStationaryTransformer的LCLid,以便将来我们可以反转变换
02a-Dealing_with_Non-Stationarity-Train+Val.ipynb 笔记本也保存了训练和验证数据集的相应文件。
我们处理的数据集几乎可以忽略趋势,并且在整个过程中相当平稳。这些变换的影响在具有强烈趋势和异方差性的时间序列中会更加明显。
最佳实践:
这种在建模前显式地进行去趋势和去季节化的操作也可以视为一种 提升 形式。这应被视为将所有内容一起建模的另一种替代方案。在某些情况下,让模型以数据驱动的方式端到端学习可能比使用显式去趋势和去季节化注入这些强归纳偏置表现得更好,反之亦然。交叉验证的测试分数应始终具有最终决定权。
恭喜你完成了一个充满新概念、一些统计学和数学知识的复杂章节。从将机器学习模型应用于时间序列的角度来看,本章的概念将非常有助于将你的模型提升到一个新的水平。
小结
在上一章中,我们深入到实践层面,并在此基础上继续推进,回顾了平稳性等概念以及如何处理此类非平稳时间序列。我们学习了可用于显式处理非平稳时间序列的技术,例如差分、去趋势、去季节化等。为了将这些技术整合起来,我们看到了自动转换目标变量的方法,学习了如何使用所提供的实现,并将其应用于我们的数据集。现在,我们已经掌握了将时间序列有效转换为机器学习数据集所需的技能,在下一章中,我们将开始使用我们创建的特征,将一些机器学习模型应用于该数据集。
参考文献
以下是本章的参考文献:
- Bayazit, M. and Önöz, B. (2007), 趋势分析中是否需要进行预白化?, Hydrological Sciences Journal, 52:4, 611–624. https://doi.org/10.1623/hysj.52.4.611..
- White, H. (1980), 异方差一致性协方差矩阵估计及异方差直接检验. Econometrica Vol. 48, No. 4 (May 1980), pp. 817–838 (22 pages). https://doi.org/10.2307/1912934..
- Box, G. E. P. and Cox, D. R. (1964), 变换分析. Journal of the Royal Statistical Society, Series B, 26, 211–252. http://www.ime.usp.br/~abe/lista/pdfQWaCMboK68.pdf..
- Guerrero, Victor M. (1993), 基于幂变换的时间序列分析. Journal of Forecasting, Volume 12, Issue 1, 37–48. https://onlinelibrary.wiley.com/doi/10.1002/for.3980120104..
延伸阅读
要了解更多关于本章涉及的主题,请参阅以下资源:
- 时间序列分析中的平稳性,作者Shay Palachy:, 作者Shay Palachy: https://towardsdatascience.com/stationarity-in-time-series-analysis-90c94f27322
- 比较R中的ADF检验函数,作者Fabian Kostadinov(同样的概念也可以在Python中实现):, 作者Fabian Kostadinov(同样的概念也可以在Python中实现): https://fabian-kostadinov.github.io/2015/01/27/comparing-adf-test-functions-in-r/
- 肯德尔τ:https://www.statisticshowto.com/kendalls-tau/
- 曼-肯德尔趋势检验:https://www.statisticshowto.com/wp-content/uploads/2016/08/Mann-Kendall-Analysis-1.pdf
- 泰尔-森估计量:https://en.wikipedia.org/wiki/Theil%E2%80%93Sen_estimator
- 基于相关图的统计推断——维基百科:https://en.wikipedia.org/wiki/Correlogram#Statistical_inference_with_correlograms
- 异方差性检测的怀特检验:https://itfeature.com/hetero/white-test-of-heteroscedasticity/