Prefer Butterworth filter over Savitzky-Golay for scientific post-processing¶
2025-05-08
For science & engineering data analysis, we often need to smooth noisy time-series measurements. We want to distort the "real" signal as little as possible (in both amplitude and delay), and we are filtering during post-processing (not real time).
The Savitzky-Golay filter is often touted for providing zero amplitude distortion and zero delay. However, a Butterworth filter applied forward and backward:
- also provides zero amplitude distortion and zero delay
- better removes high-frequency noise
- stands on firmer theoretical ground from a signal-processing perspective
tl;dr: Stop using the Savitzky-Golay filter for post-processing and use a forward-backward Butterworth filter instead. My recommended solution is:
from scipy.signal import sosfiltfilt, butter
fs = ... # [Hz] TODO sampling frequency
fc = ... # [Hz] TODO filter cutoff frequency
x_raw = ... # TODO your signal
ORDER = 5 # 5 probably is fine, note that applying the filter forward-backward
# effectively doubles the filter order.
sos = butter(ORDER, Wn=fc, fs=fs, output="sos")
x_filtered = sosfiltfilt(sos, x_raw)
The superior characteristics of the forward-backward Butterworth filter are illustrated in the Bode plot below. If you are new to Bode plots, or simply want more details, read on below.
The task: remove high-frequency noise without distortion¶
Often, we are trying to measure a relatively slow phenomenon, but our sensors also pick up high-frequency noise.1 For scientific post-processing, it is imperative to remove the high-frequency noise while:
- Accurately preserving the amplitude of the signal.
- Accurately preserving the signal's position in time, e.g. because we want to determine the times at which some events in the signal occurred.
In signal processing terms, this means we require a filter with:
- Gain magnitude of 1 in the pass band
- Zero phase delay
Zero delay with non-causal filters
Zero phase delay is not possible with real-time (causal) filters; but because we are post-processing, we can cheat and "look ahead in time" (i.e. use a non-causal filter). This makes our zero delay requirement possible.
Meet the contenders¶
The Butterworth filter has the flattest possible gain in the pass band (for a filter of a given order with monotonically decreasing gain). That is a strong theoretical recommendation for our scientific post-processing application.
The parameters of the filter are a cutoff frequency and an order:
- The cutoff frequency should be about 2x the highest frequency you care about in the data, and less than the frequency of known noise sources (e.g. 60 Hz power).
- Higher orders make the filter better until numerical instabilities appear. An order of 4 or 5 is good for post-processing with 64-bit floating-point numbers (you may need a lower order for numerical stability with 32-bit floats).
The Savitzky–Golay filter can be thought of as fitting a polynomial to successive windows of the data, but in practice it is implemented as a convolution of the signal with a pre-computed kernel (i.e. a FIR filter). Theoretically, the Savitzky–Golay filter does not have much to recommend it.
Further, the parameters of the filter are specified as a window length and a polynomial order.
It is tricky to relate these to the more useful cutoff frequency; but the window length for a given cutoff frequency can be approximated using equation 11 from this paper, which I implemented in python as the savgol_window_length
function.
Visualize filter performance with Bode plots¶
A Bode plot visualizes the magnitude and phase of a filter's response versus frequency. For the post-processing application, we want a Bode plot that looks like this:

Now, we can return to the figure this post started with (it is inlined again below): a Bode plot of the Butterworth (blue) and Savitzky–Golay (tan) filters. For a fair comparison, both filters are set for a -3 dB cutoff at 0.01 times the sampling frequency (vertical black line).

Both filters have magnitude of 1 in most of the pass band. At frequencies above the cutoff, the gain of the Butterworth filter drops off rapidly as desired, whereas the behavior of the Savitzky–Golay filter is genuine garbage.
Both filters have zero phase over all frequencies, but that's table stakes for non-causal post-processing.
Compare filters on noisy time-series data¶
The figure below shows each filter's performance on example time series data. The underlying signal is a Lorentzian pulse at t=7 s (black dashed curve), and is contaminated with white noise with a standard deviation of 0.1.

Both the Butterworth forward-backward (blue) and Savitzky–Golay (tan) filters track the overall shape of the signal well and get the pulse peak at the right time.
As a cautionary note, the plot also shows a forward-only Butterworth filter (red). It has phase delay. If you tried to measure the peak time from this filtered signal, you would get the wrong answer (unless you compensate for the phase delay).
With scipy.signal
, use sosfiltfilt
, not sosfilt
!
Zooming in, we see that the Butterworth forward-backward filter does a better job of rejecting high-frequency noise than Savitzky–Golay. This is consistent with what we saw in the Bode plot.

Code¶
The python scripts that made then figures in this post are available on github.
Further reading¶
scipy.signal.butter
docsscipy.signal.sosfiltfilt
docs- S. W. Smith, The Scientist and Engineer's Guide to Digital Signal Processing
Thank you to James Logan for discussing the ideas in this post with me and reviewing the plots. Any errors are my own.
-
This post focuses on removing noise that is separated from the "true" signal in frequency (e.g. electrical noise from 60 Hz power, or high-frequency vibrations of the instrument). Other filtering techniques are better if you want to reject noise on other bases (e.g. outliers). ↩