# Where the Numbers Settle

> The answer moves with the data.

Canonical URL: <https://datadriven.io/problems/where_the_numbers_settle>

Domain: Python · Difficulty: easy · Seniority: L5

## Problem

Sensor readings arrive one at a time, and a dashboard has to show the running mean for each sensor at any moment. Implement a `StreamAverager` class: `add(key, value)` records one numeric reading under `key`, and `get_averages()` returns a dict mapping each key seen so far to the mean of its readings, or an empty dict before anything is recorded. A driver `run_stream_averager(operations)` is provided: `operations` is a list where each entry is either `['add', key, value]` or `['get_averages']`. Build one `StreamAverager`, apply the operations in order, and return a parallel list of results, with `None` for each `'add'` and the dict for each `'get_averages'`. Means are floats, so `add('sensor_a', 10)`, `add('sensor_a', 30)`, `get_averages()` gives `{'sensor_a': 20.0}`.

## Worked solution and explanation

### What this really is

Under the sensor-reading costume, this is per-key aggregation state that has to survive a whole stream of operations. By the time `get_averages()` runs, the readings it needs have already streamed past, so the object has to carry them forward: nothing about the current call can reconstruct them. And a mean is two facts, not one, a total and a count kept together per key, so a single value per sensor can never answer it. The trap that actually bites is the division. Reach for `//` and `1.5` collapses to `1`, so a clean-looking mean silently drops its fraction; pool every sensor into one running figure and you report a blended number instead of one average per key. Either way `get_averages()` hands back numbers that look fine and are wrong. (There is a seniority tell hiding here too: keep a running sum and count and each read is one division, but buffer the raw values in a list per key and every `get_averages()` re-sums the whole history.)

---

### Break it down

#### Step 1: Carry a sum and a count per key

Two dicts are enough: one maps each key to the total of its readings, the other to how many readings it has seen. You never store the raw values, so memory grows with the number of distinct keys, not with the length of the stream.

#### Step 2: Start empty without a constructor

Give the class `sums = None` and `counts = None` as class-level defaults, then replace them with real dicts on the instance the first time `add` runs. A fresh averager reads the `None` defaults so it starts empty, and a `get_averages()` before any `add` can short-circuit straight to `{}`.

#### Step 3: Fold each reading in

`add` folds the new reading in: `self.sums[key] = self.sums.get(key, 0) + value`, and the same one-line pattern bumps the count. `dict.get` with a default of `0` handles the first sighting of a key without a separate branch.

#### Step 4: Divide only when asked

`get_averages` walks the keys and returns `self.sums[key] / self.counts[key]`. Python's `/` is float division, so `40 / 2` is `20.0` and the fraction always survives. With no keys the loop never runs and you return `{}`.

#### Step 5: Replay the stream

The driver `run_stream_averager(operations)` builds one averager and walks the ops in order, appending the return of `add` (which is `None`) for an `['add', key, value]` and the dict from `get_averages()` for a `['get_averages']`, producing exactly one result per operation.

---

### The solution

**Running sum and count, divided lazily at read time**

```python
class StreamAverager:
    sums = None
    counts = None

    def add(self, key, value):
        if self.sums is None:
            self.sums = {}
            self.counts = {}
        self.sums[key] = self.sums.get(key, 0) + value
        self.counts[key] = self.counts.get(key, 0) + 1

    def get_averages(self):
        if self.sums is None:
            return {}
        averages = {}
        for key in self.sums:
            averages[key] = self.sums[key] / self.counts[key]
        return averages


def run_stream_averager(operations):
    averager = StreamAverager()
    results = []
    for op in operations:
        if op[0] == "add":
            results.append(averager.add(op[1], op[2]))
        else:
            results.append(averager.get_averages())
    return results
```

> **Why the sentinel default is safe**
>
> The `None` class-level defaults are immutable, so they are never shared mutable state: the first `add` swaps in dicts that live on the instance. That is the constructor-free way to make every fresh averager start from empty.

> **Time and space**
>
> **Time:** `add` is O(1). `get_averages` is O(k) where k is the number of distinct keys.
> 
> **Space:** O(k), one sum and one count per key. Nothing scales with the length of the stream.

> **Interviewers watch for**
>
> The tell is what you decided to keep. A candidate who stores just a sum and a count per key has understood that the mean is derivable and the raw values are dead weight. Reaching straight for that, rather than a list of every reading, is the seniority signal here.

> **Common pitfall**
>
> Storing all values in a list per key and recomputing the mean on every call. That is O(n) time per `get_averages` in the total number of readings and O(n) space, when a running sum and count give you O(k) time and space instead.

---

## Common follow-up questions

- How would you add a remove(key, value) method? _(Tests subtracting from sum and decrementing count, handling the zero-count edge case.)_
- What if you needed a windowed average over the last N values per key? _(Tests using a deque per key with a fixed max length.)_
- How would you make this thread-safe for concurrent writers? _(Tests knowledge of locks or concurrent data structures for multi-threaded access.)_

## Related

- [All practice problems](https://datadriven.io/problems)
- [Mock interview mode](https://datadriven.io/interview/where_the_numbers_settle)
- [Python Interview Questions](https://datadriven.io/python-interview-questions)
- [Data Engineering Interview Prep Guide](https://datadriven.io/data-engineer-interview-prep)
- [Daily Challenge](https://datadriven.io/daily)

---

Source: DataDriven (https://datadriven.io). DataDriven is the data engineering interview community. Live code execution in SQL, Python, and Spark sandboxes. Every feature is open to every member.