Fast Fourier Transform (FFT) in Python with NumPy

A fast Fourier transform in Python takes one NumPy call: np.fft.rfft(signal) turns a signal into its frequency components, and np.fft.rfftfreq() tells you which frequency each value belongs to:

import numpy as np
spectrum = np.fft.rfft(signal)
freqs = np.fft.rfftfreq(len(signal), d=1 / sample_rate)

All the numbers and the plot below come from Python 3.12.5, NumPy 2.5.3 and SciPy 1.18.1, run on a synthetic signal so you can see exactly what goes in and what comes out.

We’ll find the hidden frequencies in a noisy signal, plot the spectrum, filter out noise, spot a weekly cycle in daily website traffic, and see why it’s called fast.

Finding frequencies with numpy.fft

The signal you’ll analyze mixes a 50 Hz wave, a weaker 120 Hz wave and a lot of random noise. Looking at it, you’d never guess what’s inside; the FFT pulls both frequencies straight out:

import numpy as np

fs = 1000                                  # samples per second
t = np.arange(0, 1, 1 / fs)                # one second of data
rng = np.random.default_rng(0)
signal = 1.0 * np.sin(2 * np.pi * 50 * t) + 0.5 * np.sin(2 * np.pi * 120 * t) + rng.normal(0, 0.8, t.size)

spectrum = np.fft.rfft(signal)                    # FFT for real-valued input
freqs = np.fft.rfftfreq(signal.size, d=1 / fs)    # the frequency of each bin, in Hz
amplitude = 2 * np.abs(spectrum) / signal.size    # scale back to the signal's units

top = np.argsort(amplitude)[-2:][::-1]
for i in top:
    print(f"{freqs[i]:>6.1f} Hz  amplitude {amplitude[i]:.2f}")
print("bins:", spectrum.size, "| resolution:", freqs[1] - freqs[0], "Hz")

Output:

  50.0 Hz  amplitude 1.01
 120.0 Hz  amplitude 0.52
bins: 501 | resolution: 1.0 Hz
Command Prompt output of NumPy rfft finding the 50 Hz and 120 Hz frequencies hidden in a noisy signal with their amplitudes
The two strongest bins are 50 Hz and 120 Hz, with amplitudes close to the 1.0 and 0.5 we put in.

You’ll want rfft() for real-valued data like audio or sensor readings; it returns only the non-negative frequencies. Multiplying by 2 and dividing by the length scales the result back to the original amplitudes.

Your frequency resolution is the sample rate divided by the number of samples. One second of data gives 1 Hz bins; record longer to separate frequencies that are closer together.

Plotting the FFT spectrum

A spectrum plot lets you see the same thing at a glance. The time-domain signal looks like noise, but the frequency domain has two clear spikes:

import numpy as np

fs = 1000                                  # samples per second
t = np.arange(0, 1, 1 / fs)                # one second of data
rng = np.random.default_rng(0)
signal = 1.0 * np.sin(2 * np.pi * 50 * t) + 0.5 * np.sin(2 * np.pi * 120 * t) + rng.normal(0, 0.8, t.size)

import matplotlib.pyplot as plt

amplitude = 2 * np.abs(np.fft.rfft(signal)) / signal.size
freqs = np.fft.rfftfreq(signal.size, d=1 / fs)

fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(8, 5.5))
ax1.plot(t[:200], signal[:200], lw=0.8)
ax1.set_title("Noisy signal (first 0.2 s)")
ax1.set_xlabel("Time (s)")
ax2.plot(freqs, amplitude, lw=0.8)
ax2.set_title("FFT amplitude spectrum")
ax2.set_xlabel("Frequency (Hz)")
ax2.set_xlim(0, 250)
plt.tight_layout()
plt.show()
Matplotlib plot of a noisy time-domain signal above its NumPy FFT amplitude spectrum with peaks at 50 Hz and 120 Hz
Two sharp peaks stand above a flat noise floor. That’s the whole point of looking in the frequency domain.

How do you filter noise with an FFT?

Zero out the weak frequency bins, then run the inverse transform, irfft(), to get back to a cleaned-up signal:

import numpy as np

fs = 1000                                  # samples per second
t = np.arange(0, 1, 1 / fs)                # one second of data
rng = np.random.default_rng(0)
signal = 1.0 * np.sin(2 * np.pi * 50 * t) + 0.5 * np.sin(2 * np.pi * 120 * t) + rng.normal(0, 0.8, t.size)

clean = 1.0 * np.sin(2 * np.pi * 50 * t) + 0.5 * np.sin(2 * np.pi * 120 * t)

spectrum = np.fft.rfft(signal)
freqs = np.fft.rfftfreq(signal.size, d=1 / fs)
amplitude = 2 * np.abs(spectrum) / signal.size

spectrum[amplitude < 0.25] = 0              # keep only the strong frequencies
filtered = np.fft.irfft(spectrum, n=signal.size)

rms = lambda x: np.sqrt(np.mean(x ** 2))
print(f"error before filtering: {rms(signal - clean):.3f}")
print(f"error after filtering:  {rms(filtered - clean):.3f}")
print("frequencies kept:", freqs[spectrum != 0])

Output:

error before filtering: 0.782
error after filtering:  0.032
frequencies kept: [ 50. 120.]
Command Prompt output of filtering a noisy signal with NumPy rfft and irfft, showing the error dropping and the frequencies kept
Keeping just the 50 Hz and 120 Hz bins cuts the error from about 0.78 to 0.03.

I use a hard cutoff like this for demos. For real audio or sensor filtering, SciPy’s filter design tools give smoother results without ringing artifacts.

Using FFT to find a weekly cycle in business data

FFT isn’t just for audio, and you can use it on business data too. A year of daily website visits has a weekday/weekend pattern buried under growth and noise. Remove the trend first, then look for the strongest cycle:

import numpy as np

rng = np.random.default_rng(3)
days = np.arange(364)                                   # 52 weeks of daily visits
weekly = 1200 * np.cos(2 * np.pi * days / 7)             # weekday/weekend swing
visits = 8000 + 4 * days + weekly + rng.normal(0, 500, days.size)

detrended = visits - np.polyval(np.polyfit(days, visits, 1), days)
amp = np.abs(np.fft.rfft(detrended))
freqs = np.fft.rfftfreq(days.size, d=1)                  # cycles per day
peak = np.argmax(amp[1:]) + 1                            # skip the zero-frequency bin
print(f"strongest cycle: every {1 / freqs[peak]:.1f} days")

Output:

strongest cycle: every 7.0 days
Command Prompt output of a NumPy FFT on a year of daily website visits finding the strongest cycle every 7 days
The FFT finds the 7-day cycle, even with a growth trend and plenty of noise in the data.

The trend line comes from np.polyfit. Removing it keeps the slow growth from swamping the lowest frequencies. For other ways to explore patterns in data, see principal component analysis in Python.

Why is it called the fast Fourier transform?

The direct formula compares every sample with every frequency, which is O(n²). The FFT reorganizes that work into O(n log n). On just 2,048 samples you’ll see a dramatic difference:

import timeit
import numpy as np
from scipy import fft as sfft

x = np.random.default_rng(0).normal(size=2048)

def dft(x):                                    # the direct O(n^2) formula
    n = np.arange(x.size)
    return np.exp(-2j * np.pi * n[:, None] * n / x.size) @ x

print("same result:", np.allclose(dft(x), np.fft.fft(x)), np.allclose(np.fft.fft(x), sfft.fft(x)))
t_dft = timeit.timeit(lambda: dft(x), number=3) / 3
t_np = timeit.timeit(lambda: np.fft.fft(x), number=200) / 200
t_sp = timeit.timeit(lambda: sfft.fft(x), number=200) / 200
print(f"direct DFT {t_dft * 1000:.1f} ms | numpy.fft {t_np * 1000:.3f} ms | scipy.fft {t_sp * 1000:.3f} ms")

Output:

same result: True True
direct DFT 168.7 ms | numpy.fft 0.022 ms | scipy.fft 0.012 ms

NumPy and SciPy give identical results. scipy.fft adds extras such as multithreading with workers= and more transform types, so I use it for heavier work.

numpy.fft functions you’ll use most

FunctionWhat it does
np.fft.rfft(x)FFT of real input (non-negative frequencies only)
np.fft.rfftfreq(n, d)Frequency for each rfft bin, given sample spacing d
np.fft.irfft(X, n)Inverse of rfft, back to the time domain
np.fft.fft(x)Full complex FFT, positive and negative frequencies
np.fft.fftshift(X)Move zero frequency to the center for plotting

Keep exploring NumPy and SciPy:

Frequently asked questions

How do I do a fast Fourier transform in Python?

Use np.fft.rfft(signal) for real data and np.fft.rfftfreq(len(signal), d=1/sample_rate) to get the matching frequencies.

What’s the difference between fft and rfft?

fft returns positive and negative frequencies for complex input. rfft is for real input and returns only the non-negative half, which is all you need.

How do I get the amplitude from an FFT?

Take np.abs() of the result, multiply by 2 and divide by the number of samples (leave the zero-frequency bin unscaled by 2).

Should I use numpy.fft or scipy.fft?

Both are fine. scipy.fft is usually a bit faster on large inputs and supports multiple worker threads.

Why are my FFT frequencies wrong?

Usually the sample spacing passed to rfftfreq is wrong. It must be 1 divided by your sample rate.

Where is numpy.fft documented?