Noisy sampled signals often contain a slower component that should be kept and a faster component that should be reduced before analysis. SciPy can design a digital Butterworth filter and apply it to a NumPy array, which fits sensor, audio, and measurement data that already has a known sample rate.
A fourth-order low-pass design from butter() with output=“sos” produces second-order sections for sosfiltfilt(). Passing fs keeps cutoff and response checks in hertz instead of normalized Nyquist fractions.
A synthetic signal with one component below the cutoff and one component above it makes the attenuation easy to inspect in both the filter response and the FFT amplitudes. sosfiltfilt() is an offline, forward-backward filter, so use sosfilt() when streaming or causal output must not use future samples.
import numpy as np from scipy.signal import butter, freqz_sos, sosfiltfilt sample_rate = 200.0 duration = 2.0 t = np.arange(0, duration, 1 / sample_rate) raw = np.sin(2 * np.pi * 5 * t) + 0.5 * np.sin(2 * np.pi * 40 * t) sos = butter(4, 12, btype="lowpass", fs=sample_rate, output="sos") filtered = sosfiltfilt(sos, raw) w, h = freqz_sos(sos, worN=[5, 12, 40], fs=sample_rate) gain_db = 20 * np.log10(np.maximum(np.abs(h), 1e-12)) freqs = np.fft.rfftfreq(raw.size, d=1 / sample_rate) input_fft = np.abs(np.fft.rfft(raw)) * 2 / raw.size filtered_fft = np.abs(np.fft.rfft(filtered)) * 2 / filtered.size def amp_at(spectrum, hz): return spectrum[np.argmin(np.abs(freqs - hz))] print(f"samples: {raw.size}") print(f"sos sections: {sos.shape[0]}") for hz, db in zip(w, gain_db): print(f"filter gain at {hz:>4.0f} Hz: {db:>7.2f} dB") print(f"input 5 Hz amplitude: {amp_at(input_fft, 5):.3f}") print(f"filtered 5 Hz amplitude: {amp_at(filtered_fft, 5):.3f}") print(f"input 40 Hz amplitude: {amp_at(input_fft, 40):.3f}") print(f"filtered 40 Hz amplitude: {amp_at(filtered_fft, 40):.3f}")
fs sets the sample rate for both butter() and freqz_sos(). The cutoff value 12 is therefore interpreted as 12 Hz, not as a normalized fraction of Nyquist.
$ python3 butterworth_filter.py samples: 400 sos sections: 2 filter gain at 5 Hz: -0.00 dB filter gain at 12 Hz: -3.01 dB filter gain at 40 Hz: -46.46 dB input 5 Hz amplitude: 1.000 filtered 5 Hz amplitude: 1.000 input 40 Hz amplitude: 0.500 filtered 40 Hz amplitude: 0.002
The response is near 0 dB at 5 Hz, about -3 dB at the 12 Hz Butterworth cutoff, and far lower at 40 Hz. The FFT amplitudes show the same effect after filtering.
Set sample_rate to the real sampling frequency. Keep the cutoff below half that rate for low-pass and high-pass filters, and use a two-value cutoff such as [8, 20] with btype=“bandpass” for a band-pass filter.
sosfiltfilt() uses samples from both sides of each point, so it is not suitable for live streaming filters that must produce causal output.
$ rm butterworth_filter.py