时间序列的傅里叶变换(FFT),但清理后数据的两端会向彼此移动。

3

我有一个时间序列,表示虚拟环境中的X和Z坐标。

X = np.array(df["X"])
Z = np.array(df["Z"])

X 和 Z 坐标都包含来自不同源的噪声。为了滤除这些噪声,我想使用傅里叶变换。 经过一些研究,我使用了此处代码中的内容对我的数据进行了去噪。

def fft_denoiser(x, n_components, to_real=True):
    n = len(x)

    # compute the fft
    fft = np.fft.fft(x, n)

    # compute power spectrum density
    # squared magnitud of each fft coefficient
    PSD = fft * np.conj(fft) / n

    # keep high frequencies
    _mask = PSD > n_components
    fft = _mask * fft

    # inverse fourier transform
    clean_data = np.fft.ifft(fft)

    if to_real:
        clean_data = clean_data.real

    return clean_data

在设置了n_components之后,我喜欢使用被清理过的数据。这个过程进行得相当顺利,因为我绘制了X坐标图:

enter image description here

但是只有在开头和结尾处,清理过的数据突然朝着彼此的值移动... 有人可以帮助解释一下是什么原因,以及我应该如何克服这个问题吗?
1个回答

2
你遇到这个问题的原因是因为FFT隐含地假设提供的输入信号是周期性的。如果你重复你的原始数据,你会发现每个周期都有一个大的不连续性(当信号从约20回落到约5时)。一旦去除了一些高频分量,你就会看到边缘处略微不太锐利的不连续性(在开头和结尾各几个样本)。
为了避免这种情况,你可以使用线性FIR滤波器在时间域中进行过滤,这可以在不考虑周期性假设的情况下处理数据序列。
对于这个答案的目的,我构建了一个合成测试信号(你可以用它来重新创建相同的条件),但你显然可以使用自己的数据。
# Generate synthetic signal for testing purposes
fs = 1 # Hz
f0 = 0.002/fs
f1 = 0.01/fs
dt = 1/fs
t = np.arange(200, 901)*dt
m = (25-5)/(t[-1]-t[0])
phi = 4.2
x = 5 + m*(t-t[0]) + 2*np.sin(2*np.pi*f0*t) + 1*np.sin(2*np.pi*f1*t+phi) + 0.2*np.random.randn(len(t))

现在,为了设计滤波器,我们可以对_mask取逆变换(而不是应用该掩模):
import numpy as np

# Design denoising filter
def freq_sampling_filter(x, threshold):
  n = len(x)

  # compute the fft
  fft = np.fft.fft(x, n)

  # compute power spectrum density
  # squared magnitud of each fft coefficient
  PSD = fft * np.conj(fft) / n

  # keep frequencies with large contributions
  _mask = PSD > threshold
  _coff = np.fft.fftshift(np.real(np.fft.ifft(_mask)))
  return _coff

coff = freq_sampling_filter(x, threshold)

“阈值”是一个可调参数,应该选择它来保留你想要的足够频率成分并摆脱不需要的频率成分。当然这是高度主观的。
然后我们可以简单地使用scipy.signal.filtfilt函数应用过滤器:
from scipy.signal import filtfilt

# apply the denoising filter
cleaned = filtfilt(coff, 1, x, padlen=len(x)-1, padtype='constant')

为了说明,使用上述生成的合成信号的阈值10会产生以下原始数据(变量x)和清理后数据(变量cleaned): {{link1:enter image description here}}
选择padtype为'constant'可以确保过滤后的值从未过滤的数据的起始和结束值开始和结束。
替代方法:
正如在评论中发布的那样,对于更长的数据集,filtfilt可能会很昂贵。 作为替代方案,可以使用基于FFT的卷积来执行过滤,方法是使用scipy.fftconvolve。请注意,在这种情况下,没有与filtfilt的padtype参数相当的东西,因此我们需要手动填充信号以避免在开头和结尾出现边缘效应。
n = len(x)
# Manually pad signal to avoid edge effects
x_padded = np.concatenate((x[0]*np.ones(n-1), x, x[-1]*np.ones((n-1)//2)))
# Filter using FFT-based convolution
cleaned = fftconvolve(x_padded, coff, mode='same')
# Extract result (remove data from padding)
cleaned = cleaned[2*(n-1)//2:-n//2+1]

供参考,以下是长度为700的上述信号的一些基准比较(时间以秒为单位,因此越小越好):
filtfilt    : 0.3831593
fftconvolve : 0.00028040000000029153

请注意,相对性能会有所不同,但是基于FFT的卷积在信号长度较长时预计会表现得更好。

这个回答非常清晰!非常感谢,这正是我在寻找的!但是,当我将其应用于自己的数据时,遇到了一个问题,我的数据非常大(约20,000个实例)。使用spicy.signal.filtfilt进行过滤需要很长时间...不幸的是,我必须对许多数据集执行此操作...您是否知道加快此过程的方法?非常感谢! - Ablu_68
我在这个答案中使用了 filtfilt,因为您没有提到大型数据集,这使得解决方案更简单。但是我相信对于奇数的 n,您可以在开头添加 n-1 次第一个值,并在结尾添加另外 (n-1)/2 次最后一个值,然后使用 fftconvolve 并且摆脱前面的 3*(n-1)/2 个输出。如果有机会,我可能会将其变成官方答案/更新。 - SleuthEye
@SleuthEye,freq_sampling_filter函数中的threshold是什么?还是你想写成n_components?为了重现结果,在你创建测试原始数据时,该函数参数使用的值是多少? - msh855
@msh855 我已更新答案,包含了在示例中用于可重复性的threshold值。尽管如此,我使用变量threshold是因为我认为它更符合FFT值高于该阈值时保留的思想,而不是OP的fft_denoiser函数参数的n_components,这表明保留指定数量的频率分量。 - SleuthEye

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接