Where the Numbers Settle

An easy Python interview practice problem on DataDriven. Write the Python and run it against test cases, with instant feedback.

Domain
Python
Difficulty
easy
Seniority
L5
Asked in
Meta

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}.

The answer moves with the data.

Example 1

Input:

operations = [["add","sensor_a",10],["add","sensor_b",20],["add","sensor_a",30],["get_averages"]]

Output:

[null,null,null,{"sensor_a":20,"sensor_b":20}]

Example 2

Input:

operations = [["get_averages"]]

Output:

[{}]

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
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

Common follow-up questions