FFT and spectral analysis with scipy.fft
Transform real signals with rfft, apply an appropriate window, read frequencies correctly, and understand what scipy.fft adds over numpy.fft.
Real-input transforms
import numpy as np
from scipy import fft
fs = 1000.0 # sampling rate in Hz
t = np.arange(0, 2.0, 1 / fs)
sig = 0.6 * np.sin(2 * np.pi * 50 * t) + 0.3 * np.sin(2 * np.pi * 120 * t)
spectrum = fft.rfft(sig) # real input: half the work
freqs = fft.rfftfreq(sig.size, d=1 / fs) # matching frequency axis
magnitude = np.abs(spectrum) / sig.size
magnitude[1:-1] *= 2 # one-sided spectrum correction
peaks = freqs[np.argsort(magnitude)[-3:]]
print(np.sort(peaks))rfftreturns only the non-negative frequencies; useirfftto get back to a real signal.fftfreqandrfftfreqbuild the matching axis. Reading a spectrum without them is the most common mistake in signal analysis.- The amplitude of a component is
2 * |X[k]| / Nfor a one-sided spectrum, not|X[k]|.
Leakage and windowing
window = fft.get_window("hann", sig.size)
windowed = sig * window
f, t_spec, Sxx = fft.spectrogram(sig, fs, nperseg=256, noverlap=128, window="hann")
print(f.shape, t_spec.shape, Sxx.shape)
n = sig.size
nf = np.arange(0, n // 2 + 1)
freq = nf * fs / n
plain = np.abs(np.fft.rfft(sig)) / n
tapered = np.abs(np.fft.rfft(windowed)) / (window.sum())
print(plain[:5].round(4), tapered[:5].round(4))| Window | Main lobe | Best for |
|---|---|---|
boxcar | Narrowest | Signals that are exactly periodic in the record |
hann | Moderate | General purpose; the sensible default |
hamming | Moderate, lower sidelobes | Tone detection near other tones |
blackman | Wide | Very large dynamic range |
flattop | Widest | Accurate amplitudes, poor frequency resolution |
A Fourier transform assumes the record repeats forever. If the signal does not start and end at the same value, that discontinuity spreads energy across the spectrum as leakage. A window tapers the ends and trades a slightly wider peak for much lower sidelobes.
scipy.fft versus numpy.fft
scipy.fftuses the pocketfft implementation and is generally faster, especially for sizes with large prime factors.- It adds
rfftwithworkers, DCT and DST types I to IV, fractional FFTs, and a standardisednext_fast_lenfor picking a good length. - Both produce the same mathematics, so replacing
np.fftwithscipy.fftis a drop-in change in most code. scipy.fft.next_fast_lenis the honest way to pad a signal, rather than rounding to the next power of two out of habit.
💡
Forgetting to scale is the subtle failure: an unnormalised spectrum has values proportional to
N, so the same signal sampled for longer appears to have a larger amplitude. Normalise by the number of samples, or by the window sum when you have tapered.FAQ
Why do I see a peak at 0 Hz?
The mean of the signal has been removed by the transform, and a constant is a zero-frequency component. Subtract the mean before transforming if you only care about oscillatory content.
How do I get a spectrogram's time axis in seconds?
fft.spectrogram returns the frequency array, the time array and the power matrix. The time array already accounts for segment length and overlap, so plot against it directly rather than reconstructing it by hand.Related
Statistics and signal processing Image processing with scipy.ndimage
Last refreshed 2026-09-18.