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.
Steps to filter a signal with a SciPy Butterworth filter:
- Create a sample script that designs the filter, applies it, and checks the main frequencies.
- butterworth_filter.py
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.
- Run the script to confirm the low-pass filter keeps the 5 Hz component and attenuates the 40 Hz component.
$ 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.
- Replace the synthetic signal with measured samples from the project.
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.
- Keep sosfiltfilt() for offline arrays that can be filtered in both directions.
sosfiltfilt() uses samples from both sides of each point, so it is not suitable for live streaming filters that must produce causal output.
- Remove the sample script after copying the pattern into the project.
$ rm butterworth_filter.py
Mohd Shakir Zakaria is a cloud architect with deep roots in software development and open-source advocacy. Certified in AWS, Red Hat, VMware, ITIL, and Linux, he specializes in designing and managing robust cloud and on-premises infrastructures.