Digital Filter Design Guide: Selection, Comparison, and Python Implementation Hub

Digital filter design guide: pick the right filter (Butterworth, Chebyshev, Elliptic, Bessel, FIR, MA, EMA, Median, adaptive) by purpose, implementation, and priority metric, with a unified Python evaluation framework.

Introduction

“Remove the noise.” “Keep only this band.” “Smooth the waveform.” In real-world signal processing, the hardest question is often which filter to pick. Do you need linear phase? Real-time operation? Robustness to outliers? Different requirements change the optimal answer.

This article serves as a cross-cutting hub that consolidates 11+ filter articles on this blog. It organizes filter selection into three orthogonal axes (purpose, implementation form, priority metric), provides a characteristic-comparison matrix, walks through a use-case decision flow, gives a common evaluation framework built on the Bode plot, and links out to every detailed article. The goal is to provide a map for deciding “which one to use” in the shortest possible path, leaving the deep math to the individual posts.

Three Axes of Filter Selection

In practice, digital filter selection is most easily organized along three independent axes.

Axis 1: Purpose in the Frequency Domain (what to pass, what to block)

TypePassbandTypical use
Lowpass (LP)\(f < f_c\)Anti-aliasing, smoothing, low-frequency extraction
Highpass (HP)\(f > f_c\)DC removal, drift removal, edge enhancement
Bandpass (BP)\(f_1 < f < f_2\)Band extraction, demodulation
Bandstop (BS)\(f < f_1\) or \(f > f_2\)Broadband noise rejection
Notchreject only \(f \approx f_0\)Mains hum (50/60 Hz), narrowband interference
All-passpass everything (phase only changes)Phase correction, group-delay equalization

See lowpass, highpass, bandpass, and notch for details.

Axis 2: Implementation Form (FIR / IIR / fixed / adaptive)

ClassExamplesProperties
FIR (fixed)windowed, Parks–McClellan, moving average, Savitzky–GolayAlways stable, can be exactly linear-phase, more taps needed
IIR (fixed)Butterworth, Chebyshev, Elliptic, BesselSteep with few coefficients, stability care required, nonlinear phase
Adaptive (time-varying)LMS, RLS, WienerTracks changing environments, needs desired/reference signal, convergence tuning
Nonlinear (robust)median, bilateral, morphologicalOutlier-robust, edge-preserving, outside linear theory

The deep comparison between FIR and IIR lives in FIR vs IIR.

Axis 3: Priority Metric (what matters most)

The frequency response \(H(j\omega)\) has multiple metrics that trade off against one another.

MetricDefinitionWhere it matters
Passband flatness\(\max_{f \in \text{pass}} \big\| \|H(jf)\| - 1 \big\|\)Measurement, hi-fi audio, biomedical
Stopband attenuation\(\min_{f \in \text{stop}} 20\log_{10}\|H(jf)\|\)Anti-aliasing, interference rejection
Transition steepnessRoll-off [dB/oct]Spectral separation, channelization
Phase linearity\(\angle H(j\omega) \propto \omega\)Audio, image, biomedical waveforms
Group-delay flatness\(\tau_g(\omega) = -d\angle H/d\omega\) constantControl systems, communications transients
Ripple tolerancePassband/stopband ripple amplitudeBuys design freedom at a cost
Computational costMultiplies/adds per sampleEmbedded, real-time

Maximally flat passband → Butterworth. Steepest possible → Elliptic. Strict phase requirement → linear-phase FIR or Bessel. The mapping from priority metric to filter family practically writes itself.

Characteristic Comparison Matrix

Putting representative filters side by side makes selection obvious.

FilterPassband flatnessStopband attenuationPhaseGroup delayComputeTypical use
ButterworthMaximally flatGentleNonlinearModerateLowGeneral-purpose LP/HP, measurement
Chebyshev IEquirippleSteepStrongly bentLargerLowStopband matters more than passband
Chebyshev IIFlatEquirippleModerateModerateLowAllowed ripple in stopband
Elliptic (Cauer)EquirippleEquirippleMost nonlinearLargestLowMinimum order for steepest cutoff
BesselSmoothGentleNearly linearMost flatLowTransient response, control
Moving Average (MA)\(\text{sinc}\) -shapedWeakLinear\((N-1)/2\)TinySmoothing, trend extraction
EMA (exp. moving avg.)1st-order IIR LPGentleNonlinearSmallTinyReal-time smoothing, tracking
Savitzky–GolayPeak-preservingModerateLinear\((N-1)/2\)LowSpectroscopy, peak-preserving smooth
FIR (windowed)DesignableDesignableExactly linear\((N-1)/2\)Mid/HighAudio, image, biomedical
MedianNonlinearNonlinearUndefined\((N-1)/2\)MidImpulse removal, edge preservation
WienerSignal-stats dep.Signal-stats dep.MMSE-optimalDesign-dep.MidDenoising, signal restoration
Adaptive (LMS/RLS)Time-varyingTime-varyingTime-varyingTime-varyingMid/HighEcho cancellation, interference
ComplementaryFlat when summedFlat when summedDesign-dep.Design-dep.TinySensor fusion (IMU attitude)

Read this matrix column-first: pick the metric you must maximize and the best-in-column filter becomes your first candidate. “Most flat group delay” → Bessel. “Steepest cutoff” → Elliptic. “Linear phase with near-zero compute” → moving average.

Use-Case Decision Flow

Ten typical scenarios cover most decisions you’ll face in practice.

Scenario 1: Just smooth a waveform in real time

EMA (exponential moving average) wins. A 1st-order IIR needs 1 multiply + 1 add per sample, lightest possible. Lower \(\alpha\) for heavier smoothing.

Scenario 2: Offline phase-distortion-free smoothing

Moving average (MA) or scipy.signal.filtfilt + any IIR. The former is exactly linear-phase; the latter is zero-phase (forward+backward pass cancels phase).

Scenario 3: Smooth without flattening peaks/edges

Savitzky–Golay filter. Local polynomial fitting preserves peak position and height far better than MA. The standard for chemistry and spectroscopy.

Scenario 4: Remove spike-like outlier noise

Median filter. Linear filters smear outliers across the window; median is order-statistic-based and removes a single spike completely while preserving edges.

Scenario 5: Need a steep roll-off (anti-aliasing, channel separation)

Elliptic for minimum order, Butterworth if no ripple is allowed (use higher order). Chebyshev sits in between.

Scenario 6: Linear phase is required (audio, biomedical waveforms, images)

Linear-phase FIR by default — coefficient symmetry guarantees exact linear phase, at the cost of more taps. If IIR is forced, Bessel is the next-best approximation.

Scenario 7: Minimize transient/step-response distortion (control, triggered measurement)

Bessel filter is optimal. Its maximally flat group delay means every frequency component is delayed identically, preserving waveform shape with minimal overshoot.

Scenario 8: Real-time, lightweight embedded implementation

EMA > MA > low-order Butterworth in increasing cost. Implement IIR as cascaded 2nd-order sections (SOS) for stability.

Scenario 9: Pinpoint removal of mains hum (50/60 Hz) or narrowband interference

Notch filter. Narrower Q minimizes impact on surrounding signal. Trivial to design: one line with scipy.signal.iirnotch.

Scenario 10: Time-varying environment / reference signal available (echo cancellation, ANC, system ID)

Adaptive filter (LMS/RLS). Fixed coefficients cannot track time-varying systems. If the MMSE-optimal solution is known a priori, the Wiener filter is the gold standard. For multi-sensor fusion, complementary filter trades optimality for almost zero compute.

Quick-Reference Tree

┌─ Outliers (impulses) dominate? ──> Median

├─ Strict linear phase required?
   ├─ Yes ──> FIR (windowed / Parks-McClellan) or Bessel
   └─ No
       
       ├─ Steepest cutoff?      ──> Elliptic > Chebyshev > Butterworth
       ├─ Flattest passband?    ──> Butterworth
       └─ Best transient?       ──> Bessel

├─ Real-time + ultra-light?  ──> EMA / MA
├─ Peak-preserving smooth?   ──> Savitzky-Golay
├─ Mains hum removal?        ──> Notch
└─ Time-varying / reference? ──> Adaptive (LMS/RLS) / Wiener / Complementary

Design Parameter Selection

Cutoff Frequency Normalization

Most scipy.signal calls accept a normalized frequency \(W_n = f_c / (f_s/2)\) , with \(f_s\) the sampling rate and \(W_n \in (0, 1)\) . Pass fs= to specify physical frequencies directly — fewer off-by-two errors.

Order Selection Rule

Given passband attenuation \(A_p\) [dB], stopband attenuation \(A_s\) [dB], passband edge \(f_p\) , and stopband edge \(f_s\) , scipy.signal.iirdesign computes the required order and coefficients automatically.

from scipy import signal

# Spec: fs=1000Hz, ≤1dB ripple up to 80Hz passband edge,
#        ≥40dB attenuation beyond 120Hz stopband edge.
sos = signal.iirdesign(
    wp=80, ws=120, gpass=1.0, gstop=40.0, fs=1000.0,
    ftype="ellip",  # 'butter' / 'cheby1' / 'cheby2' / 'ellip' / 'bessel'
    output="sos",
)

Rule of thumb: start by giving only the spec to iirdesign. Hand-pick the order via butter/cheby1/ellip/bessel only when you need fine control.

Ripple Tolerance

rp (passband ripple [dB]) applies to Chebyshev I / Elliptic; rs (stopband attenuation [dB]) to Chebyshev II / Elliptic. 1 dB of passband ripple is inaudible but often forbidden for instrumentation. Allowing ripple lets you reduce the order — that’s the trade-off.

Group-Delay Constraint

If group delay must stay below a bound, look first at Bessel. If still insufficient, cascade an all-pass network to equalize phase. Use scipy.signal.group_delay((b, a), w=...) to evaluate.

FIR Order Heuristic

For a Kaiser-window FIR, given transition width \(\Delta f\) and stopband attenuation \(A_s\) [dB], the required order is approximately

\[ N \approx \frac{A_s - 8}{2.285 \, \cdot 2\pi \, \Delta f / f_s} \tag{1} \]

Design with scipy.signal.firwin(N, fc, fs=fs, window=("kaiser", beta)), or use the more powerful signal.remez (Parks–McClellan).

from scipy import signal

# FIR lowpass: 1024 taps, cutoff 100Hz, Hamming window
taps = signal.firwin(1024, cutoff=100, fs=1000.0, window="hamming")

# Median filter: window size 5
y = signal.medfilt(x, kernel_size=5)

Common Evaluation Framework

Whatever filter you design, the standard evaluation is to plot four things side by side: impulse response, frequency response (Bode plot), group delay, step response. The skeleton below takes any IIR/FIR coefficients (b, a) and produces all four.

import numpy as np
import matplotlib.pyplot as plt
from scipy import signal


def evaluate_filter(b, a, fs=1000.0, n_impulse=128, title="Filter"):
    """Four-panel evaluation of any IIR/FIR filter."""
    # 1) Impulse response
    impulse = np.zeros(n_impulse)
    impulse[0] = 1.0
    h_imp = signal.lfilter(b, a, impulse)

    # 2) Frequency response (Bode)
    w, h = signal.freqz(b, a, worN=4096, fs=fs)
    mag_db = 20 * np.log10(np.abs(h) + 1e-12)
    phase_deg = np.degrees(np.unwrap(np.angle(h)))

    # 3) Group delay
    w_gd, gd = signal.group_delay((b, a), w=4096, fs=fs)

    # 4) Step response
    step = np.ones(n_impulse)
    h_step = signal.lfilter(b, a, step)

    fig, axes = plt.subplots(2, 2, figsize=(12, 8))

    axes[0, 0].stem(np.arange(n_impulse), h_imp, basefmt=" ")
    axes[0, 0].set_title("Impulse Response")
    axes[0, 0].set_xlabel("n [samples]")
    axes[0, 0].grid(alpha=0.3)

    ax_m = axes[0, 1]
    ax_m.semilogx(w, mag_db, label="Magnitude [dB]")
    ax_m.set_ylim(-80, 5)
    ax_m.set_title("Bode (Magnitude)")
    ax_m.set_xlabel("Frequency [Hz]")
    ax_m.grid(True, which="both", alpha=0.3)

    axes[1, 0].semilogx(w_gd, gd)
    axes[1, 0].set_title("Group Delay [samples]")
    axes[1, 0].set_xlabel("Frequency [Hz]")
    axes[1, 0].grid(True, which="both", alpha=0.3)

    axes[1, 1].plot(np.arange(n_impulse), h_step)
    axes[1, 1].axhline(1.0, color="gray", linestyle=":")
    axes[1, 1].set_title("Step Response")
    axes[1, 1].set_xlabel("n [samples]")
    axes[1, 1].grid(alpha=0.3)

    fig.suptitle(title)
    plt.tight_layout()
    plt.show()


# Side-by-side comparison: same cutoff, same order, four families
fs, fc, N = 1000.0, 100.0, 4
for name, design in [
    ("Butterworth", signal.butter(N, fc, fs=fs, output="ba")),
    ("Chebyshev I", signal.cheby1(N, 1.0, fc, fs=fs, output="ba")),
    ("Elliptic", signal.ellip(N, 1.0, 40.0, fc, fs=fs, output="ba")),
    ("Bessel", signal.bessel(N, fc, fs=fs, norm="mag", output="ba")),
]:
    b, a = design
    evaluate_filter(b, a, fs=fs, title=f"{name} N={N}, fc={fc}Hz")

Run this framework on candidate filters and you immediately see: Butterworth’s flat passband, Chebyshev’s ripple, Elliptic’s steepest roll-off, Bessel’s flat group delay and clean step response. Whenever in doubt, drop the candidates into this skeleton and decide visually.

For deeper reading on frequency response, see Bode plot; to combine with input spectrum analysis, see FFT and windowing + PSD.

Here is the full catalog, grouped by category with a one-line positioning.

IIR Family (fixed coefficients, low compute, steep)

By Frequency Specification

FIR / Averaging Family (linear phase, lightweight)

Adaptive / Statistical Family (time-varying, optimization-based)

Special Purpose / Nonlinear

Evaluation / Analysis Tools

Summary

  • Filter selection becomes tractable when organized along three axes: purpose, implementation form, and priority metric.
  • Read the characteristic comparison matrix column-first: the best filter for your top-priority metric is immediately visible.
  • The use-case decision tree pivots on outlier handling (Median), linear phase (FIR/Bessel), steepness (Elliptic), embedded real-time (EMA/MA), and time-varying systems (Adaptive).
  • For design, give the spec to scipy.signal.iirdesign and let it minimize the order automatically.
  • Always evaluate with the four-panel framework: impulse response, Bode plot, group delay, step response.

For the mathematics and implementation details of each filter, follow the related articles above — every one is designed to drill down vertically from this hub.

References

  • Oppenheim, A. V., & Schafer, R. W. (2009). Discrete-Time Signal Processing (3rd ed.). Prentice Hall.
  • Proakis, J. G., & Manolakis, D. G. (2006). Digital Signal Processing: Principles, Algorithms, and Applications (4th ed.). Prentice Hall.
  • Parks, T. W., & Burrus, C. S. (1987). Digital Filter Design. Wiley.
  • Smith, S. W. (1997). The Scientist and Engineer’s Guide to Digital Signal Processing. California Technical Publishing.
  • scipy.signal — SciPy documentation