Filtering signals is essential for cleaning up noisy data, extracting trends, and preparing inputs for further analysis in science, engineering, and data work. In Python, the scipy.signal subpackage makes designing and applying filters straightforward and flexible.
Here’s how to filter signals effectively and what you need to know to get real results, fast.
What is Signal Filtering and Why Use It?
Filtering allows you to remove unwanted components from a signal, such as high-frequency noise, or to isolate specific frequencies of interest.
In practice, you’ll use filtering when you need to smooth sensor data, preprocess audio, clean ECG traces, or prepare signals for analysis like peak detection.
Core Steps for Filtering a Signal
The typical workflow for filtering a signal in Python with scipy.signal includes:
- Design the filter: Choose the type (low-pass, high-pass, band-pass, band-stop), select parameters (cutoff frequencies, filter order), and design the filter using tools like butter, cheby1, or firwin.
- Apply the filter: Use filtfilt for zero-phase filtering (preferred for most offline use), or lfilter for causal filtering.
- Visualize and check: Always plot before and after to confirm the filter’s effect.
Designing a Butterworth Low-Pass Filter
A Butterworth filter is a common choice for basic filtering because it has a flat frequency response in the passband. Suppose you want to smooth out high-frequency noise from a sampled signal.
Example: Design a low-pass filter to remove noise above 10 Hz from a signal sampled at 100 Hz.
from scipy.signal import butter, filtfilt
import numpy as np
import matplotlib.pyplot as plt
# Create a noisy signal
fs = 100 # Sampling frequency (Hz)
t = np.linspace(0, 5, fs*5) # 5 seconds
clean = np.sin(2*np.pi*2*t) # 2 Hz signal
noise = 0.5 * np.random.randn(len(t))
signal = clean + noise
# Design the filter
cutoff = 10 # Desired cutoff frequency (Hz)
order = 4 # Filter order
nyq = 0.5 * fs # Nyquist frequency
normal_cutoff = cutoff / nyq
b, a = butter(order, normal_cutoff, btype="low", analog=False)
# Apply the filter
filtered = filtfilt(b, a, signal)
# Plot the result
plt.figure(figsize=(10, 4))
plt.plot(t, signal, label="Noisy signal", alpha=0.5)
plt.plot(t, filtered, label="Filtered signal", linewidth=2)
plt.plot(t, clean, label="Original clean", linestyle="dashed")
plt.legend()
plt.xlabel('Time (s)')
plt.title('Low-pass Butterworth Filtering')
plt.tight_layout()
plt.show()
Key points:
- Always normalize the cutoff by dividing by the Nyquist frequency.
- filtfilt applies the filter forward and backward, preventing phase shift.
- Adjust the filter order for sharper or more gradual cutoff.
When to Use lfilter vs filtfilt
- filtfilt: Preferred for most offline applications (analysis, data cleaning) because it eliminates phase distortion.
- lfilter: Use when you need causal, real-time filtering (e.g., in live systems) but expect phase delay.
from scipy.signal import lfilter
# Single-pass filtering (introduces phase shift)
filtered_lagged = lfilter(b, a, signal)
High-Pass, Band-Pass, and Band-Stop Filters
Changing btype gives different behaviors:
- High-pass: Use btype=’high’, supply a cutoff above which to keep signal.
- Band-pass: Supply [low, high] frequencies as a list or tuple to pass a specific band.
- Band-stop: Use btype=’bandstop’ to suppress a frequency band (e.g., to remove power line noise).
Example: Band-pass between 1 and 10 Hz
b, a = butter(order, [1/nyq, 10/nyq], btype="band")
filtered = filtfilt(b, a, signal)
Practical Tips
- Choose the right order: Higher order = sharper cutoff, but more ringing and potential instability. For most uses, order 4–6 works well.
- Visualize frequency response: Use scipy.signal.freqz to check how the filter shapes frequencies.
- For non-uniform or nonstationary data: Consider more advanced filters or adapt filter design.
- Edge effects: filtfilt reduces edge artifacts, but if signal is short, results near start and end may be unreliable.
Common Pitfalls
- Using the wrong sampling frequency (fs): Always match fs to your data.
- Incorrect cutoff normalization: Cutoff must be a fraction of Nyquist.
- Filtering data with gaps or NaNs: Interpolate or clean missing data first.
- Overfiltering: Too aggressive filtering can distort the true signal.
Summary Table: Typical Filter Design in scipy.signal
Filter Type | Design Function | Example Cutoff | Usage |
---|---|---|---|
Low-pass | butter | 10 Hz | Remove high-frequency noise |
High-pass | butter | 1 Hz | Remove baseline drift |
Band-pass | butter | [1, 10] Hz | Isolate frequency band |
Band-stop | butter | [49, 51] Hz | Remove 50 Hz powerline interference |
FIR filter | firwin | See docs | When you need linear phase |
Wrapping Up
Filtering with scipy.signal gives you total control over noise reduction and frequency isolation. With just a few lines of code, you can design robust filters for any standard application.
Always visualize and tune your filter settings based on your specific signal and use case. If you’re moving beyond basic filtering, check out advanced filter types (Chebyshev, elliptic, FIR) and adaptive methods in the scipy.signal documentation.