奇异谱分析(SSA)根据观测到的时间序列构造轨迹矩阵,并对轨迹矩阵进行分解和重构,从而提取出代表原时间序列不同成分的信号,如长期趋势信号、周期信号、噪声信号等,从而进一步对分解得到的信号进行分析。
Step1:根据原始时间序列构建轨迹矩阵 X X X
Step2:对矩阵X进行奇异值分解: X = ∑ i = 1 r σ i U i V i T X=\sum_{i=1}^{r} \sigma_i U_i V_i^T X=∑i=1rσiUiViT
Step3:按奇异值生成 r r r个子矩阵: X i = σ i U i V i T X_i = \sigma_i U_i V_{i}^T Xi=σiUiViT
Step4:根据某一分组原则将子矩阵 X i X_i Xi分为 m m m个组
Step5:对子矩阵 X i X_i Xi进行对角均值化处理得到子序列 F i ~ \widetilde{F_i} Fi
Step6:对 m m m个组中的子序列相加得到分组子序列。
输入:原始时间序列 y y y,窗口长度 L ( 2 ≤ L ≤ N / 2 ) L (2 \le L \le N/2) L(2≤L≤N/2), N = l e n ( y ) N=len(y) N=len(y)
X = [ y 1 y 2 y 3 y 4 … y N − L + 1 y 2 y 3 y 4 y 5 … y N − L + 2 y 3 y 4 y 5 y 6 … y N − L + 3 ⋮ ⋮ ⋮ ⋮ ⋱ ⋮ y L y L + 1 y L + 2 y L + 3 … y N ] (1) \mathbf{X} = \begin{bmatrix}\tag{1} y_1 & y_2 & y_3 & y_4 &\ldots & y_{N-L+1} \\ y_2 & y_3 & y_4 & y_5 &\ldots & y_{N-L+2} \\ y_3 & y_4 & y_5 & y_6 &\ldots & y_{N-L+3} \\ \vdots & \vdots & \vdots & \vdots & \ddots & \vdots \\ y_{L} & y_{L+1} & y_{L+2} & y_{L+3} & \ldots & y_{N} \\ \end{bmatrix} X=⎣⎢⎢⎢⎢⎢⎡y1y2y3⋮yLy2y3y4⋮yL+1y3y4y5⋮yL+2y4y5y6⋮yL+3………⋱…yN−L+1yN−L+2yN−L+3⋮yN⎦⎥⎥⎥⎥⎥⎤(1)
显然矩阵 X X X是一个汉克尔矩阵(每一条副对角线的元素都相等),矩阵的行数为窗口长度 L L L,矩阵的列数为 N − L + 1 N-L+1 N−L+1。
X = U Σ V T (2) X = U \Sigma V^T \tag{2} X=UΣVT(2)
其中:
U U U: L × L L \times L L×L酉矩阵
Σ \Sigma Σ: L × K L \times K L×K对角阵
V V V: K × K K \times K K×K酉矩阵
奇异值分解将轨迹矩阵 X X X分解为酉矩阵 U U U,对角阵 Σ \Sigma Σ和酉矩阵 V V V的线性组合。这意味着:
X = ∑ i = 1 r σ i U i V i T = ∑ i = 1 r X i (3) \begin{aligned}\tag{3} \mathbf{X} & = \sum_{i=1}^{r}\sigma_i U_i V_i^{\text{T}} \\ & = \sum_{i=1}^{r}\mathbf{X}_i \end{aligned} X=i=1∑rσiUiViT=i=1∑rXi(3)
r r r表示矩阵 X X X的非零特征根数也即矩阵的秩。
不妨设根据某一分组原则将子矩阵分为了trend、periodic、noise 3组,对应地矩阵 X X X分解得到的子序列将被组合为3部分。
X ~ (trend) = ∑ t ∈ t r e n d X ~ t ⟹ F ~ (trend) = ∑ t ∈ t r e n d F ~ t X ~ (periodic) = ∑ p ∈ p e r i o d i c X ~ p ⟹ F ~ (periodic) = ∑ p ∈ p e r i o d i c F ~ p X ~ (noise) = ∑ n ∈ n o i s e X ~ n ⟹ F ~ (noise) = s u m n ∈ n o i s e F ~ n (4) \begin{aligned}\tag{4} \tilde{\mathbf{X}}^{\text{(trend)}} & = \sum_{t \in trend}\tilde{\mathbf{X}}_t & \implies & \tilde{F}^{\text{(trend)}} = \sum_{t \in trend} \tilde{F}_t \\ \tilde{\mathbf{X}}^{\text{(periodic)}} & = \sum_{p \in periodic}\tilde{\mathbf{X}}_p & \implies & \tilde{F}^{\text{(periodic)}} = \sum_{p \in periodic} \tilde{F}_p\\ \tilde{\mathbf{X}}^{\text{(noise)}} & = \sum_{n \in noise}\tilde{\mathbf{X}}_n & \implies & \tilde{F}^{\text{(noise)}} = sum_{n \in noise} \tilde{F}_n \end{aligned} X~(trend)X~(periodic)X~(noise)=t∈trend∑X~t=p∈periodic∑X~p=n∈noise∑X~n⟹⟹⟹F~(trend)=t∈trend∑F~tF~(periodic)=p∈periodic∑F~pF~(noise)=sumn∈noiseF~n(4)
y ~ k = { 1 k ∑ m = 1 k x m , k − m + 1 1 ≤ k < L ∗ 1 L ∗ ∑ m = 1 L ∗ x m , k − m + 1 L ∗ ≤ k ≤ K ∗ 1 N − k + 1 ∑ m = k − K ∗ + 1 N − K ∗ + 1 x m , k − m + 1 K ∗ < k ≤ N (5) \widetilde{y}_{k} = \left\{ \begin{array}{lr}\tag{5} \frac{1}{k}\sum_{m=1}^{k} x_{m, k-m+1} & \ 1 \le k < L^* \\ \frac{1}{L^*}\sum_{m=1}^{L^*} x_{m, k-m+1} & \ L^* \le k \le K^* \\ \frac{1}{N-k+1}\sum_{m=k-K^*+1}^{N-K^*+1} x_{m, k-m+1} & \ K^* < k \le N \\ \end{array} \right. y k=⎩⎪⎨⎪⎧k1∑m=1kxm,k−m+1L∗1∑m=1L∗xm,k−m+1N−k+11∑m=k−K∗+1N−K∗+1xm,k−m+1 1≤k<L∗ L∗≤k≤K∗ K∗<k≤N(5)
其中, L ∗ = m i n ( L , N − L + 1 ) , K ∗ = m a x ( L , N − L + 1 ) L^* = min(L,N-L+1),K^*=max(L,N-L+1) L∗=min(L,N−L+1),K∗=max(L,N−L+1)
数据来源于:Kaggle原油价格预测
'''数据处理'''
import pandas as pd
import numpy as np
'''数据可视化'''
import matplotlib.pyplot as plt
plt.rcParams['figure.dpi'] = 800 #调整分辨率
plt.rcParams['font.sans-serif']=['SimHei'] #显示中文
plt.rcParams['axes.unicode_minus']=False #正常显示负号
###加载数据集###
file = r'D:/机器学习/crude-oil-price.csv'
oil_info = pd.read_csv(file,index_col='date')
price = oil_info['price']
# 算法封装
class SSA(object):
__supported_types = (pd.Series,np.ndarray,list) #限制时间序列的输入类型
def __init__(self,tseries,L):
'''
Args:
tseries:原始时间序列
L:窗口长度
'''
if not isinstance(tseries, self.__supported_types):
raise TypeError("请确保时间序列的数据类型为Pandas Series,NumpPy array 或者list")
else:
self.orig_TS = pd.Series(tseries)
self.N = len(tseries) #原始时间序列长度
if not 2 <= L <= self.N / 2:
raise ValueError("窗口长度必须介于[2,N/2]")
self.L = L #窗口长度,轨迹矩阵的行数
self.K = self.N - self.L + 1 #轨迹矩阵的列数
self.X = np.array([self.orig_TS.values[i:L+i] for i in range(0,self.K)]).T
#奇异值分解
self.U,self.Sigma,VH = np.linalg.svd(self.X)
self.r = np.linalg.matrix_rank(self.X) #矩阵的秩等于非零特征值的数量
#每一个非零特征值都对应一个子矩阵,子矩阵对角平均化后得到原始时间序列的一个子序列
self.TS_comps = np.zeros((self.N,self.r))
#对角平均还原
for i in range(self.r):
X_elem = self.Sigma[i] * np.outer(self.U[:,i], VH[i,:])
X_rev = X_elem[::-1]
self.TS_comps[:,i] = [X_rev.diagonal(j).mean() for j in range(-X_rev.shape[0]+1, X_rev.shape[1])]
def comps_to_df(self):
'''将子序列数组转换成DataFrame类型
'''
cols = ["F{}".format(i) for i in range(self.r)]
return pd.DataFrame(data=self.TS_comps,columns=cols,index=self.orig_TS.index)
def reconsruct(self,indices):
'''重构,可以是部分重构(相当于子序列的分组合并),也可以是全部合并(重构为原序列)
Args:
indices 重构所选择的子序列
'''
if isinstance(indices,int):
indices = [indices]
ts_vals = self.TS_comps[:,indices].sum(axis=1)
return pd.Series(ts_vals,index=self.orig_TS.index)
def vis(self):
'''可视化子序列
'''
fig,axs = plt.subplots(self.r,sharex='all')
for i in range(self.r):
axs[i].plot(self.reconsruct(i),lw=1)
price_SSA = SSA(price,7)
comps_df = price_SSA.comps_to_df()