What Is a Bandpass Filter?
A bandpass filter (BPF) passes signals within a specified frequency band (passband) and attenuates signals outside that band (stopband).
Applications include radio channel selection, speech processing, and biomedical signal filtering (ECG, EEG noise removal).
Key Parameters
| Parameter | Description |
|---|---|
| \(f_l\) | Lower cutoff frequency (-3 dB point) |
| \(f_h\) | Upper cutoff frequency (-3 dB point) |
| \(f_0 = \sqrt{f_l f_h}\) | Center frequency (geometric mean) |
| \(BW = f_h - f_l\) | Bandwidth |
| \(Q = f_0 / BW\) | Q factor (selectivity) |
A higher Q value means a narrower, more selective passband.
Frequency Response Derivation
Lowpass-to-Bandpass Transformation
A bandpass filter can be designed from a lowpass prototype (LPF) via a frequency transformation. In the analog domain, replace the complex variable \(s\) as:
\[ s \rightarrow \frac{s^2 + \omega_l \omega_h}{s(\omega_h - \omega_l)} \tag{1}\]where \(\omega_l = 2\pi f_l\) and \(\omega_h = 2\pi f_h\).
Applying transformation \((1)\) to a 1st-order Butterworth LPF \(H_{LP}(s) = \frac{\omega_c}{s + \omega_c}\) yields a 2nd-order bandpass filter:
\[ H\_{BP}(s) = \frac{BW \cdot s}{s^2 + BW \cdot s + \omega_0^2} \tag{2}\]where \(BW = \omega_h - \omega_l\) and \(\omega_0 = \sqrt{\omega_l \omega_h}\).
Discrete-Time Bandpass Filter (Bilinear Transform)
Equation \((2)\) is converted to the digital domain via the bilinear transform:
\[s = \frac{2}{T} \cdot \frac{1 - z^{-1}}{1 + z^{-1}} \tag{3}\]Substituting \((3)\) into \((2)\) yields the digital BPF transfer function:
\[ H(z) = \frac{b_0 + b_1 z^{-1} + b_2 z^{-2}}{1 + a_1 z^{-1} + a_2 z^{-2}} \tag{4}\]In practice, scipy.signal handles this computation automatically.
Python Implementation
IIR Bandpass Filter with scipy.signal
import numpy as np
import matplotlib.pyplot as plt
from scipy import signal
# --- Parameters ---
fs = 1000.0 # Sampling frequency [Hz]
f_low = 50.0 # Lower cutoff [Hz]
f_high = 150.0 # Upper cutoff [Hz]
order = 4 # Filter order
# --- Design Butterworth bandpass filter ---
nyq = fs / 2.0
low = f_low / nyq
high = f_high / nyq
b, a = signal.butter(order, [low, high], btype='bandpass')
# --- Compute frequency response ---
w, h = signal.freqz(b, a, worN=8000, fs=fs)
# --- Plot ---
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8))
# Gain (dB)
ax1.semilogx(w, 20 * np.log10(np.abs(h) + 1e-10))
ax1.axvline(f_low, color='r', linestyle='--', label=f'$f_l$ = {f_low} Hz')
ax1.axvline(f_high, color='g', linestyle='--', label=f'$f_h$ = {f_high} Hz')
ax1.axhline(-3, color='gray', linestyle=':', label='-3 dB')
ax1.set_xlabel('Frequency [Hz]')
ax1.set_ylabel('Gain [dB]')
ax1.set_title(f'Butterworth Bandpass Filter (order={order})')
ax1.legend()
ax1.grid(True, which='both', alpha=0.3)
ax1.set_xlim([1, nyq])
ax1.set_ylim([-80, 5])
# Phase
ax2.semilogx(w, np.angle(h, deg=True))
ax2.axvline(f_low, color='r', linestyle='--')
ax2.axvline(f_high, color='g', linestyle='--')
ax2.set_xlabel('Frequency [Hz]')
ax2.set_ylabel('Phase [degrees]')
ax2.set_title('Phase Response')
ax2.grid(True, which='both', alpha=0.3)
ax2.set_xlim([1, nyq])
plt.tight_layout()
plt.savefig('bandpass_response.png', dpi=150, bbox_inches='tight')
plt.show()
FIR Bandpass Filter Design
IIR filters have nonlinear phase, but FIR filters can achieve linear phase. This is important in audio processing where phase distortion is audible.
from scipy.signal import firwin, freqz
# --- FIR bandpass filter ---
numtaps = 101 # Number of taps (odd number recommended)
fir_bpf = firwin(
numtaps,
[f_low, f_high],
pass_zero=False, # Bandpass specification
fs=fs,
window='hamming'
)
w_fir, h_fir = freqz(fir_bpf, worN=8000, fs=fs)
# IIR vs FIR comparison
plt.figure(figsize=(10, 5))
plt.semilogx(w, 20 * np.log10(np.abs(h) + 1e-10), label='IIR Butterworth (order=4)', linewidth=2)
plt.semilogx(w_fir, 20 * np.log10(np.abs(h_fir) + 1e-10), label=f'FIR Hamming (taps={numtaps})', linewidth=2, linestyle='--')
plt.axvline(f_low, color='r', linestyle=':', alpha=0.7, label=f'$f_l$ = {f_low} Hz')
plt.axvline(f_high, color='g', linestyle=':', alpha=0.7, label=f'$f_h$ = {f_high} Hz')
plt.axhline(-3, color='gray', linestyle=':', alpha=0.7)
plt.xlabel('Frequency [Hz]')
plt.ylabel('Gain [dB]')
plt.title('Bandpass Filter Comparison: IIR vs FIR')
plt.legend()
plt.grid(True, which='both', alpha=0.3)
plt.xlim([1, nyq])
plt.ylim([-80, 5])
plt.tight_layout()
plt.show()
Applying the Filter to a Signal
# --- Generate test signal ---
t = np.linspace(0, 1.0, int(fs), endpoint=False)
# Target signal: 100 Hz sine wave
signal_pure = np.sin(2 * np.pi * 100 * t)
# Noise: 10 Hz low-freq + 300 Hz high-freq + white noise
noise = (
0.5 * np.sin(2 * np.pi * 10 * t) +
0.5 * np.sin(2 * np.pi * 300 * t) +
0.3 * np.random.randn(len(t))
)
x_noisy = signal_pure + noise
# --- Apply filter ---
# lfilter: causal filter (real-time, has phase delay)
y_lfilter = signal.lfilter(b, a, x_noisy)
# filtfilt: zero-phase filter (offline, no phase delay)
y_filtfilt = signal.filtfilt(b, a, x_noisy)
# --- Comparison plot ---
fig, axes = plt.subplots(3, 1, figsize=(12, 8), sharex=True)
axes[0].plot(t[:200], x_noisy[:200], alpha=0.8, label='Noisy signal')
axes[0].set_ylabel('Amplitude')
axes[0].set_title('Input Signal (noisy)')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
axes[1].plot(t[:200], y_lfilter[:200], color='orange', label='lfilter (causal)')
axes[1].plot(t[:200], signal_pure[:200], color='gray', linestyle='--', alpha=0.6, label='True signal')
axes[1].set_ylabel('Amplitude')
axes[1].set_title('After lfilter (with phase delay)')
axes[1].legend()
axes[1].grid(True, alpha=0.3)
axes[2].plot(t[:200], y_filtfilt[:200], color='green', label='filtfilt (zero-phase)')
axes[2].plot(t[:200], signal_pure[:200], color='gray', linestyle='--', alpha=0.6, label='True signal')
axes[2].set_ylabel('Amplitude')
axes[2].set_xlabel('Time [s]')
axes[2].set_title('After filtfilt (zero-phase)')
axes[2].legend()
axes[2].grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('bandpass_filtering_result.png', dpi=150, bbox_inches='tight')
plt.show()
IIR vs FIR Design Comparison
| Property | IIR (Butterworth, etc.) | FIR |
|---|---|---|
| Phase response | Nonlinear (phase distortion) | Linear phase achievable |
| Number of coefficients | Small (\(2N+1\) approx.) | Large (tens to hundreds) |
| Computational cost | Low | High (depends on tap count) |
| Stability | Risk with high orders | Always stable |
| Stopband roll-off | Steep at low order | Improves with tap count |
| Group delay | Non-constant | Constant (\((N-1)/2\) samples) |
| Typical use | Real-time processing | Audio, medical signals |
Design guidelines:
- Low computational cost in real-time → IIR (Butterworth / Chebyshev)
- Linear phase required (audio, medical) → FIR (Hamming / Kaiser window)
- Offline processing, zero phase delay →
filtfilt(works with both IIR and FIR)
Effect of Filter Order
fig, ax = plt.subplots(figsize=(10, 6))
for ord_n in [2, 4, 6, 8]:
b_n, a_n = signal.butter(ord_n, [low, high], btype='bandpass')
w_n, h_n = signal.freqz(b_n, a_n, worN=8000, fs=fs)
ax.semilogx(w_n, 20 * np.log10(np.abs(h_n) + 1e-10), label=f'order={ord_n}')
ax.axhline(-3, color='gray', linestyle=':', label='-3 dB')
ax.axvline(f_low, color='r', linestyle='--', alpha=0.5)
ax.axvline(f_high, color='g', linestyle='--', alpha=0.5)
ax.set_xlabel('Frequency [Hz]')
ax.set_ylabel('Gain [dB]')
ax.set_title('Effect of Filter Order on Bandpass Response')
ax.legend()
ax.grid(True, which='both', alpha=0.3)
ax.set_xlim([1, nyq])
ax.set_ylim([-80, 5])
plt.tight_layout()
plt.show()
Higher order yields steeper roll-off, but IIR filters become numerically unstable at very high orders. Order ≤ 8 is a practical guideline.
Related Articles
- Lowpass Filter Design: Moving Average, Butterworth, Chebyshev — LPF prototype design that forms the basis of bandpass filter design.
- Butterworth Filter Design: Theory and Python Implementation — The Butterworth LPF prototype used in bandpass design.
- FIR vs IIR Filters: A Comprehensive Comparison — Design philosophy differences between FIR and IIR filters.
- Notch Filter Design and Python Implementation — Band-stop (notch) filter that is the complement of a bandpass filter.
- Frequency Characteristics of the Exponential Moving Average Filter — EMA as a 1st-order IIR lowpass filter derived via Z-transform.
- FFT: Theory and Python Implementation — FFT fundamentals for verifying bandpass filter effects in the frequency domain.
- Window Functions and Power Spectral Density — Mathematical background of window functions used in FIR filter design.
- Adaptive Filters (LMS/RLS): Theory and Python Implementation — Filters that automatically adjust their frequency response to the environment.
References
- Proakis, J. G., & Manolakis, D. G. (2006). Digital Signal Processing: Principles, Algorithms, and Applications. Pearson.
- scipy.signal.butter — SciPy documentation
- scipy.signal.filtfilt — SciPy documentation
Related Tools
- DevToolBox - Free Developer Tools — 85+ tools including JSON formatter, regex tester
- CalcBox - Everyday Calculation Tools — 61+ tools including statistics and frequency conversion