Streaming moving average
Latency dashboards, sensor feeds and training-loss curves all show a moving average. The values arrive one at a time and the stream may never end, so you can’t collect them first.
Write a generator moving_average(stream, window):
- For each value from
stream, yield the mean of the lastwindowvalues seen so far, including this one. - Before
windowvalues have arrived, average whatever you have. - Be lazy. It must work on an infinite stream.
- Each step should be
O(1), notO(window). windowis at least1.
>>> list(moving_average([2, 4, 6, 8], 2))
[2.0, 3.0, 5.0, 7.0]
Hints get more specific as you go. Reveal one, try again, then come back.
Re-summing the window on every step is O(window) per value. Instead, keep a running total.
When a new value arrives, add it to the total. When the window is too full, subtract the oldest value and drop it.
collections.deque gives you append on the right and popleft on the left, both O(1). Yield
total / len(window_deque).
Try one more hint first? The solution is always here, but working it out is what makes it stick.
Approach
Keep the last window values in a deque and their sum in a variable. Each new value adds to the sum.
Once the window is over-full, the oldest value leaves and is subtracted.
from collections import deque
def moving_average(stream, window):
buf = deque()
total = 0
for x in stream:
buf.append(x)
total += x
if len(buf) > window:
total -= buf.popleft()
yield total / len(buf)
deque(maxlen=window) drops the oldest item for you, but it doesn’t tell you what it dropped. You
still need that value to subtract, so read buf[0] before appending if you go that way.
Complexity
O(1) time per value and O(window) memory, however long the stream runs.
Why this matters
This is the shape of every streaming aggregate: keep a small summary, update it with each event, and
emit a result without looking back over history. Window functions in SQL
(AVG(x) OVER (ROWS 99 PRECEDING)) and stream processors like Flink use the same trick. One caveat
with floats: adding and subtracting for millions of steps lets rounding error creep in. Long-running
systems recompute the sum from the buffer now and then, or use integers or math.fsum.
Per-problem discussion threads are coming. Until then, questions and better solutions are welcome on LinkedIn or by email at hello@builtfrombits.com.
Press ⌘ ↵ or Run to check the visible tests. Submit runs the hidden ones too. Everything runs in your browser.