A dashboard plots the average of the last 1,000 sensor readings at every point. The code sums each window from scratch:
import time
def moving_average(xs: list[float], window: int) -> list[float]:
return [sum(xs[i : i + window]) / window for i in range(len(xs) - window + 1)]
readings = [float(i % 100) for i in range(100_000)]
for window in (10, 100, 1_000):
start = time.perf_counter()
moving_average(readings, window)
print(f"window {window:>5}: {time.perf_counter() - start:.3f}s")
On a laptop with Python 3.15, it printed:
window 10: 0.009s
window 100: 0.031s
window 1000: 0.231s
The wider the window, the slower it gets, because each average adds up
window values again. Most of them were in the previous window too. You
might reach for NumPy at this point. Change the algorithm first.
Reuse the sums you already have
A prefix sum list holds the running total of the input, starting from
zero. For xs = [3, 1, 4, 1, 5], the prefix sums are
[0, 3, 4, 8, 9, 14]. Any window's sum is then one subtraction. The sum of
xs[1:4], which is 1 + 4 + 1, is prefix[4] - prefix[1], or 9 - 3 = 6.
You build the prefix list once, and each average costs one subtraction and one division, whatever the window size. A prefix-sum version of the code above took about 4 milliseconds at all three sizes. You write it in the exercise.
When NumPy helps, and when it doesn't
A NumPy array stores its numbers side by side in one block of memory, and its operations loop over them in compiled code. A list stores references to separate float objects, and a Python loop handles them one at a time. So work on a whole array is fast, but only once the data is in an array:
import time
import numpy as np
readings = [float(i % 100) for i in range(1_000_000)]
start = time.perf_counter()
total = sum(readings)
print(f"sum of the list: {time.perf_counter() - start:.4f}s")
start = time.perf_counter()
array = np.asarray(readings)
print(f"list to array: {time.perf_counter() - start:.4f}s")
start = time.perf_counter()
array_total = float(array.sum())
print(f"sum of the array: {time.perf_counter() - start:.4f}s")
assert total == array_total
With NumPy 2.5 on Python 3.14, it printed:
sum of the list: 0.0018s
list to array: 0.0135s
sum of the array: 0.0002s
Summing the array is about ten times faster than summing the list. Converting the list costs about seven times more than summing it, though. If the data arrives as a list and you need one result, stay with the list. If you keep the array and run many operations on it, NumPy wins.
A Python for loop over an array is slower than one over a list. Each item
the loop reads becomes a new Python object. On the same laptop, a for
loop that adds up the array took between two and four times as long as the
same loop over the list. Use NumPy's whole-array operations, or don't use
NumPy.
Prefix sums can lose small values
A float holds about 16 significant digits. When the running total gets large, small values added to it can round away:
from itertools import accumulate
values = [1e16, 1.0, 1.0]
prefix = [0.0, *accumulate(values)]
recovered = [prefix[i + 1] - prefix[i] for i in range(len(values))]
print(recovered) # [1e+16, 0.0, 0.0]
1e16 + 1.0 rounds back to 1e16, so both 1.0 values are lost before
the subtraction. Readings of a similar size, like those in the exercise,
lose almost nothing. If your values range over many orders of magnitude,
test against math.fsum on each window, which avoids this loss.
Check your understanding You get 50 sensor readings, then later 50,000. Should you switch to NumPy? Show answer Hide answer
Not because of the count alone. First check the algorithm. If each result repeats work, as the direct moving average does, prefix sums fix it in plain Python. Then measure at both sizes, including the conversion to an array. If the rest of your program already uses arrays, NumPy becomes the better choice, because you pay for the conversion once.
Going further
The standard library's array module stores numbers compactly without
NumPy, but it has no whole-array arithmetic. For typed NumPy code, see
NumPy's typing reference.