Moving average and low-pass filters explained

How a moving average and an exponential low-pass filter smooth noisy sensor readings, what the window size and alpha change, why a median copes with outliers, and the lag every filter costs, shown on a robot driving at a wall. Change the numbers and press Run.

Guidefree, runs in your browser

A filter takes a stream of noisy readings and turns it into a steadier estimate of the thing being measured. The moving average and the exponential low-pass filter are the two that almost everyone starts with. They smooth distance sensors, thermometers, accelerometers and microphones on robots and in electronics of every kind, and the seven-day average of daily figures in the news is a moving average too. On this page the robot drives towards a wall and measures the gap with a distance sensor whose readings are typically about 3 cm out, and each demo below is a real program you can change and run.

The overhead view shows the robot driving up the mat towards the wall. The chart under it shows the gap in cm. raw is what the distance sensor said: jagged, because of the noise. The second line is the filter's estimate. truth is the real gap, which the simulator knows exactly (position(), the lab's overhead camera) and a real robot does not. A good filter stays close to the truth line: smoother than raw, and not behind it.

The idea in two lines

moving average:      estimate = the mean of the last N readings
exponential filter:  estimate = alpha × new reading + (1 - alpha) × estimate
  • N is the window size: how many readings the moving average keeps.
  • alpha is the weight the exponential filter gives each new reading, between 0 and 1.

Both do the same job. A bigger window, or a smaller alpha, leaves less noise and makes the estimate later. Everything on this page is about that trade.

Why readings need filtering

A reading is not a fact. When the robot's sensor says 57.2 cm, the wall is somewhere near 57 cm, and 57.2 is one draw from a spread of possible readings centred on the real gap. The size of that spread is the standard deviation, sigma. The sensor in these demos has a sigma of about 3 cm, and it produces a fresh reading every tenth of a second.

Averaging n independent readings divides the noise by the square root of n:

noise after averaging = sigma / √n

Four readings halve the noise; nine divide it by three. A robot that is standing still can take as many readings as it likes and average them all. A robot that is moving cannot, because the gap is changing while it reads. It needs an estimate that updates with every new reading, and that is what a moving average and a low-pass filter are.

The moving average: choosing the window size

Keep the last N readings in a list and take their mean. When a new reading arrives, it goes on the end and the oldest one falls off the front.

The robot starts 90 cm from the wall and drives at it at 8.2 cm/s. The program averages the last 5 readings, and once the robot is up to speed it measures two things: how far the average sits behind the truth, and how much noise is left in it.

N = 5 at 8.2 cm/s: 1.8 cm of noise left against 3.3 cm in the raw readings, and the average runs 1.5 cm (0.19 s) behind the true gap.
The program
from bugbot import *
connect()

# change this number and press Run
N = 5            # how many readings to average

window = []
errors = []      # the average minus the true gap
forward(45)
for tick in range(80):          # 8 seconds
    raw = distance()
    window.append(raw)
    if len(window) > N:
        window.pop(0)           # the oldest reading falls off
    average = sum(window) / len(window)
    truth = 90 - position()[1]  # the gap, from the overhead camera
    plot("raw", raw)
    plot("average", average)
    plot("truth", truth)
    if tick == 20:
        y0 = position()[1]
    if tick >= 20:              # once the robot is at full speed
        errors.append(average - truth)
    wait(0.1)
speed = (position()[1] - y0) / 6.0
stop()
late = sum(errors) / len(errors)
spread = (sum((e - late) ** 2 for e in errors) / len(errors)) ** 0.5
print("speed", round(speed, 1), "cm/s")
print("late by", round(late, 1), "cm =", round(late / speed, 2), "s")
print("noise left", round(spread, 1), "cm")
Put this demo on your own site

Paste it into a school website, Moodle, Google Sites or a blog. More options on the embed page.

The average line is visibly smoother than raw, and it sits a little above the truth line the whole way in: the robot is closer to the wall than its average says.

That lag comes from what an average is. On a gap that is shrinking steadily, the mean of the last N readings equals the reading from the middle of the window, which is (N - 1) / 2 readings old. At a reading every 0.1 s, a window of 5 is 0.2 s behind. Change N and run it again:

Window N Noise left Late by Theory, (N - 1) / 2 readings
1 (no filter) 3.3 cm 0 s 0 s
3 2.3 cm 0.08 s 0.1 s
5 1.8 cm 0.19 s 0.2 s
9 1.2 cm 0.39 s 0.4 s
15 1.0 cm 0.68 s 0.7 s

The lag grows in step with the window, as the theory says. The noise falls more slowly: 3.3 cm divided by the square root of 9 is 1.1 cm, close to the 1.2 cm measured, and going from 9 readings to 15 buys only another 0.2 cm while it adds 0.3 s of lag. That is the shape of every filter: the first bit of smoothing is cheap and each extra bit costs more delay for less gain.

A moving average costs N numbers of memory and N additions each time round the loop. That is nothing on a laptop and can matter on a small microcontroller reading many sensors quickly.

The exponential filter: choosing alpha

The exponential filter does the same job in one line with nothing to store but the last estimate:

filtered = ALPHA * raw + (1 - ALPHA) * filtered

The new reading gets weight alpha and the old estimate keeps the rest. The reading before gets alpha × (1 - alpha), the one before that alpha × (1 - alpha)², and so on, so older readings fade away geometrically. That is why it is also called an exponentially weighted moving average (EWMA). In electronics it behaves like a resistor and capacitor smoothing a voltage, and both are called a first order low-pass filter.

Here it is on the same drive, with alpha = 0.2.

alpha = 0.2: 1.3 cm of noise left, and the filtered line runs 3.1 cm (0.38 s) behind the true gap, almost exactly what a moving average of 9 does.
The program
from bugbot import *
connect()

# change this number and press Run
ALPHA = 0.2      # the weight given to each new reading

filtered = distance()           # start at the first reading
errors = []      # the filtered value minus the true gap
forward(45)
for tick in range(80):          # 8 seconds
    raw = distance()
    filtered = ALPHA * raw + (1 - ALPHA) * filtered
    truth = 90 - position()[1]  # the gap, from the overhead camera
    plot("raw", raw)
    plot("filtered", filtered)
    plot("truth", truth)
    if tick == 20:
        y0 = position()[1]
    if tick >= 20:              # once the robot is at full speed
        errors.append(filtered - truth)
    wait(0.1)
speed = (position()[1] - y0) / 6.0
stop()
late = sum(errors) / len(errors)
spread = (sum((e - late) ** 2 for e in errors) / len(errors)) ** 0.5
print("speed", round(speed, 1), "cm/s")
print("late by", round(late, 1), "cm =", round(late / speed, 2), "s")
print("noise left", round(spread, 1), "cm")
Put this demo on your own site

Paste it into a school website, Moodle, Google Sites or a blog. More options on the embed page.

For the exponential filter the lag is about (1 - alpha) / alpha readings. Change ALPHA and run it again:

alpha Noise left Late by Theory, (1 - alpha) / alpha readings Like a window of
0.5 2.2 cm 0.08 s 0.1 s 3
0.3 1.6 cm 0.22 s 0.23 s 5.7
0.2 1.3 cm 0.38 s 0.4 s 9
0.1 1.1 cm 0.85 s 0.9 s 19

The last column is (2 - alpha) / alpha, the moving average window with the same lag. Turned round, a window of N matches alpha = 2 / (N + 1). It is worth knowing both ways, so that "alpha = 0.1" turns into a picture: about the last 19 readings, nearly 2 seconds of them.

Compare the rows for alpha = 0.2 and N = 9: 0.38 s late with 1.3 cm of noise, against 0.39 s late with 1.2 cm. For the same lag, the two filters leave almost the same noise. The exponential filter is the usual choice on small robots because it needs no list, and the moving average is the usual choice when you want a result that anyone can check by hand.

At alpha = 0.05 the theory says 1.9 s of lag. The filter needs about 5 seconds of driving to build up to it, and then sits 15 cm behind the truth, which is 1.8 s at this speed. The demo's figures for it (1.6 s late, 2.2 cm of "noise left") are thrown out by that slow build-up, which counts as spread. A filter that slow is only useful for a thing that barely changes.

Two details every version of this filter needs:

  • Start it at the first reading, as the demo does with filtered = distance(). Starting at 0 makes the first second of output a climb from 0 up to the real gap, which looks exactly like a real signal and is not one.
  • Run it once per new reading. The sensor gives a fresh reading every 0.1 s. Filtering the same reading ten times in a fast loop makes the filter act as if it had seen ten readings, so it is smoother and later than the alpha you chose says.

Written as filtered = filtered + alpha × (raw - filtered), the update says: move your current belief towards the new reading by a fraction of the difference. A Kalman filter has exactly that shape, except that it works out the fraction every tick from how uncertain it is, instead of using a fixed alpha.

Why it is called low-pass

The signal the robot cares about, the gap closing as it drives, changes over seconds. The noise is a new random number every tenth of a second. A filter that lets slow changes through and blocks fast ones is a low-pass filter: low frequencies pass, high ones are cut. Both the moving average and the exponential filter are low-pass filters.

This works because the signal and the noise are far apart in speed. When the thing you want to see changes as fast as the noise does, no filter can separate them, and the only fixes are a better sensor or more sensors.

Outliers: the median filter

Noise is a reading that is a little wrong. An outlier is a reading that is not measuring what you think it is at all. Distance sensors produce them all the time: the beam misses the object and hits something behind it, or something passes in front.

In this scene another robot shuttles across the mat between the robot and the wall. Each time it crosses the beam, the sensor measures it instead of the wall: two readings in a row, 30 to 35 cm too short. The program keeps the last 5 readings and works out both their mean and their median (sort them and take the middle one), then stops when the median says the wall is 35 cm away.

Median of 5: the other robot crosses the beam three times and the median ignores all three, while the mean is dragged 11.9 cm short. The robot stops 34.3 cm from the wall.
The program
from bugbot import *
connect()

# change these numbers and press Run
N = 5            # how many readings in the window
STOP = 35        # stop this far from the wall, cm

window = []
worst = {"raw": 0, "mean": 0, "median": 0}   # most cm too short
forward(40)
for tick in range(120):
    raw = distance()
    window.append(raw)
    if len(window) > N:
        window.pop(0)
    mean = sum(window) / len(window)
    median = sorted(window)[len(window) // 2]
    truth = 90 - position()[1]  # the gap, from the overhead camera
    plot("raw", raw)
    plot("mean", mean)
    plot("median", median)
    plot("truth", truth)
    for name, value in (("raw", raw), ("mean", mean), ("median", median)):
        worst[name] = max(worst[name], truth - value)
    if median < STOP:
        break
    wait(0.1)
stop()
wait(0.5)
print("stopped with the wall", round(90 - position()[1], 1), "cm away")
for name in worst:
    print(name, "was at worst", round(worst[name], 1), "cm too short")
Put this demo on your own site

Paste it into a school website, Moodle, Google Sites or a blog. More options on the embed page.

The raw line dips sharply three times, at about 1.1 s, 3.5 s and 5.8 s, down to 35.1 cm short of the truth at worst. The mean follows every dip, shallower but wider: at worst it says the wall is 11.9 cm closer than it is. The median does not move for any of them, and is never more than 2.2 cm short. Averaging an outlier does not remove it. It spreads it over the next few estimates.

Change the last if to stop on raw instead and the robot stops 64.8 cm from the wall, fooled by the second crossing. Stopping on the mean happens to get away with it at 35 cm, but set STOP = 40 and it stops at 46.6 cm, fooled by the third crossing, while the median stops at 37.3 cm.

A median of N ignores up to (N - 1) / 2 bad readings in the window. Here the bad readings come in pairs, so a median of 5 is enough and a median of 3 is not: set N = 3 and the median is fooled by the third crossing too, and the robot stops 47.3 cm from the wall. Make the window more than twice as long as the longest burst of bad readings you expect.

The median has costs of its own. It sorts the window every tick, and it lags a changing signal by about as much as a mean of the same size. A common arrangement is a short median first, to throw out the outliers, then an exponential filter on its output to smooth the noise.

The other defence is a gate: ignore any reading that is too far from what you expected, such as the last estimate. Count the readings you throw away. A gate that rejects half of them is not protecting you from bad data; it is telling you that your expectation is wrong.

The price: every filter is late

Every filter on this page delays the signal. There is no clever way round it: to tell a real change from noise, a filter has to wait for more readings, and waiting is delay. On a robot driving at 8.2 cm/s, 0.4 s of lag means the estimate is 3 cm behind where the robot is.

That matters most when the filtered value is used to control the robot, because the controller then acts on old news. This program holds the robot 25 cm from the wall: it filters the distance, and drives forwards or backwards in proportion to the error, stopping when the filtered gap is within 1 cm of 25. Here alpha is 0.03, a very heavy filter.

alpha = 0.03: the filtered gap is so far behind that the robot drives through its 25 cm target to 9.7 cm from the wall before it backs off, and ends 23.8 cm away.
The program
from bugbot import *
connect()

# change this number and press Run
ALPHA = 0.03     # the filter's weight on each new reading

TARGET = 25.0    # cm from the wall
gap = distance()                # start at the first reading
closest = 100
for tick in range(250):         # 25 seconds
    gap = ALPHA * distance() + (1 - ALPHA) * gap
    truth = 90 - position()[1]  # the gap, from the overhead camera
    closest = min(closest, truth)
    plot("filtered gap", gap)
    plot("truth", truth)
    plot("target", TARGET)
    error = gap - TARGET
    if abs(error) < 1.0:
        stop()                  # close enough: sit still
    else:
        drive(max(-45, min(45, 2.2 * error)), 0, 0)
    wait(0.1)
stop()
print("closest", round(closest, 1), "cm from the wall")
print("ended", round(truth, 1), "cm from the wall")
Put this demo on your own site

Paste it into a school website, Moodle, Google Sites or a blog. More options on the embed page.

At alpha = 0.03 the filter is about 0.97 / 0.03 = 32 readings behind, over 3 seconds. When the robot's true gap passes 25 cm, the filtered gap still says 48 cm, so the program is still driving forwards at full speed. The wheels only stop once the filtered gap has fallen to about 32 cm, and by then the robot is 9.7 cm from the wall. It sits there for 3 seconds while the filtered gap catches up and drops far enough below 25 to move the motors backwards, and then it backs off to 23.8 cm.

Change ALPHA and run it again:

alpha Closest to the wall Where it ends
1 (no filter) 26.2 cm 26.2 cm
0.3 28.3 cm 28.3 cm
0.1 26.7 cm 26.7 cm
0.07 23.5 cm 23.5 cm
0.05 19.1 cm 19.1 cm
0.03 9.7 cm 23.8 cm

Down to alpha = 0.1 the robot never goes past its target. From 0.07 down it overshoots, by more each time. At 0.05 it stops 5.9 cm too close and stays there: by the time the filter has caught up, the error asks for a push too small to move the motors. The top rows, which stop 1.2 to 3.3 cm short of the target, have the same cause: the motors' own dead band, which the PID controller guide explains.

Without a filter at all, this robot copes here. A typical reading is 3 cm out, and 3 cm times a gain of 2.2 asks for under 7 percent of power, while this robot needs about 15 percent before its wheels turn. Only the odd large error in a reading gets through, and it nudges the robot a short way forwards. With a higher gain or a noisier sensor, much more of the noise would go straight into the motors, and that is when a light filter earns its place.

The trap is a familiar one. A controller twitches, so the sensor gets filtered; the controller is smoother, so the filter is made heavier; now the delay is large and the controller overshoots or swings for a completely different reason. Two causes, the same symptom, opposite fixes. The rule of thumb: keep the filter's delay small compared with how quickly the thing you are controlling responds.

Ways to pay less for filtering:

  • Filter as little as you can get away with, not as much as makes the chart look nice.
  • Filter the measurement, and work out the error from the filtered measurement. Filtering the error itself delays the robot's response to the target changing as well.
  • Filter only the part that needs it. In a PID controller the D term is the one that amplifies noise, so it is often the only term that gets filtered.
  • Predict. A filter that also tracks how fast the value is changing can look ahead by its own delay. That is what a Kalman filter adds.

How to choose a filter

  1. Stand the robot still and measure the noise: take a hundred readings and work out sigma.
  2. Decide how much noise the thing that uses the number can live with. Averaging N readings divides sigma by √N, so to go from 3 cm to 1 cm you need about 9 readings.
  3. Use a moving average of that size, or an exponential filter with alpha = 2 / (N + 1): here 0.2.
  4. Work out the delay, (N - 1) / 2 readings, and multiply by how fast things change. If the robot moves 3 cm in that time and that is too much, the answer is not a better filter. It is a better sensor, a slower robot, or a filter that predicts.
  5. If the readings have outliers, put a short median in front, more than twice as long as the longest run of bad readings.
  6. Test it while the robot does the job. A filter tuned on a robot standing still always prefers the heaviest filter, because nothing is changing for it to be late about.

Questions

What is a moving average filter?

It replaces each reading with the mean of the last N readings. Random noise partly cancels out in the mean, so the output is smoother than the input, and the more readings in the window, the smoother it is. The price is lag: the output describes the middle of the window, (N - 1) / 2 readings ago.

What is a low-pass filter?

A filter that lets slow changes through and blocks fast ones. On a sensor, the thing you want to measure usually changes slowly and the noise changes quickly, so a low-pass filter removes most of the noise and keeps most of the signal. The moving average and the exponential filter on this page are both low-pass filters, and so is a resistor and capacitor in a circuit.

What is the difference between a moving average and an exponential moving average?

A moving average gives the last N readings equal weight and ignores everything older. An exponential moving average gives the newest reading weight alpha and lets every older reading fade away geometrically, so it needs only one stored number instead of a list of N. With alpha = 2 / (N + 1) the two have the same lag and leave almost the same noise: on this page, alpha = 0.2 and N = 9 were both about 0.4 s late.

What value of alpha should I use?

Start around 0.2, which behaves like an average of the last 9 readings and lags by about 4 readings. Use a bigger alpha (up to about 0.5) when the value changes quickly or feeds a controller, and a smaller one only when the value hardly changes. The lag is about (1 - alpha) / alpha readings, so at 10 readings a second, alpha = 0.05 is nearly 2 seconds late.

How do I choose the window size for a moving average?

Work out how much noise you can accept. Averaging N readings divides the noise by the square root of N, so halving it takes 4 readings and dividing it by 3 takes 9. Then check the lag, (N - 1) / 2 readings, against how fast the value changes. If the lag is too big for the noise you need to remove, no window size will do, and you need a better sensor or a filter that predicts.

Why does my filtered signal lag behind?

Every filter that smooths also delays. To tell a real change from noise it has to wait for more readings, so its output describes the recent past rather than now. On a steadily changing signal the lag is about (N - 1) / 2 readings for a moving average and (1 - alpha) / alpha readings for an exponential filter. Less smoothing is the only way to have less lag, unless the filter also predicts.

When should I use a median filter instead of a moving average?

When the problem is outliers rather than noise: occasional readings that are completely wrong, such as a distance sensor seeing something that passes in front. A mean is dragged by every bad reading; a median of N ignores up to (N - 1) / 2 of them. On this page, a single passing robot pulled a mean of 5 readings 11.9 cm short, and a median of 5 no more than 2.2 cm. Many programs run a short median first and then smooth its output.

How do you write a moving average in Python?

Keep a list. Each time a reading arrives, append it, remove the first item with pop(0) if the list is longer than N, and take sum(window) / len(window). For an exponential filter you need no list: set filtered to the first reading, then each time do filtered = alpha * raw + (1 - alpha) * filtered. Both are in the demos on this page.

How do you smooth noisy sensor readings on a robot?

Read the sensor at the rate it produces new readings, and pass each one through a filter: an exponential filter with an alpha around 0.2 is a good start, with a short median in front if the sensor gives occasional wild readings. Plot the raw and filtered values together while the robot is doing its real job, and check that the filtered line is not so late that the robot reacts to where things were rather than where they are.

Is a moving average filter on the GCSE or A level specification?

Not as a filter in GCSE or A level Computer Science. The pieces are there in maths: the mean and median are GCSE Maths, moving averages of time series are in GCSE Statistics, and standard deviation and the normal distribution are in A level Maths. Writing a filter for a sensor makes a good A level Computer Science programming project, because the results can be measured and compared.

Learn it step by step

These lessons build the same ideas one at a time, each with tasks the simulator marks.

  1. U4.1 A reading is a distribution Noise and filtering, University
  2. U4.2 Averaging Noise and filtering, University
  3. U4.3 The low pass filter Noise and filtering, University
  4. U4.4 Outliers Noise and filtering, University
  5. U4.5 The price of filtering Noise and filtering, University
  6. U4.6 Choosing the filter Noise and filtering, University
  7. U4.7 Project: hold the gap Noise and filtering, University
Open the lessons