Introduction
A Bode plot visualizes the frequency response \(H(j\omega)\) of a linear time-invariant system as two stacked plots: magnitude [dB] and phase [degrees] on a logarithmic frequency axis. Systematized by Hendrik Bode in the 1930s for control engineering, it is now the standard tool for filter design, transfer function analysis, and stability evaluation across signal processing and control.
For filter design specifically, every key trait of Butterworth (maximally flat), Chebyshev (equiripple), highpass and lowpass (roll-off rate) is visible on a Bode plot. This article consolidates the math, reading techniques, and Python implementation so you can use the Bode plot as a cross-cutting hub for understanding all of these filters.
Mathematical Background
Transfer Function and Frequency Response
For a continuous-time LTI system with transfer function \(H(s)\) , the frequency response is obtained by substituting \(s = j\omega\) :
\[H(j\omega) = |H(j\omega)| \, e^{j\angle H(j\omega)} \tag{1}\]A Bode plot decomposes this complex-valued function into two plots:
- Magnitude plot: \(20\log_{10}|H(j\omega)|\) vs. \(\log_{10}\omega\)
- Phase plot: \(\angle H(j\omega)\) (degrees) vs. \(\log_{10}\omega\)
The Decibel Definition
Expressing magnitude in decibels turns multiplication into addition. For two systems \(H_1, H_2\) in series:
\[20\log_{10}|H_1 H_2| = 20\log_{10}|H_1| + 20\log_{10}|H_2| \tag{2}\]This means a Bode plot of a higher-order filter is the sum of the plots of its first-order factors, which makes complex filters tractable by superposition of simple shapes.
Phase Delay and Group Delay
Two key quantities readable from the phase plot are phase delay and group delay:
\[\tau_p(\omega) = -\frac{\angle H(j\omega)}{\omega}, \quad \tau_g(\omega) = -\frac{d\angle H(j\omega)}{d\omega} \tag{3}\]Group delay \(\tau_g\) is directly the slope of the phase plot and indicates waveform distortion. A linear-phase FIR filter has a constant group delay, which appears on a Bode plot as a phase that is a strictly linear function of frequency.
Bode Plots of 1st- and 2nd-Order Systems
First-Order Lowpass
\[H(s) = \frac{1}{1 + s/\omega_c} \tag{4}\]with magnitude
\[|H(j\omega)| = \frac{1}{\sqrt{1 + (\omega/\omega_c)^2}} \tag{5}\]reads asymptotically as
- \(\omega \ll \omega_c\) : \(|H|_{dB} \approx 0\) dB (passband)
- \(\omega = \omega_c\) : \(|H|_{dB} = -3\) dB (cutoff)
- \(\omega \gg \omega_c\) : \(|H|_{dB} \approx -20\log_{10}(\omega/\omega_c)\) dB (-20 dB/dec roll-off)
The phase passes through \(-45^\circ\) at \(\omega_c\) and asymptotes to \(-90^\circ\) . A first-order highpass is the mirror image: a \(+20\) dB/dec rise in the low band and a phase rotation from \(+90^\circ\) down to \(0^\circ\) .
Second-Order Resonance
For a second-order system
\[H(s) = \frac{\omega_n^2}{s^2 + 2\zeta\omega_n s + \omega_n^2} \tag{6}\]the magnitude exhibits a resonant peak when \(\zeta < 1/\sqrt{2}\) , with peak value
\[M_p = \frac{1}{2\zeta\sqrt{1-\zeta^2}} \tag{7}\]Smaller \(\zeta\) gives a sharper peak. The passband ripple of Chebyshev filters and both-band ripple of elliptic filters can be seen as the combined effect of multiple such second-order resonances. The high-frequency roll-off is -40 dB/dec, twice as steep as the first-order case.
Filter Types and Their Bode Plot Signatures
| Filter | Passband | Roll-off | Phase |
|---|---|---|---|
| Butterworth (order \(N\) ) | Maximally flat | \(-20N\) dB/dec | Relatively smooth |
| Chebyshev Type I | Equiripple | \(-20N\) dB/dec | Steep near band edge |
| Elliptic (Cauer) | Both-band ripple | Steepest | Steepest |
| Highpass | Flat for \(f > f_c\) | \(-20N\) /dec low | \(+90N^\circ \to 0^\circ\) |
| Bandpass | Flat inside band | \(\pm 20N\) /dec | \(0^\circ\) at center |
| Notch | Deep dip at \(f_0\) | Very steep | \(\pm 90^\circ\) flip at \(f_0\) |
The Bode plot thus serves as a visual dictionary of filter species: order \(N\) is read from the roll-off slope, attenuation type from the passband/stopband ripple shape, and phase distortion from the curvature of the phase plot.
Python Implementation
Standard Approach with scipy.signal.bode
scipy.signal.bode generates Bode plot data directly from a continuous-time transfer function.
import numpy as np
import matplotlib.pyplot as plt
from scipy import signal
# --- 1st-order lowpass: H(s) = 1 / (1 + s/wc) ---
wc = 2 * np.pi * 100 # Cutoff angular frequency [rad/s] (100 Hz)
num = [1.0]
den = [1.0 / wc, 1.0]
system = signal.TransferFunction(num, den)
# bode returns magnitude [dB] and phase [deg]
w, mag, phase = signal.bode(system, w=np.logspace(0, 4, 1000))
f = w / (2 * np.pi) # Convert to Hz
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 7), sharex=True)
ax1.semilogx(f, mag, linewidth=2)
ax1.axhline(-3, color='gray', linestyle=':', label='-3 dB')
ax1.axvline(wc / (2 * np.pi), color='r', linestyle='--', label=f'$f_c$ = {wc/(2*np.pi):.0f} Hz')
ax1.set_ylabel('Magnitude [dB]')
ax1.set_title('Bode Plot - 1st-order Lowpass')
ax1.grid(True, which='both', alpha=0.3)
ax1.legend()
ax2.semilogx(f, phase, linewidth=2, color='orange')
ax2.axvline(wc / (2 * np.pi), color='r', linestyle='--')
ax2.axhline(-45, color='gray', linestyle=':', label='-45°')
ax2.set_xlabel('Frequency [Hz]')
ax2.set_ylabel('Phase [degrees]')
ax2.grid(True, which='both', alpha=0.3)
ax2.legend()
plt.tight_layout()
plt.show()
Using semilogx is essential: a linear axis crushes the low-frequency structure, eliminating the Bode plot’s main strength of viewing multi-decade behavior at a glance.
IIR Digital Filters
For IIR digital filters such as Butterworth and Chebyshev, use scipy.signal.freqz instead.
import numpy as np
import matplotlib.pyplot as plt
from scipy import signal
fs = 1000.0 # Sampling frequency [Hz]
fc = 100.0 # Cutoff [Hz]
orders = [2, 4, 8]
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8), sharex=True)
for N in orders:
sos = signal.butter(N, fc, btype='low', fs=fs, output='sos')
w, h = signal.sosfreqz(sos, worN=4096, fs=fs)
# Magnitude (log freq + dB)
ax1.semilogx(w, 20 * np.log10(np.abs(h) + 1e-12), label=f'N={N}', linewidth=2)
# Phase (unwrap to remove 360° jumps)
ax2.semilogx(w, np.degrees(np.unwrap(np.angle(h))), label=f'N={N}', linewidth=2)
ax1.axhline(-3, color='gray', linestyle=':', label='-3 dB')
ax1.axvline(fc, color='r', linestyle='--', alpha=0.5)
ax1.set_ylabel('Magnitude [dB]')
ax1.set_title('Bode Plot - Butterworth Lowpass (digital)')
ax1.set_ylim(-100, 5)
ax1.grid(True, which='both', alpha=0.3)
ax1.legend()
ax2.axvline(fc, color='r', linestyle='--', alpha=0.5)
ax2.set_xlabel('Frequency [Hz]')
ax2.set_ylabel('Phase [degrees]')
ax2.set_xlim(1, fs / 2)
ax2.grid(True, which='both', alpha=0.3)
ax2.legend()
plt.tight_layout()
plt.show()
Each unit increase in order \(N\) steepens the high-frequency roll-off by 20 dB/dec and deepens the phase rotation by an additional \(-90^\circ\) . This is the best exercise to develop intuition for predicting filter characteristics from order alone.
Side-by-Side Comparison of Multiple Filters
Computing each filter manually lets us overlay their Bode plots for direct comparison.
import numpy as np
import matplotlib.pyplot as plt
from scipy import signal
fs = 1000.0
fc = 100.0
N = 4
f = np.logspace(0, np.log10(fs / 2), 2000)
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8), sharex=True)
filters = {
'Butterworth': signal.butter(N, fc, btype='low', fs=fs, output='ba'),
'Chebyshev I': signal.cheby1(N, 1.0, fc, btype='low', fs=fs, output='ba'),
'Chebyshev II': signal.cheby2(N, 40.0, fc, btype='low', fs=fs, output='ba'),
'Elliptic': signal.ellip(N, 1.0, 40.0, fc, btype='low', fs=fs, output='ba'),
}
for name, (b, a) in filters.items():
_, h = signal.freqz(b, a, worN=f, fs=fs)
ax1.semilogx(f, 20 * np.log10(np.abs(h) + 1e-12), label=name, linewidth=2)
ax2.semilogx(f, np.degrees(np.unwrap(np.angle(h))), label=name, linewidth=2)
ax1.axvline(fc, color='r', linestyle='--', alpha=0.5, label=f'$f_c$ = {fc} Hz')
ax1.axhline(-3, color='gray', linestyle=':', alpha=0.5)
ax1.set_ylabel('Magnitude [dB]')
ax1.set_title(f'Bode Plot Comparison (N={N})')
ax1.set_ylim(-80, 5)
ax1.grid(True, which='both', alpha=0.3)
ax1.legend()
ax2.set_xlabel('Frequency [Hz]')
ax2.set_ylabel('Phase [degrees]')
ax2.grid(True, which='both', alpha=0.3)
ax2.legend()
plt.tight_layout()
plt.show()
At identical order and cutoff, the Butterworth’s flat passband, Chebyshev I’s passband ripple, Chebyshev II’s stopband ripple, and the elliptic filter’s both-band ripple can be directly compared on the same Bode plot.
Design Guidelines
Choosing the Order from Cutoff Specifications
Given a passband edge \(f_p\) with maximum attenuation \(A_p\) [dB] and stopband edge \(f_s\) with minimum attenuation \(A_s\) [dB], the Butterworth order is
\[N \geq \frac{\log_{10}\!\left(\dfrac{10^{A_s/10} - 1}{10^{A_p/10} - 1}\right)}{2\log_{10}(f_s / f_p)} \tag{8}\]which is essentially “from two points on the Bode plot, infer the slope and back out the required roll-off rate.” scipy.signal.buttord automates this.
Watch the Phase
A steeper magnitude roll-off comes at the cost of stronger phase rotation and more frequency-dependent group delay:
- Audio and medical signals (avoid phase distortion): use FIR for linear phase, or apply
filtfiltfor zero-phase processing - Real-time control (minimize group delay): Bessel filter or low-order Butterworth
- Passband flatness is top priority: Butterworth
- Sharpest transition is top priority: Chebyshev or elliptic
The phase plot of a Bode diagram captures this magnitude/phase trade-off at a single glance.
Relation to Adaptive Filters
Bode plots only make sense for time-invariant fixed filters. Adaptive filters such as LMS/RLS have coefficients that vary over time, so a Bode plot is only a snapshot of one instant. Conversely, the Bode plot of a target response can be the goal that an adaptive filter learns to approximate.
Connection to FFT and PSD
A Bode plot describes the filter itself. To see how a real signal behaves after passing through it, combine the filter with FFT and windowing + PSD estimation. From input spectrum \(X(f)\) and output spectrum \(Y(f) = H(f) X(f)\) , estimating \(|H(f)| = |Y(f)/X(f)|\) gives an empirical Bode plot, which is the fundamental principle of system identification.
Summary
- A Bode plot shows the frequency response \(H(j\omega)\) as magnitude [dB] + phase [deg] on a log frequency axis
- Decibels convert serial cascading into addition; the log axis makes multi-decade behavior uniformly readable
- A first-order pole/zero gives \(\pm 20\) dB/dec, a second-order resonance gives \(\pm 40\) dB/dec, and an \(N\) -th order filter gives \(\pm 20N\) dB/dec
- The unique signatures of Butterworth, Chebyshev, elliptic, highpass, bandpass, and notch filters all appear directly on a Bode plot
- In Python,
scipy.signal.bode(continuous-time) andscipy.signal.freqz(discrete-time) are the standard tools
See the related articles below for the design theory of each specific filter type.
Related Articles
- Butterworth Filter Design: Theory and Python Implementation - The canonical IIR filter whose maximally flat roll-off is best understood on a Bode plot.
- Chebyshev Filter Design: Theory and Python Implementation - Learn how passband ripple appears on a Bode plot.
- Highpass Filter Design and Python Implementation - Read the low-frequency roll-off and \(+90^\circ\) phase rotation on a Bode plot.
- Lowpass Filter Design Compared - The most fundamental subject for Bode plot comparison.
- Bandpass Filter Design and Python Implementation - Observe band-centered magnitude and the symmetric phase profile.
- Notch Filter Design and Python Implementation - See the deep dip at \(f_0\) and the sharp phase flip in the Bode plot.
- FIR vs IIR Filters: Characteristics, Design, and Python Implementation - Understand how linear-phase FIR produces a straight-line phase plot.
- Adaptive Filters (LMS/RLS): Theory and Python Implementation - Time-varying filters that contrast with the time-invariant Bode plot.
- Fast Fourier Transform (FFT): Theory and Python Implementation - The FFT theory used to measure empirical Bode plots.
- Window Functions and PSD Estimation - Leakage suppression and PSD estimation for empirical Bode plots.
- Bessel Filter: Theory and Python Implementation - Bessel’s maximally flat group delay appears as a near-linear phase plot — the cleanest comparison target against Butterworth and Chebyshev phase responses.
- Time-Frequency Analysis Guide - Sister hub that extends from the time-invariant frequency response shown on a Bode plot to the FFT / STFT / wavelet / Hilbert toolkit for time-varying and non-stationary signals.
References
- Bode, H. W. (1945). Network Analysis and Feedback Amplifier Design. Van Nostrand.
- Ogata, K. (2010). Modern Control Engineering (5th ed.). Prentice Hall.
- Oppenheim, A. V., & Schafer, R. W. (2009). Discrete-Time Signal Processing (3rd ed.). Prentice Hall.
- scipy.signal.bode — SciPy documentation
- scipy.signal.freqz — SciPy documentation