Filtering, spectra and peaks

SciPy · Advanced · 13 min

Sensor, audio and physiological data need noise removal and frequency analysis before modelling.

scipy.signal covers the classic digital signal processing toolkit; scipy.fft provides fast Fourier transforms.

Filter design: signal.butter(N, Wn, btype="low", fs=fs, output="sos") designs a Butterworth filter of order N with cutoff Wn in Hz when you pass the sampling rate fs. Ask for second-order sections (output="sos"): the older (b, a) form becomes numerically unstable at higher orders.

Applying filters:

  • signal.sosfilt(sos, x): causal, as a real-time system would, but delays the signal,
  • signal.sosfiltfilt(sos, x): runs forward and backward for zero phase shift, ideal for offline analysis.

Peaks: signal.find_peaks(x, height=..., distance=..., prominence=...) returns indices and properties. Prominence is usually the most useful criterion for noisy data.

Spectra: scipy.fft.rfft(x) with rfftfreq(n, d=1/fs) gives the frequency content of real signals. For noisy signals, signal.welch(x, fs=fs, nperseg=...) averages over segments to estimate the power spectral density with far less variance; signal.spectrogram or ShortTimeFFT show how it changes over time.

Remember the Nyquist limit: you can only see frequencies below fs / 2. Resample with signal.resample_poly, which filters to avoid aliasing, rather than taking every n-th sample.

Example

import numpy as np
from scipy import signal
from scipy.fft import rfft, rfftfreq

fs = 500                                        # samples per second
t = np.arange(0, 2, 1 / fs)
rng = np.random.default_rng(0)
x = np.sin(2 * np.pi * 5 * t) + 0.5 * np.sin(2 * np.pi * 80 * t) + 0.2 * rng.normal(size=t.size)

freqs = rfftfreq(t.size, d=1 / fs)
top = np.argsort(np.abs(rfft(x)))[-2:]
print(sorted(freqs[top].tolist()))                       # [5.0, 80.0] (dominant frequencies)

sos = signal.butter(4, 20, btype="low", fs=fs, output="sos")
clean = signal.sosfiltfilt(sos, x)              # zero-phase low-pass at 20 Hz
peaks, _ = signal.find_peaks(clean, prominence=1)
print(len(peaks), t[peaks[:3]].round(3))        # 9 [0.244 0.444 0.65 ] (every ~0.2 s = 5 Hz)

f, pxx = signal.welch(x, fs=fs, nperseg=256)
print(f[np.argmax(pxx)])                        # 5.859375 (near 5 Hz; bins are fs/nperseg ~ 2 Hz)

Remember

  • Design filters with output="sos" and pass fs to use Hz
  • sosfiltfilt filters without phase shift; sosfilt is causal
  • Use welch for noisy spectra and respect the Nyquist limit

Watch out for

  • Downsampling by slicing x[::10] without an anti-aliasing filter.

Check yourself

  1. Why prefer sosfiltfilt over sosfilt for offline analysis of a recorded signal?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.scipy.org/doc/scipy/tutorial/signal.html

Builds on

Unlocks

Nothing builds on this yet.