Where the average filter keeps folding every new measurement into a mean taken over the entire history, the moving average instead averages only the most recent \(n\) samples. Older samples are dropped as new ones arrive, so the estimate can keep tracking a signal that changes over time instead of settling on a single fixed value.
Definition
The moving average of order \(n\) at step \(k\) is the mean of the last \(n\) samples ending at \(k\):
\[ \overline{x}_k = \frac{x_{k-n+1}+x_{k-n+2}+\dots+x_k}{n}. \]
Going from step \(k-1\) to step \(k\), the window of \(n\) samples shifts by one position: it drops \(x_{k-n}\), the oldest sample it used to contain, and picks up \(x_k\), the newest one.
A Recursive Update, With a Catch
Subtracting the average at \(k-1\) from the average at \(k\) cancels every term the two windows share, leaving only the dropped and the added sample:
\[ \begin{array}{rl} \overline{x}_k - \overline{x}_{k-1} &= \dfrac{x_{k-n+1}+x_{k-n+2}+\dots+x_k}{n} - \dfrac{x_{k-n}+x_{k-n+1}+\dots+x_{k-1}}{n}\\[6pt] &= \dfrac{x_k - x_{k-n}}{n}. \end{array} \]
Rearranged, this gives a recursive update that looks exactly as convenient as the one for the average filter:
\[ \overline{x}_k = \overline{x}_{k-1} + \frac{x_k - x_{k-n}}{n}. \]
The catch is what this formula needs as input: not just the previous average and a sample count, but the specific value \(x_{k-n}\) that is about to leave the window. That value is one particular past measurement, not something derivable from the running average alone, so it has to have been kept around since it was first read. The moving average therefore cannot be computed from a single running scalar the way the average filter can; it needs the last \(n\) raw samples in memory at all times, typically held in a small ring buffer or FIFO queue of length \(n\).
Software Implementation
A queue holds the samples currently in the window, and a running sum is kept so that each step only needs one addition and one subtraction, regardless of \(n\):
function MovingAverage(n) {
let window = []
let sum = 0
return function (xk) {
window.push(xk)
sum += xk
if (window.length > n) {
sum -= window.shift()
}
return sum / window.length
}
} Dividing by window.length rather than by the fixed n matters during the first \(n-1\) calls, while the window is still filling up: dividing by n before \(n\) samples have actually been observed would understate the average by treating missing samples as if they contributed zero. Once the window has filled, window.length equals \(n\) and the two are the same. An implementation that only needs to run after a full window of \(n\) samples has already been collected can skip the warm-up case and divide by the constant \(n\) throughout.
Complexity
Maintaining a running sum keeps each update at \(O(1)\) time, at the cost of \(O(n)\) memory for the window itself. Recomputing the sum of the last \(n\) samples from scratch at every step, without reusing the previous sum, would cost \(O(n)\) time per update and \(O(n^2)\) time over \(n\) consecutive steps, for the same \(O(n)\) memory; the running-sum update removes that redundant work entirely.
Noise Reduction Versus Lag
Averaging \(n\) independent noisy samples divides the noise variance by \(n\), so the standard deviation of the filtered signal shrinks by a factor of \(\sqrt{n}\) compared to the raw measurements. A larger window therefore produces a visibly smoother output. The same window, however, is a weighted history of the last \(n\) inputs, so a genuine change in the signal only fully replaces the window's contents after \(n\) steps: the filtered output visibly lags behind the true signal at any transition, and the lag grows with \(n\). Choosing \(n\) is a trade-off between how much noise is removed and how quickly the filter can follow a real change, not a parameter with a single correct value.
Application
This trade-off is the reason moving averages of several different lengths are common side by side, most visibly in financial charting, where a 5-day moving average (\(\mathrm{MA}(5)\)) reacts quickly to recent price moves at the cost of noisier output, while a 20-day moving average (\(\mathrm{MA}(20)\)) is smoother but slower to reflect a genuine trend change; the gap between a short and a long moving average is itself often used as a signal.
Demonstration
Simulated ultrasonic distance readings, oscillating between a near and a far object with added noise, show both effects at once: the filtered curve (red) is noticeably smoother than the raw readings (grey), but it visibly lags behind at every rise and fall, and the lag becomes more pronounced as the window length \(n\) increases.
Key Results
- The moving average of order \(n\) is the mean of only the last \(n\) samples, so it can keep tracking a signal that drifts over time rather than settling on one value.
- Its recursive update \(\overline{x}_k=\overline{x}_{k-1}+\frac{x_k-x_{k-n}}{n}\) still needs the discarded sample \(x_{k-n}\) itself, so unlike the average filter it requires an \(O(n)\) buffer of the last \(n\) raw samples, not just a running scalar.
- A running sum keeps each update at \(O(1)\) time; recomputing the window sum from scratch every step costs \(O(n)\) per update instead.
- During warm-up, before \(n\) samples have been seen, dividing by the current window length rather than by the fixed \(n\) avoids understating the average.
- Noise falls off as \(1/\sqrt{n}\), but the filter's response to a real change lags by on the order of \(n\) samples, making the window length a noise-versus-latency trade-off.