SciPy Cheatsheet
Signal Processing
Use this SciPy reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
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
| Format | Variables | Notes |
|---|---|---|
ba | b, a (numerator/denominator) | Default butter output; unstable for high-order |
zpk | z, p, k (zeros, poles, gain) | Better for design; less stable for filtering |
sos | sos (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, acoefficients become numerically unstable forN >= 4at extreme cutoff frequencies. Useoutput='sos'andsosfiltfilt/sosfilt.
filtfiltdoubles the filter order (passes signal forward then backward). A 4th-orderbutterfilter applied withfiltfiltbehaves like an 8th-order filter.
Cutoff frequency normalization: Without
fs=,Wnis normalized to Nyquist (Wn=1.0 = fs/2). Withfs=,Wnis in Hz. Always passfsto avoid confusion.
find_peaksonly finds local maxima. To find minima, negate the signal. To find peaks with negative values, setheightappropriately.
decimatevsresample_poly:decimateis limited to integer downsampling ratios and applies a low-pass filter internally.resample_polyhandles arbitrary rational ratios and is generally preferred.