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
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 resultsCommon 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.)