保姆级教程:用Python的NumPy和Matplotlib一步步拆解时间序列(SSA实战)
·
保姆级教程:用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()
这段代码生成了三个可视化图表:
- 分别展示趋势、周期和噪声成分
- 对比原始信号和添加噪声后的信号
- 展示各成分如何组合成最终的时间序列
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 参数选择建议
-
窗口长度L :
- 对于周期性数据:取接近周期的整数倍
- 一般情况:取N/3到N/2之间
- 可通过试验多个值比较结果
-
成分选择 :
- 观察奇异值谱的"拐点"
- 前几个大奇异值通常对应信号
- 小奇异值通常对应噪声
# 自动选择显著成分的示例
significant_components = np.where(sigma > 0.1 * sigma.max())[0]
print(f"Significant components: {significant_components}")
4.3 处理真实数据的建议
-
数据预处理 :
- 去除明显异常值
- 必要时进行标准化
- 处理缺失值(SSA对缺失值敏感)
-
结果验证 :
- 使用部分数据进行重构测试
- 比较不同参数设置的效果
- 结合领域知识判断合理性
# 缺失值处理示例(简单线性插值)
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 性能优化技巧
对于长时间序列,这些技巧可以提高计算效率:
- 使用稀疏矩阵 :当轨迹矩阵有很多零元素时
- 截断SVD :只计算前k个奇异值/向量
- 并行计算 :对多个窗口长度并行运行SSA
# 使用截断SVD的示例
from scipy.sparse.linalg import svds
k = 10 # 只计算前10个成分
U, sigma, VT = svds(X, k=k)
更多推荐


所有评论(0)