SciPy Cheatsheet

Signal Processing

Use this SciPy reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.

Core Import

from scipy import signal
import numpy as np

# Common direct imports
from scipy.signal import (
    butter, cheby1, cheby2, ellip, bessel,
    filtfilt, lfilter, sosfilt, sosfiltfilt,
    freqz, freqs, sosfreqz,
    find_peaks, peak_prominences, peak_widths,
    correlate, convolve, fftconvolve,
    spectrogram, stft, istft, welch, periodogram, lombscargle,
    resample, resample_poly, decimate,
    hilbert, chirp, gausspulse, sweep_poly,
    tf2sos, tf2zpk, zpk2sos, zpk2tf, sos2tf,
    iirfilter, firwin, firwin2, remez,
    savgol_filter, medfilt, wiener
)

Filter Design

IIR Filters (Analog → Digital)

from scipy.signal import butter, cheby1, cheby2, ellip, bessel

fs = 1000.0    # sample rate (Hz)
fc = 100.0     # cutoff frequency (Hz)

# Butterworth — maximally flat passband
b, a = butter(N=4, Wn=fc, btype='low', analog=False, fs=fs)
# btype: 'low', 'high', 'band', 'bandstop'

# Band-pass
b, a = butter(N=4, Wn=[50, 150], btype='band', fs=fs)

# Chebyshev Type I — equiripple passband
b, a = cheby1(N=4, rp=1, Wn=fc, btype='low', fs=fs)   # rp = max ripple (dB)

# Chebyshev Type II — equiripple stopband
b, a = cheby2(N=4, rs=40, Wn=fc, btype='low', fs=fs)  # rs = min attenuation (dB)

# Elliptic — equiripple in both bands (steepest rolloff)
b, a = ellip(N=4, rp=1, rs=60, Wn=fc, btype='low', fs=fs)

# Bessel — maximally flat group delay (linear phase)
b, a = bessel(N=4, Wn=fc, btype='low', norm='phase', fs=fs)
# norm: 'phase', 'delay', 'mag'

Design with iirfilter (Unified API)

from scipy.signal import iirfilter, iirdesign

b, a = iirfilter(N=4, Wn=100, btype='low', ftype='butter', fs=1000, output='ba')
sos = iirfilter(N=4, Wn=100, btype='low', ftype='butter', fs=1000, output='sos')

# Design from specs (passband/stopband frequencies and attenuations)
b, a = iirdesign(wp=0.2, ws=0.4, gpass=1, gstop=40, ftype='ellip')
# wp, ws as fractions of Nyquist [0,1] when fs not given

FIR Filters

from scipy.signal import firwin, firwin2, remez, minimum_phase

# firwin — windowed sinc design
h = firwin(numtaps=101, cutoff=100.0, window='hamming', fs=1000.0, pass_zero='low')
h = firwin(numtaps=101, cutoff=[50, 150], window='hamming', fs=1000.0, pass_zero=False)
# pass_zero: True/'low'=lowpass, False/'band'=bandpass, 'high'=highpass, 'bandstop'=bandstop

# Common windows
# 'hamming', 'hann', 'blackman', 'kaiser', ('kaiser', beta), 'flattop'

# Kaiser window — control sidelobe vs transition width
# As rule of thumb: beta~2.285*(N-1)*width/fs + 0.48*atten
h = firwin(numtaps=201, cutoff=100.0, window=('kaiser', 8.0), fs=1000.0)

# firwin2 — arbitrary frequency response
freqs_spec = [0, 100, 200, 300, 500]
gains_spec = [0, 1, 1, 0, 0]
h = firwin2(numtaps=101, freq=freqs_spec, gain=gains_spec, fs=1000.0)

# Parks-McClellan (equiripple, optimal)
h = remez(numtaps=101,
           bands=[0, 80, 100, 500],
           desired=[0, 1],            # 0 in [0,80], 1 in [100,500]
           fs=1000.0)

Filter Formats: BA, ZPK, SOS

FormatVariablesNotes
bab, a (numerator/denominator)Default butter output; unstable for high-order
zpkz, p, k (zeros, poles, gain)Better for design; less stable for filtering
sossos (second-order sections)Most numerically stable; use for filtering
from scipy.signal import butter, tf2sos, tf2zpk, zpk2sos, sos2tf

# Get SOS directly (recommended)
sos = butter(N=8, Wn=100, btype='low', fs=1000, output='sos')

# Convert between formats
b, a = butter(8, 100, btype='low', fs=1000, output='ba')
sos  = tf2sos(b, a)         # ba → sos
z, p, k = tf2zpk(b, a)     # ba → zpk
sos  = zpk2sos(z, p, k)    # zpk → sos
b, a = sos2tf(sos)          # sos → ba

Applying Filters

from scipy.signal import lfilter, filtfilt, sosfilt, sosfiltfilt

t = np.linspace(0, 1, 1000)
x = np.sin(2*np.pi*50*t) + np.random.randn(1000)*0.5

sos = butter(4, 100, btype='low', fs=1000, output='sos')

# Zero-phase filtering (forward-backward) — no phase shift, doubles effective order
y = sosfiltfilt(sos, x)                     # recommended

# Causal filtering (forward only) — introduces phase delay
y = sosfilt(sos, x)

# Initial conditions for sosfilt (useful for streaming)
zi = signal.sosfilt_zi(sos) * x[0]         # initial conditions
y, zf = sosfilt(sos, x, zi=zi)

# Legacy BA-format versions
b, a = butter(4, 100, btype='low', fs=1000, output='ba')
y = filtfilt(b, a, x)     # zero-phase
y = lfilter(b, a, x)      # causal

# Savitzky-Golay (polynomial smoothing)
from scipy.signal import savgol_filter
y_smooth = savgol_filter(x, window_length=51, polyorder=3, deriv=0)
y_deriv  = savgol_filter(x, window_length=51, polyorder=3, deriv=1, delta=1/1000)

Frequency Response

from scipy.signal import freqz, freqs, sosfreqz

# Digital filter (ba)
w, h = freqz(b, a, worN=512)    # w in rad/sample
freq = w * fs / (2 * np.pi)     # convert to Hz
import matplotlib.pyplot as plt
plt.plot(freq, 20*np.log10(abs(h)))

# Digital filter (sos)
w, h = sosfreqz(sos, worN=512, fs=fs)   # fs given → w in Hz

# Analog filter
w, h = freqs(b, a, worN=np.logspace(0, 4, 200))

# Group delay
w, gd = signal.group_delay((b, a))

Spectral Analysis

from scipy.signal import welch, periodogram, spectrogram, stft, istft

fs = 1000.0
x = np.random.randn(10000)

# Welch's power spectral density (averaged)
f, Pxx = welch(x, fs=fs, nperseg=256, noverlap=128, window='hann',
                scaling='density')   # 'density' (V²/Hz) or 'spectrum' (V²)

# Periodogram (single FFT, noisy)
f, Pxx = periodogram(x, fs=fs, window='hann', scaling='density')

# Spectrogram (time-frequency)
f, t_spec, Sxx = spectrogram(x, fs=fs, nperseg=256, noverlap=200,
                               window='hann', scaling='density')
# Sxx.shape = (freq_bins, time_bins)

# STFT / ISTFT
f, t_stft, Zxx = stft(x, fs=fs, nperseg=256, noverlap=128, window='hann')
x_rec, t_rec = istft(Zxx, fs=fs, nperseg=256, noverlap=128, window='hann')

# Lomb-Scargle (non-uniform time sampling)
from scipy.signal import lombscargle
t_uneven = np.sort(np.random.rand(200)) * 10
x_uneven = np.sin(2*np.pi*2*t_uneven)
f_ls = np.linspace(0.1, 10, 500)
omega = 2 * np.pi * f_ls
pgram = lombscargle(t_uneven, x_uneven, omega, normalize=True)

Peak Finding

from scipy.signal import find_peaks, peak_prominences, peak_widths

x = np.sin(np.linspace(0, 10, 500)) + np.random.randn(500)*0.1

# Basic peak finding
peaks, props = find_peaks(x)

# With constraints
peaks, props = find_peaks(x,
    height=0.5,              # minimum peak height (scalar or [min, max])
    threshold=0.1,           # min vertical distance to neighbors
    distance=20,             # min samples between peaks
    prominence=0.3,          # min prominence
    width=5,                 # min peak width
    wlen=100,                # window for prominence calculation
    rel_height=0.5)          # fractional height for width measurement

# Returned properties dict (only requested keys present)
print(props.keys())  # 'peak_heights', 'prominences', 'widths', etc.

# Separate computations
prominences, l_bases, r_bases = peak_prominences(x, peaks)
widths, width_heights, l_ips, r_ips = peak_widths(x, peaks, rel_height=0.5)

# Find valleys (negate signal)
valleys, _ = find_peaks(-x, prominence=0.3)

Convolution & Correlation

from scipy.signal import convolve, correlate, fftconvolve, oaconvolve

a = np.array([1, 2, 3, 4, 5], dtype=float)
b = np.array([1, 0, -1], dtype=float)

# 1-D convolution
c = convolve(a, b, mode='full')     # len = len(a)+len(b)-1
c = convolve(a, b, mode='same')     # len = max(len(a), len(b))
c = convolve(a, b, mode='valid')    # len = max(len(a), len(b)) - min(...) + 1

# Cross-correlation (no flip of b)
r = correlate(a, b, mode='full')

# FFT-based (faster for large arrays)
c = fftconvolve(a, b, mode='same')

# Overlap-add (best for very different-length arrays)
c = oaconvolve(a, b, mode='same')

# 2-D
from scipy.signal import convolve2d, correlate2d
img = np.random.rand(100, 100)
kernel = np.ones((5, 5)) / 25
smoothed = convolve2d(img, kernel, mode='same', boundary='symm')

Resampling

from scipy.signal import resample, resample_poly, decimate

x = np.sin(np.linspace(0, 2*np.pi, 1000))

# Resample to N samples (FFT-based)
x_resampled = resample(x, 250)    # downsample 4x

# Resample by rational factor (polyphase, anti-aliased)
x_up = resample_poly(x, up=3, down=2)          # 3/2 rate change
x_up = resample_poly(x, 3, 2, window=('kaiser', 5.0))

# Decimate (integer downsampling, applies anti-alias filter)
x_down = decimate(x, q=4, ftype='iir', zero_phase=True)  # q must be <= 13 for iir
x_down = decimate(x, q=10, ftype='fir')

Signal Generation

from scipy.signal import chirp, gausspulse, sweep_poly, unit_impulse

t = np.linspace(0, 1, 1000)

# Linear chirp (frequency sweep)
x = chirp(t, f0=10, f1=100, t1=1, method='linear')
x = chirp(t, f0=1, f1=100, t1=1, method='logarithmic')
x = chirp(t, f0=1, f1=100, t1=1, method='quadratic')

# Gaussian pulse
x, envelope = gausspulse(t - 0.5, fc=50, bw=0.5, retenv=True)

# Unit impulse
imp = unit_impulse(100, idx='mid')      # impulse at center
imp = unit_impulse(100, idx=10)         # impulse at index 10

# Square wave
x = signal.square(2 * np.pi * 5 * t)          # duty cycle = 0.5
x = signal.square(2 * np.pi * 5 * t, duty=0.3)

# Sawtooth
x = signal.sawtooth(2 * np.pi * 5 * t)       # rising
x = signal.sawtooth(2 * np.pi * 5 * t, width=0)  # falling

Analytic Signal & Hilbert Transform

from scipy.signal import hilbert

x = np.sin(2*np.pi*10*t)

analytic = hilbert(x)            # complex analytic signal
envelope = np.abs(analytic)      # instantaneous amplitude
inst_phase = np.unwrap(np.angle(analytic))  # instantaneous phase
inst_freq = np.diff(inst_phase) / (2*np.pi*dt)  # instantaneous frequency

Common Gotchas

Always use SOS format for IIR filters. b, a coefficients become numerically unstable for N >= 4 at extreme cutoff frequencies. Use output='sos' and sosfiltfilt/sosfilt.

filtfilt doubles the filter order (passes signal forward then backward). A 4th-order butter filter applied with filtfilt behaves like an 8th-order filter.

Cutoff frequency normalization: Without fs=, Wn is normalized to Nyquist (Wn=1.0 = fs/2). With fs=, Wn is in Hz. Always pass fs to avoid confusion.

find_peaks only finds local maxima. To find minima, negate the signal. To find peaks with negative values, set height appropriately.

decimate vs resample_poly: decimate is limited to integer downsampling ratios and applies a low-pass filter internally. resample_poly handles arbitrary rational ratios and is generally preferred.