保姆级教程:用Python的NumPy和Matplotlib一步步拆解时间序列(SSA实战)

时间序列分析是数据科学中极具挑战性又充满魅力的领域。想象一下,你手头有一组股票价格、气温变化或心电图数据,如何从中提取出趋势、周期和噪声成分?这就是奇异谱分析(SSA)大显身手的时候。不同于传统的傅里叶变换,SSA不需要假设数据是平稳的,也不需要预先知道周期长度,这种非参数特性让它成为处理复杂时间序列的瑞士军刀。

本教程将用Python带你从零开始实现SSA算法,每步代码都配有详细注释和可视化展示。我们会用NumPy处理矩阵运算,用Matplotlib绘制每个关键步骤的中间结果,让你像调试程序一样直观理解SSA的工作原理。即使你刚接触时间序列分析,也能跟着这个保姆级教程轻松上手。

1. 环境准备与数据生成

1.1 安装必要库

确保你的Python环境已安装以下库:

pip install numpy matplotlib

1.2 构造模拟时间序列

我们先人工生成一个包含三种成分的合成时间序列:

  • 趋势项 :线性变化趋势
  • 周期项 :正弦波形式的周期性变化
  • 噪声项 :随机高斯噪声
import numpy as np
import matplotlib.pyplot as plt

# 参数设置
days = 180  # 总天数
period = 30  # 周期天数

# 生成趋势项(线性变化)
tend_sequence = np.linspace(2, -2, num=days)

# 生成周期项(正弦波)
time = np.linspace(0, 2*np.pi*days/period, num=days)
sin_sequence = np.sin(time)

# 生成噪声项
noise = np.random.randn(days) * 0.5

# 合成信号
signal = sin_sequence + tend_sequence
sequence = signal + noise

# 可视化各成分
plt.figure(figsize=(12, 6))
plt.plot(tend_sequence, color='yellow', label='Trend')
plt.plot(sin_sequence, color='blue', label='Periodicity')
plt.plot(noise, color='red', label='Noise', alpha=0.5)
plt.legend()
plt.title('Time Series Components')
plt.show()

# 对比原始信号与含噪信号
plt.figure(figsize=(12, 4))
plt.plot(sequence, color='red', label='With Noise')
plt.plot(signal, color='blue', label='Original')
plt.legend()
plt.title('Original vs Noisy Signal')
plt.show()

这段代码生成了三个可视化图表:

  1. 分别展示趋势、周期和噪声成分
  2. 对比原始信号和添加噪声后的信号
  3. 展示各成分如何组合成最终的时间序列

2. SSA算法核心步骤详解

2.1 构建轨迹矩阵(Embedding)

轨迹矩阵是SSA的第一步,也是理解整个算法的关键。它通过滑动窗口将一维时间序列转换为二维矩阵,这个过程称为"嵌入"。

def build_trajectory_matrix(series, window_len):
    """构建轨迹矩阵"""
    series_len = len(series)
    K = series_len - window_len + 1
    X = np.zeros((window_len, K))
    for i in range(window_len):
        X[i, :] = series[i:i+K]
    return X

# 设置窗口长度(经验值通常取序列长度的1/3到1/2)
window_len = 45  
X = build_trajectory_matrix(sequence, window_len)

# 可视化轨迹矩阵
plt.figure(figsize=(10, 6))
plt.imshow(X, cmap='viridis', aspect='auto')
plt.colorbar(label='Value')
plt.title('Trajectory Matrix Heatmap')
plt.xlabel('Window Position')
plt.ylabel('Window Length')
plt.show()

关键参数选择

  • 窗口长度L:影响成分分离效果,通常取N/3到N/2之间
  • K = N - L + 1:决定了轨迹矩阵的列数

2.2 SVD矩阵分解

奇异值分解(SVD)是SSA的核心数学工具,它将轨迹矩阵分解为三个矩阵的乘积:

# 执行SVD分解
U, sigma, VT = np.linalg.svd(X, full_matrices=False)

# 计算每个成分的贡献率
contributions = sigma**2 / np.sum(sigma**2)

# 可视化奇异值谱
plt.figure(figsize=(10, 4))
plt.plot(sigma, 'o-')
plt.title('Singular Value Spectrum')
plt.xlabel('Component Index')
plt.ylabel('Singular Value')
plt.grid(True)

# 可视化贡献率
plt.figure(figsize=(10, 4))
plt.bar(range(len(contributions)), contributions)
plt.title('Component Contributions')
plt.xlabel('Component Index')
plt.ylabel('Variance Explained')
plt.show()

SVD分解后我们得到:

  • U矩阵:左奇异向量,反映时间序列在时间域的特征
  • Σ矩阵:奇异值对角矩阵,表示各成分的重要性
  • V矩阵:右奇异向量,反映时间序列在延迟坐标域的特征

2.3 分组重构

这一步需要根据奇异值谱决定如何分组。通常前几个成分对应趋势,中间成分对应周期,最后成分对应噪声。

# 重构基本矩阵
ZT = np.zeros_like(VT)
for n in range(len(sigma)):
    ZT[n, :] = sigma[n] * VT[n, :]

# 选择要重构的组分
selected_components = [0, 1, 2]  # 示例选择前三个成分

# 可视化各组分
plt.figure(figsize=(12, 8))
for i in selected_components:
    Xi = np.outer(U[:, i], ZT[i, :])
    plt.subplot(len(selected_components), 1, i+1)
    plt.imshow(Xi, cmap='viridis', aspect='auto')
    plt.title(f'Component {i}')
    plt.colorbar()
plt.tight_layout()
plt.show()

2.4 对角平均(Diagonal Averaging)

将分组后的矩阵转换回时间序列形式:

def diagonal_averaging(Xi, series_len):
    """对角平均操作"""
    L, K = Xi.shape
    RC = np.zeros(series_len)
    for k in range(series_len):
        if k <= L - 1:
            RC[k] = np.mean([Xi[p, k-p] for p in range(k+1)])
        elif k <= K - 1:
            RC[k] = np.mean([Xi[p, k-p] for p in range(L)])
        else:
            RC[k] = np.mean([Xi[p, k-p] for p in range(k-K+1, series_len-K+1)])
    return RC

# 重构所有成分
RCs = []
for i in range(U.shape[1]):
    Xi = np.outer(U[:, i], ZT[i, :])
    RCs.append(diagonal_averaging(Xi, days))

RCs = np.array(RCs)

# 可视化前8个重构成分
plt.figure(figsize=(12, 10))
for m in range(8):
    plt.subplot(8, 1, m+1)
    plt.plot(RCs[m, :])
    plt.ylabel(f'RC {m}')
plt.suptitle('First 8 Reconstructed Components')
plt.tight_layout()
plt.show()

3. 结果分析与应用

3.1 成分分离效果评估

让我们看看SSA如何分离出原始信号中的不同成分:

# 趋势项对比
plt.figure(figsize=(12, 4))
plt.plot(RCs[0, :], color='blue', label='Extracted Trend')
plt.plot(tend_sequence, color='red', linestyle='--', label='True Trend')
plt.legend()
plt.title('Trend Component Comparison')
plt.show()

# 周期项对比
plt.figure(figsize=(12, 4))
plt.plot(RCs[1, :] + RCs[2, :], color='blue', label='Extracted Periodicity')
plt.plot(sin_sequence, color='red', linestyle='--', label='True Periodicity')
plt.legend()
plt.title('Periodic Component Comparison')
plt.show()

# 去噪效果对比
plt.figure(figsize=(12, 4))
reconstructed = RCs[0, :] + RCs[1, :] + RCs[2, :] + RCs[3, :]
plt.plot(reconstructed, color='blue', label='Denoised')
plt.plot(signal, color='red', linestyle='--', label='Original')
plt.legend()
plt.title('Denoising Effect Comparison')
plt.show()

3.2 窗口长度的影响

窗口长度L是SSA最重要的参数,它直接影响成分分离效果:

# 测试不同窗口长度
window_lengths = [30, 45, 60, 90]
plt.figure(figsize=(12, 8))

for i, L in enumerate(window_lengths):
    # 执行完整SSA流程
    X = build_trajectory_matrix(sequence, L)
    U, sigma, VT = np.linalg.svd(X, full_matrices=False)
    ZT = np.zeros_like(VT)
    for n in range(len(sigma)):
        ZT[n, :] = sigma[n] * VT[n, :]
    
    # 重构趋势项
    Xi = np.outer(U[:, 0], ZT[0, :])
    RC = diagonal_averaging(Xi, days)
    
    plt.subplot(2, 2, i+1)
    plt.plot(RC, label=f'L={L}')
    plt.plot(tend_sequence, linestyle='--', label='True Trend')
    plt.legend()
    plt.title(f'Window Length = {L}')

plt.tight_layout()
plt.show()

从图中可以看出:

  • L太小(30):趋势提取不够平滑
  • L适中(45-60):效果最佳
  • L太大(90):可能过度平滑

4. 实战技巧与常见问题

4.1 成分分组策略

如何选择哪些成分组合在一起?这里有一些经验法则:

成分类型 特征 通常包含的RC
趋势项 变化缓慢,对应最大的奇异值 RC0, RC1
周期项 中等奇异值,成对出现 RC2+RC3, RC4+RC5
噪声项 小奇异值,无规律波动 剩余所有RC

4.2 参数选择建议

  1. 窗口长度L

    • 对于周期性数据:取接近周期的整数倍
    • 一般情况:取N/3到N/2之间
    • 可通过试验多个值比较结果
  2. 成分选择

    • 观察奇异值谱的"拐点"
    • 前几个大奇异值通常对应信号
    • 小奇异值通常对应噪声
# 自动选择显著成分的示例
significant_components = np.where(sigma > 0.1 * sigma.max())[0]
print(f"Significant components: {significant_components}")

4.3 处理真实数据的建议

  1. 数据预处理

    • 去除明显异常值
    • 必要时进行标准化
    • 处理缺失值(SSA对缺失值敏感)
  2. 结果验证

    • 使用部分数据进行重构测试
    • 比较不同参数设置的效果
    • 结合领域知识判断合理性
# 缺失值处理示例(简单线性插值)
def interpolate_missing(series):
    missing = np.isnan(series)
    indices = np.arange(len(series))
    series[missing] = np.interp(indices[missing], indices[~missing], series[~missing])
    return series

4.4 性能优化技巧

对于长时间序列,这些技巧可以提高计算效率:

  1. 使用稀疏矩阵 :当轨迹矩阵有很多零元素时
  2. 截断SVD :只计算前k个奇异值/向量
  3. 并行计算 :对多个窗口长度并行运行SSA
# 使用截断SVD的示例
from scipy.sparse.linalg import svds

k = 10  # 只计算前10个成分
U, sigma, VT = svds(X, k=k)

更多推荐