QuiddityML

By · 22 October 2026 · 4 min read

Exponential moving average (EMA) explained

How the EMA update works, why it starts too low and how bias correction fixes that, and how to read a beta like 0.9 or 0.999 as a window.

An exponential moving average (EMA) is a running average that gives the newest value the most weight and lets older values fade out gradually, without storing the history. It is a common way to smooth a noisy stream of numbers, like sensor readings or daily temperatures, so the overall level shows through the jumps.

What problem does an EMA solve?

Sensor readings, daily temperatures and website traffic all jump around from one reading to the next. A smoothed estimate that follows the overall level, nudged a little with every new reading, is often more useful than any single noisy value.

A plain average would treat every value equally and needs either the full list or a count of how many values came before. An EMA keeps one number and updates it in place, weighting recent values more than old ones.

The EMA update rule

Call the running average after step $t$ by the name $v_t$, and the newest observed value $x_t$. A number $\beta$ between 0 and 1, usually close to 1, sets how much of the old average is kept. The update is

$$v_t = \beta v_{t-1} + (1 - \beta) x_t$$

The new average is a blend: a $\beta$ share of the old average plus a $(1 - \beta)$ share of the newest value. A high $\beta$ like 0.99 moves slowly and smooths out noise. A low $\beta$ like 0.5 reacts quickly and smooths less.

A noisy stream of observations over 50 steps with the EMA drawn as a smooth line through them, under the update rule v_t = beta v_(t-1) + (1 - beta) x_t with 0 < beta < 1.

A worked example

Take $\beta = 0.9$, start from $v_0 = 0$, and feed in the value 10 three times:

$$v_1 = 0.9 \times 0 + 0.1 \times 10 = 1.0$$

$$v_2 = 0.9 \times 1.0 + 0.1 \times 10 = 1.9$$

$$v_3 = 0.9 \times 1.9 + 0.1 \times 10 = 2.71$$

The average climbs toward 10, slowly. In code the update is one line inside a loop:

1def ema(values, beta):
2    v, out = 0.0, []
3    for x in values:
4        v = beta * v + (1 - beta) * x
5        out.append(round(v, 4))
6    return out
7 
8print(ema([10, 10, 10], beta=0.9))
9# [1.0, 1.9, 2.71]

The running average v climbing from v_0 = 0 to v_1 = 1.0, v_2 = 1.9 and v_3 = 2.71 while every observation equals 10.

Unrolling the recurrence

Each $v_t$ is built from $v_{t-1}$, which is built from $v_{t-2}$, and so on. Substituting each one into the next, all the way down to $v_0 = 0$, turns the update into a weighted sum over every value seen so far:

$$v_t = (1 - \beta) \sum_{k=0}^{t-1} \beta^k x_{t-k}$$

Here $k$ counts how many steps back a value is, so $x_{t-k}$ is the value from $k$ steps ago and its weight is $(1 - \beta)\beta^k$. With $\beta = 0.9$ the newest value gets weight 0.1 and the one before it gets 0.09. After that come 0.081 and 0.0729, each weight 0.9 times the one before.

Bars for the EMA weights with beta = 0.9: 0.1 on the newest value x_t, then 0.09, 0.081, 0.0729 and down to 0.0387 on x_(t-9), next to the unrolled sum.

Every past value is still in the average. Its weight shrinks by the same factor at each step back, which is where the word "exponential" comes from.

Bias correction

An EMA that starts at $v_0 = 0$ begins too low. After three observations of 10, the worked example still reads 2.71, which is a poor estimate of a stream that has been 10 the whole time. The weights explain why: after $t$ steps they add up to $1 - \beta^t$ instead of 1, and the missing share still belongs to the starting value of 0.

Bias correction divides that missing share back out. The corrected estimate, written $\hat v_t$, is

$$\hat v_t = \frac{v_t}{1 - \beta^t}$$

At $t = 1$ with $\beta = 0.9$ the divisor is $1 - 0.9 = 0.1$, so $\hat v_1 = 1.0 / 0.1 = 10$. At $t = 2$ it is $1 - 0.81 = 0.19$, and $1.9 / 0.19 = 10$ again. As $t$ grows, $\beta^t$ shrinks toward 0 and the divisor approaches 1, so the correction fades out once the EMA has seen enough values.

1def ema_corrected(values, beta):
2    v, out = 0.0, []
3    for t, x in enumerate(values, start=1):
4        v = beta * v + (1 - beta) * x
5        out.append(round(v / (1 - beta ** t), 4))
6    return out
7 
8print(ema_corrected([10, 10, 10], beta=0.9))
9print(ema_corrected([10, 12, 8, 30, 10, 10], beta=0.9))
10# [10.0, 10.0, 10.0]
11# [10.0, 11.0526, 9.9262, 15.7633, 14.3559, 13.4263]

The second stream has a spike to 30 at step 4. The corrected average moves up to about 15.8 and then eases back down, since the spike keeps a shrinking share of the weight.

The raw EMA 10(1 - 0.9^t) rising from 1.0 toward 10 over 30 steps while the corrected estimate sits at 10 from the first step, with a table showing 1.0 / 0.1, 1.9 / 0.19 and 2.71 / 0.271 all equal to 10.

Reading beta as a window

A quick way to read a $\beta$ is as a window: an EMA averages over roughly the last $1 / (1 - \beta)$ values. So $\beta = 0.9$ covers about the last 10 values, $\beta = 0.99$ about the last 100, and $\beta = 0.999$ about the last 1000.

The window has no hard edge. At the window's end, the weight on a value is still about 35% to 37% of the weight on the newest value, and older values keep a smaller share after that.

The EMA weights relative to the newest value for beta = 0.9, 0.99 and 0.999, each drawn over three windows, with the weight at step 10, 100 and 1000 marked at 0.35, 0.37 and 0.37.

Common mistakes

Expected value and variance covers the plain average an EMA is compared against. The Adam optimizer and machine learning optimizers show where running averages like this one appear in training.

QuiddityML teaches the exponential moving average as a concept in the Math track's numerical computation unit. The exercises on it include turning the update rule into an ema_step function, picking the line that bias-corrects an EMA, and spotting the bug in an EMA loop.