Smooth irregular updates without changing the response every time the rate changes

KEVIN LOGGENBERG · BLOG

Smooth irregular updates without changing the response every time the rate changes

2026-10-29 · 4 MIN READ

A basic moving average is easy to add to a noisy value. The awkward part appears when updates stop arriving at a steady rate.

← All articles
4 min read
On this page

A basic moving average is easy to add to a noisy value. The awkward part appears when updates stop arriving at a steady rate.

A smoothing factor that feels responsive at twenty updates per second can feel sluggish at two. The code is unchanged, but the amount of smoothing per second is different.

I prefer to describe the filter in seconds, then calculate the per-update factor from elapsed time.

A synthetic stepped signal is smoothed with a fixed factor and a time-aware factor; sparse updates make the fixed-factor result respond more slowly.

The chart uses a generated signal and an intentional change in update rate. It is not recorded application data.

Give the filter a time constant

A first-order exponential filter can be written as:

output = output + alpha * (input - output)
alpha = 1 - exp(-elapsed_seconds / time_constant)

The time constant describes the response to a held input step. After one time constant, the output has travelled about 63.2 percent of the distance to the new value. After three, it has travelled about 95 percent.

For a 0.5-second time constant and a 0.1-second interval:

alpha = 1 - exp(-0.1 / 0.5)
      ≈ 0.1813

For a 0.5-second interval, alpha becomes approximately 0.6321. The larger factor accounts for the longer time between updates.

Use a numerically careful calculation

For very small intervals, subtracting a value close to one from one can lose floating-point precision. Python's expm1(x) calculates exp(x) - 1 more accurately near zero.

from math import expm1, isfinite

def smooth(previous, current, elapsed_seconds, tau=0.5):
    if not all(isfinite(v) for v in
               [previous, current, elapsed_seconds, tau]):
        raise ValueError('Values must be finite')
    if tau <= 0 or elapsed_seconds < 0:
        raise ValueError('Invalid timing')
    alpha = -expm1(-elapsed_seconds / tau)
    return previous + alpha * (current - previous)

print(round(smooth(0, 10, 0.1), 3))  # 1.813
print(round(smooth(0, 10, 0.5), 3))  # 6.321

This update treats the supplied input as representing the interval. It does not reconstruct values that were never observed. If an input changes halfway through a long gap, knowing only the value at the end cannot tell the filter when that change occurred.

Initialise from a real value

Starting the output at zero can create a visible ramp that has nothing to do with the measured quantity. Unless zero has a deliberate meaning, initialise from the first valid input.

Reject or explicitly handle missing and non-finite values. Allowing a NaN into the recursive state can poison every later result even after the incoming data becomes valid again.

Use elapsed time from a monotonic source. Wall-clock corrections should not make an interval negative or unexpectedly huge.

Decide what a long gap means

After a long interruption, the calculated factor approaches one. The next observation can therefore move the output almost directly to the current input. That may be exactly what you want for a display that has been stale for several seconds.

Another application may need to mark the old value invalid, reset the filter or ask for several new observations. Clamping elapsed time is possible, but it changes the stated time response. Make that decision explicit rather than hiding it in an unexplained constant.

Smoothing also adds delay. Keep the raw data where a sudden change matters for analysis, and avoid treating a smoothed display value as a better measurement merely because it looks calm.

Compare equal amounts of elapsed time

A useful test applies the same held step using many short intervals and a few long intervals. With this update equation, both should reach the same result after the same total time, apart from numerical rounding.

Then test a changing signal with missing observations. The results may differ because the available information differs. That is a sampling limitation, not something a smoother can promise to repair.

Reference: Python's exponential math functions. The filter equation and its held-input assumption are stated above.

… views… comments

THE CONVERSATION

What do you think?

A question, a different approach, or something worth adding.

Sign in to comment or likeCreate an account

Loading the conversation…

KEEP EXPLORING

More from the workbench

START A CONVERSATION

Have a problem worth solving?

Let’s talk about your idea, your team, or the work you’ve seen here.

Get in touch
Kevin Loggenberg

A COLLECTION OF WORK & EXPERIENCE

Expanded project screenshot