使用 Python 进行时间序列特征提取,从理论到实践

以下是提取时间序列分析特征时需要了解的所有内容

时间序列是一种特殊的动物。欢迎来到雲闪世界

当我开始我的机器学习职业生涯时,我这样做是因为我喜欢物理(开始机器学习的奇怪原因),并且从物理学中我了解到我也非常喜欢编码和数据科学。我并不真正关心数据类型。我只想坐在电脑前每天写 10k 行代码。

事实是,即使你不在乎(我现在仍然真的不在乎),你的职业生涯也会让你关注某些类型的数据而不是其他数据。

如果你在SpaceX 工作,你可能不会做很多 NLP,但你会做很多信号处理。如果你在Netflix工作,你最终可能会接触很多NLP推荐 系统。如果你在Tesla工作,你肯定会成为计算机视觉专家并从事图像工作。

当我开始以物理学家的身份工作,然后继续攻读工程学博士学位时,我立即被抛入了信号的世界
这只是工程学的自然世界:每次你设置并从中提取信息时,最终你都会处理一个信号。别误会我的意思,工程学并不是唯一一个以信号为电影明星的世界。另一个非常著名的例子是金融股票价格的时间序列。这是信号(时间与价格)的另一个例子。但如果出于某种原因你正在处理信号,你应该记住这篇博文的第一句话:

时间序列是一种特殊的动物。

这意味着,许多对表格数据或图像进行的转换/操作/处理技术对于时间序列来说具有另一种含义(如果它们有意义的话)。我们以特征提取为例。

“特征提取”的理念是“处理”我们拥有的数据,并确保我们提取所有有意义的特征,以便下一步(通常是机器学习应用程序)可以从中受益。换句话说,它是一种通过提供重要特征并过滤掉所有不太重要的特征来“帮助”机器学习步骤的方法。

这是完整的特征提取过程:

图片由作者制作

现在,当我们考虑特征提取器(比如表格数据和信号)时,我们正在进行两项完全不同的运动。

例如,峰谷的概念傅里叶变换小波变换的概念以及独立 分量分析 (ICA)的概念只有在处理信号时才真正有意义。
我之所以这么说和展示,只是为了让你相信,有一套特征提取技术只属于信号。

现在有两种宏类方法可以进行特征提取:

  • 基于数据驱动的方法:这些方法旨在通过查看信号来提取特征。我们忽略机器学习步骤及其目标(例如分类、预测或回归),我们只查看信号、对其进行处理并从中提取信息。
  • 基于模型的方法:这些方法着眼于整个流程并旨在找到需要解决的特定问题的特征。

数据驱动方法的优点是它们通常在计算上简单易用,并且不需要相应的目标输出。缺点是这些特征不是针对您的问题的。例如,对信号进行傅里叶变换并将其用作特征可能不是使用在端到端模型中训练的特定特征的最佳方法。

为了这篇博文的目的,我们将只讨论数据驱动方法。具体来说,我们将讨论基于特定领域的方法、基于频率的方法 基于时间的方法和基于统计的方法。
让我们开始吧!

1. 领域特定特征提取

我要描述的第一个特征有点故意含糊其辞
事实上,提取特征的最佳方法是考虑你面临的具体问题。例如,假设你正在处理来自工程实验的信号,并且你非常关心t = 6s 之后的振幅。在这些情况下,特征提取通常没有意义(对于随机情况,t=6s 可能并不比 t =10s 更特殊),但它实际上与你的情况极为相关。这就是我们所说的领域特定特征提取。我知道这不需要大量的数学和编码,但这就是它的意义所在,因为它极大地依赖于你的具体情况。

2.基于频率的特征提取

2.1 解释

这种方法与我们的时间序列/信号的频谱分析有关。
这是什么意思呢?如果我们观察信号,我们有一个自然域。自然域是观察信号的最简单方法,它是时间域,这意味着我们将信号视为给定时间(或向量) 。

例如,让我们在自然域中考虑这个信号:

如果我们将其绘制出来,我们会得到这样的结果:

图片由作者制作

这是自然(时间)域,也是我们数据集中最简单的域。我们可以在域中转换它。
正如我们在符号表达式中看到的,我们的信号有三个周期分量。频域的思想是将信号分解为其周期分量频率幅度相位

信号 y(t) 的傅里叶变换Y(k) 如下:

图片由作者制作

这描述了频率为k的分量的振幅相位。在提取有意义的特征方面,我们可以提取10 个主要分量(振幅最高的分量)的振幅、相位和频率值。这些将是 10x3 特征(振幅、频率和相位 x 10 ),它们将根据频谱信息描述您的时间序列。

现在,这种方法可以扩展。例如,我们可以不基于正弦/余弦函数来分解信号,而是基于小波(另一种周期波形式)来分解信号。 这种分解称为小波分解

我知道这很难理解,所以让我们从编码部分开始,向你展示我的意思……

2.2 代码

现在,让我们在现实生活中实现它。让我们从非常简单的傅里叶变换开始。

首先我们需要邀请一些朋友参加聚会:

import numpy as np
import matplotlib.pyplot as plt
import pandas as pd

现在我们以此信号为例:

plt.figure(figsize=(10,5))
x = np.linspace(-2*np.pi,2*np.pi,1000)
y = np.sin(x) + 0.4*np.cos(2*x) + 2*np.sin(3.2*x)
plt.plot(x,y,color='firebrick',lw=2)
plt.xlabel('Time (t)',fontsize=24)
plt.xticks(fontsize=14)
plt.yticks(fontsize=14)
plt.grid(alpha=0.4)
plt.ylabel('y',fontsize=24)
Text(0, 0.5, 'y')
Notebook Image

该信号有三个主要分量。一个分量的幅度= 1,频率= 1;一个分量的幅度= 0.4,频率= 2;一个分量的幅度 = 2,频率= 3.2。我们可以通过运行傅里叶变换来恢复它们:

import numpy as np
import matplotlib.pyplot as plt

# Generate the time-domain signal
x = np.linspace(-8*np.pi, 8*np.pi, 10000)
y = np.sin(x) + 0.4*np.cos(2*x) + 2*np.sin(3.2*x)
y= y -np.mean(y)
# Perform the Fourier Transform
Y = np.fft.fft(y)
# Calculate the frequency bins
frequencies = np.fft.fftfreq(len(x), d=(x[1] - x[0]) / (2*np.pi))
# Normalize the amplitude of the FFT
Y_abs = 2*np.abs(Y) / len(x)
# Zero out very small values to remove noise
Y_abs[Y_abs < 1e-6] = 0
relevant_frequencies = np.where((frequencies>0) & (frequencies<10))
Y_phase = np.angle(Y)[relevant_frequencies]
frequencies = frequencies[relevant_frequencies]
Y_abs = Y_abs[relevant_frequencies]

# Plot the magnitude of the Fourier Transform
plt.figure(figsize=(10, 6))
plt.plot(frequencies, Y_abs)
plt.xlim(0, 10)  # Limit x-axis to focus on relevant frequencies
plt.xticks([3.2,1,2])
plt.title('Fourier Transform of the Signal')
plt.xlabel('Frequency (Hz)')
plt.ylabel('Magnitude')
plt.grid(True)
plt.show()

我们可以清楚地看到三个峰值及其对应的振幅和频率。

现在,我们实际上并不需要任何花哨的绘图(这只是为了表明这种方法有效),但我们可以用一个非常简单的函数完成所有事情,这个函数就是这个:

def extract_fft_features( y, x=None,  num_features = 5,max_frequency = 10):
  y= y -np.mean(y)
  # Perform the Fourier Transform
  Y = np.fft.fft(y)
  # Calculate the frequency bins
  if x is None:
    x = np.linspace(0,len(y))
  frequencies = np.fft.fftfreq(len(x), d=(x[1] - x[0]) / (2*np.pi))
  Y_abs = 2*np.abs(Y) / len(x)
  Y_abs[Y_abs < 1e-6] = 0
  relevant_frequencies = np.where((frequencies>0) & (frequencies<max_frequency))
  Y_phase = np.angle(Y)[relevant_frequencies]
  frequencies = frequencies[relevant_frequencies]
  Y_abs = Y_abs[relevant_frequencies]
  largest_amplitudes = np.flip(np.argsort(Y_abs))[0:num_features]
  top_5_amplitude = Y_abs[largest_amplitudes]
  top_5_frequencies = frequencies[largest_amplitudes]
  top_5_phases = Y_phase[largest_amplitudes]
  fft_features = top_5_amplitude.tolist()+top_5_frequencies.tolist()+top_5_phases.tolist()
  amp_keys = ['Amplitude '+str(i) for i in range(1,num_features+1)]
  freq_keys = ['Frequency '+str(i) for i in range(1,num_features+1)]
  phase_keys = ['Phase '+str(i) for i in range(1,num_features+1)]
  fft_keys = amp_keys+freq_keys+phase_keys
  fft_dict = {fft_keys[i]:fft_features[i] for i in range(len(fft_keys))}
  fft_data = pd.DataFrame(fft_features).T
  fft_data.columns = fft_keys
  return fft_dict, fft_data

因此你给我信号y和(可选):

  • x时间数组
  • 要考虑的特征(或峰值)的数量
  • 您愿意探索的最大频率

这是输出:

x = np.linspace(-8*np.pi, 8*np.pi, 10000)
y = np.sin(x) + 0.4*np.cos(2*x) + 2*np.sin(3.2*x)
extract_fft_features(x=x,y=y,num_features=3)[1]

Amplitude 1	Amplitude 2	Amplitude 3	Frequency 1	Frequency 2	Frequency 3	Phase 1	Phase 2	Phase 3
0	1.531008	0.990294	0.983615	3.249675	3.124687	0.9999	-1.56266	1.578706	-1.568175

如果我们想使用小波(不是正弦/余弦)提取特征,我们可以进行小波变换。我们需要安装这个:

pip install PyWaveletsPyWavelets

然后运行这个:

import numpy as np
import pywt
import pandas as pd

def extract_wavelet_features(y, wavelet='db4', level=3, num_features=5):
    y = y - np.mean(y)  # Remove the mean

    # Perform the Discrete Wavelet Transform
    coeffs = pywt.wavedec(y, wavelet, level=level)

    # Flatten the list of coefficients into a single array
    coeffs_flat = np.hstack(coeffs)

    # Get the absolute values of the coefficients
    coeffs_abs = np.abs(coeffs_flat)

    # Find the indices of the largest coefficients
    largest_coeff_indices = np.flip(np.argsort(coeffs_abs))[0:num_features]

    # Extract the largest coefficients as features
    top_coeffs = coeffs_flat[largest_coeff_indices]

    # Generate feature names for the wavelet features
    feature_keys = ['Wavelet Coeff ' + str(i+1) for i in range(num_features)]

    # Create a dictionary for the features
    wavelet_dict = {feature_keys[i]: top_coeffs[i] for i in range(num_features)}

    # Create a DataFrame for the features
    wavelet_data = pd.DataFrame(top_coeffs).T
    wavelet_data.columns = feature_keys

    return wavelet_dict, wavelet_data

# Example usage:
wavelet_dict, wavelet_data = extract_wavelet_features(y)
wavelet_data


Wavelet Coeff 1	Wavelet Coeff 2	Wavelet Coeff 3	Wavelet Coeff 4	Wavelet Coeff 5
0	-8.1864	-8.00138	-7.760325	7.65852	7.621079

3.基于统计的特征提取

3.1 解释

特征提取的另一种方法是依靠传统的统计数据
给定一个信号,可以做多种事情来从中提取一些统计信息。按从简单到复杂的顺序,以下是我们可以提取的信息列表:

  • 平均值只不过是信号的总和除以信号的时间步长数。非常简单

  • 方差,即信号与平均值的差异

  • 偏度峰度。这些指标用于测试时间序列的分布“非高斯”程度。偏度描述其不对称程度,峰度描述其“尾部”程度。
  • 分位数:这些值将时间序列划分为具有概率范围的区间。例如,0.25 分位数,值 = 10,表示时间序列中 25% 的值低于 10,其余 75% 的值大于 10
  • 自相关。这基本上可以告诉你时间序列的“模式化”程度,即时间序列中模式的强度。更正式地说,这个指标表示时间序列值与其自身过去值的相关性。
  • :表示时间序列的复杂性或不可预测性。我在这里写了一整篇博客文章。

到 2024 年,上述每个属性都可以通过一行代码实现(真是活在当下)。具体方法如下:

import numpy as np
import pandas as pd
from scipy.stats import skew, kurtosis

def extract_statistical_features(y):
    def calculate_entropy(y):
        # Ensure y is positive and normalized
        y = np.abs(y)
        y_sum = np.sum(y)

        # Avoid division by zero
        if y_sum == 0:
            return 0

        # Normalize the signal
        p = y / y_sum

        # Calculate entropy
        entropy_value = -np.sum(p * np.log2(p + 1e-12))  # Add a small value to avoid log(0)

        return entropy_value
    # Remove the mean to center the data
    y_centered = y - np.mean(y)
    y = y+np.max(np.abs(y))*10**-4

    # Statistical features
    mean_value = np.mean(y)
    variance_value = np.var(y)
    skewness_value = skew(y)
    kurtosis_value = kurtosis(y)
    autocorrelation_value = np.correlate(y_centered, y_centered, mode='full')[len(y) - 1] / len(y)
    quantiles = np.percentile(y, [25, 50, 75])
    entropy_value = calculate_entropy(y)  # Add a small value to avoid log(0)

    # Create a dictionary of features
    statistical_dict = {
        'Mean': mean_value,
        'Variance': variance_value,
        'Skewness': skewness_value,
        'Kurtosis': kurtosis_value,
        'Autocorrelation': autocorrelation_value,
        'Quantile_25': quantiles[0],
        'Quantile_50': quantiles[1],
        'Quantile_75': quantiles[2],
        'Entropy': entropy_value
    }

    # Convert to DataFrame for easy visualization and manipulation
    statistical_data = pd.DataFrame([statistical_dict])

    return statistical_dict, statistical_data

y = np.sin(x) + 0.4*np.cos(2*x) + 2*np.sin(3.2*x)
wavelet_dict, wavelet_data = extract_statistical_features(y)
wavelet_data

Mean	Variance	Skewness	Kurtosis	Autocorrelation	Quantile_25	Quantile_50	Quantile_75	Entropy
0	0.00069	2.617477	0.118425	-1.129153	2.617477	-1.330042	-0.086535	1.389426	9.667876

4.基于时间的特征提取器

4.1 解释

在本节中,我们将重点介绍如何通过提取时间特征来提取时间序列的信息。特别是,我们将提取峰和谷的信息。

有多个特征最好了解,以便提取信号的峰值,例如预期宽度、预期阈值或平台大小。如果您有任何此类信息(例如,出于某种原因,您只想考虑振幅 > 2 的峰值),您可以调整参数,否则,您可以将所有参数保留为默认值。

我们还有权决定需要多少个峰值/特征。例如,我们可能想要 N = 10:仅最大的 10 个峰值+谷值。请记住,如果我们这样做,我们就会拥有 Nx2 = 20 个特征(10 个位置和 10 个振幅)。

4.2 代码

与往常一样,实现此目的的代码相当简单:

import numpy as np
from scipy.signal import find_peaks

def extract_peaks_and_valleys(y, N=10):
    # Find peaks and valleys
    peaks, _ = find_peaks(y)
    valleys, _ = find_peaks(-y)

    # Combine peaks and valleys
    all_extrema = np.concatenate((peaks, valleys))
    all_values = np.concatenate((y[peaks], -y[valleys]))

    # Sort by absolute amplitude (largest first)
    sorted_indices = np.argsort(-np.abs(all_values))
    sorted_extrema = all_extrema[sorted_indices]
    sorted_values = all_values[sorted_indices]

    # Select the top N extrema
    top_extrema = sorted_extrema[:N]
    top_values = sorted_values[:N]

    # Pad with zeros if fewer than N extrema are found
    if len(top_extrema) < N:
        padding = 10 - len(top_extrema)
        top_extrema = np.pad(top_extrema, (0, padding), 'constant', constant_values=0)
        top_values = np.pad(top_values, (0, padding), 'constant', constant_values=0)

    # Prepare the features
    features = []
    for i in range(N):
        features.append(top_values[i])
        features.append(top_extrema[i])

    # Create a dictionary of features
    feature_dict = {f'peak_{i+1}': features[2*i] for i in range(N)}
    feature_dict.update({f'loc_{i+1}': features[2*i+1] for i in range(N)})

    return feature_dict,pd.DataFrame([feature_dict])

# Example usage:
x = np.linspace(-2*np.pi,2*np.pi,1000)
y = np.sin(x) + 0.4*np.cos(2*x) + 2*np.sin(3.2*x)
features = extract_peaks_and_valleys(y, N=10)
features[1]

peak_1	peak_2	peak_3	peak_4	peak_5	peak_6	peak_7	peak_8	peak_9	peak_10	loc_1	loc_2	loc_3	loc_4	loc_5	loc_6	loc_7	loc_8	loc_9	loc_10
0	2.89733	2.712237	2.695335	2.694532	2.628401	2.614476	2.308689	2.008004	1.397136	1.37607	924	695	70	539	224	310	454	778	617	148

请记住,如果我们选择 N = 10 个峰值,但您的信号中实际上只有 M = 4 个峰值,则剩余的 6 个位置和峰值幅度将为 0。

5. 使用哪种方法?

好的,我们已经看到了 4 种不同类别的方法。

我们应该使用哪一个?

我不会用“这取决于问题本身”这种外交辞令来打击你,因为当然,这总是取决于问题本身。

事实是,如果您有基于领域的特征提取,这始终是您的最佳选择:如果实验的物理原理或问题的先验知识清晰,您应该依靠它并将这些特征视为最重要的特征,甚至可能将它们视为唯一的特征。有时(很多时候)您没有基于领域的特征,这没关系。

我认为,对于频率统计基于时间的特征,你应该将它们一起使用。你应该将这些特征添加到你的数据集中,然后看看其中一些是否有帮助、没有帮助,或者实际上混淆了你的机器学习模型。

6. 结论

非常感谢您抽出时间与我交流。我想借此机会总结一下我们所做的一切。

  1. 我们介绍了特征提取的概念。我们解释了它的重要性以及了解时间序列的具体技术的重要性
  2. 我们解释了基于模型数据驱动的特征提取技术之间的区别。基于模型的技术是端到端训练的特征提取技术。在这篇博文中,我们重点介绍了独立于给定任务执行的数据驱动技术
  3. 我们讨论了基于领域的特征提取技术,这些技术源于感兴趣的特定问题
  4. 我们讨论了频谱技术,这些技术涉及信号的傅里叶/频谱
  5. 我们讨论了统计技术,从信号中提取平均值、标准差、熵和自相关的值
  6. 我们讨论了基于时间的技术,从信号中提取峰值信息
  7. 我们简要介绍了针对您的具体情况应采用哪种 技术

感谢关注雲闪世界。(Aws解决方案架构师vs开发人员&GCP解决方案架构师vs开发人员)

 订阅频道(https://t.me/awsgoogvps_Host)
 TG交流群(t.me/awsgoogvpsHost)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值