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))
  • rfft returns only the non-negative frequencies; use irfft to get back to a real signal.
  • fftfreq and rfftfreq build 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]| / N for 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))
WindowMain lobeBest for
boxcarNarrowestSignals that are exactly periodic in the record
hannModerateGeneral purpose; the sensible default
hammingModerate, lower sidelobesTone detection near other tones
blackmanWideVery large dynamic range
flattopWidestAccurate 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.fft uses the pocketfft implementation and is generally faster, especially for sizes with large prime factors.
  • It adds rfft with workers, DCT and DST types I to IV, fractional FFTs, and a standardised next_fast_len for picking a good length.
  • Both produce the same mathematics, so replacing np.fft with scipy.fft is a drop-in change in most code.
  • scipy.fft.next_fast_len is 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.

Statistics and signal processing Image processing with scipy.ndimage

Last refreshed 2026-09-18.