# The Narrow Lens

> A narrow timeframe. Everything inside matters.

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

Domain: Python · Difficulty: medium · Seniority: L5 · Asked in: Sigmoid

## Problem

Raw sensor `values` arrive in order and are smoothed before alerting by replacing each reading with the average of that reading and the ones just before it, at most `window_size` readings in all. A `window_size` below 1 means smoothing is switched off for the stream, and the result is an empty list.

## Example 1

Input:

```
window_size = 3, values = [10,20,30,40,50]
```

Output:

```
[10,15,20,30,40]
```

## Example 2

Input:

```
window_size = 2, values = [5,15,25]
```

Output:

```
[5,10,20]
```

## Worked solution and explanation

### Why this problem exists in real interviews

This is a trailing-average smoother wearing a sensor-stream costume, and the whole problem lives in one decision: what do you divide by? Everyone gets the full-window region right. What separates candidates is the warm-up region, the positions before the window has filled, where the correct divisor is how many readings you actually have, not the window size you were handed. Get that wrong and every early output comes back deflated, which on a real alerting pipeline means the first few minutes of every stream silently read low.

---

### Break down the requirements

#### Step 1: Read the contract: stateless function, parallel output

The signature is `the_narrow_lens(window_size, values)` and it returns a list the same length as `values`. There is no class and no state carried across calls. One list in, one fully materialized list out, one trailing average per input position.

#### Step 2: Handle the warm-up region

At index i the divisor is the count of readings actually in the window, which is min(i + 1, `window_size`), not `window_size`. The positions before the window fills are warm-up and average every reading seen so far. Dividing by the full window size early returns deflated averages and fails the first visible test at index 0.

#### Step 3: Branch on warm-up versus full window

Split the positions in two. While `i < window_size` the window is the warm-up prefix `values[:i + 1]`; after that it is `values[i - window_size + 1:i + 1]`, inclusive of position i and exactly `window_size` long. Branching this way also means the start index can never go negative, so Python's wrap-around slicing never pulls readings from the end of the list. Slicing to i instead of i + 1 shifts every average back one slot.

#### Step 4: Guard the window that holds nothing

A `window_size` below 1 means smoothing is off, and the statement asks for an empty list. Without the guard, a zero-size window slices out nothing and `sum(window) / len(window)` divides by zero. One early `if window_size < 1 or not values: return []` settles it before the main path starts.

---

### The solution

**Stateless trailing average over the input list**

```python
def the_narrow_lens(window_size, values):
    # A window that holds no readings has no average to report.
    if window_size < 1 or not values:
        return []

    averages = []
    for i in range(len(values)):
        if i < window_size:
            # Warm-up: the window is everything seen so far.
            window = values[:i + 1]
        else:
            window = values[i - window_size + 1:i + 1]
        averages.append(sum(window) / len(window))
    return averages
```

> **Cost Analysis**
>
> Time: O(n * w) where n is len(values) and w is `window_size`, because each position re-sums up to w elements. For n up to 100K and w up to n, the worst case is quadratic. An incremental running sum (add `values[i]`, subtract the reading that just left the window once it is full) drops this to O(n) and matters when n is large or `the_narrow_lens` runs inside a tight loop. Space is O(n) for the output plus O(w) for the slice.

> **Interviewers Watch For**
>
> Whether you state the warm-up rule out loud before writing code, whether you pick the right divisor (the length of the actual window, not `window_size`), and whether you can produce the O(n) running-sum variant on follow-up. Strong candidates also note that the result is a list of floats, not ints.

> **Common Pitfall**
>
> Dividing by `window_size` for every position. The visible test with `values=[10, 20, 30, 40, 50]` and `window_size=3` expects `[10.0, 15.0, 20.0, ...]`, but dividing by 3 at index 0 returns 3.33 and the entire result drifts low. The other classic miss is `values[start:i]` (exclusive of i) instead of `values[start:i + 1]`, which shifts every average back one slot.

---

## Common follow-up questions

- Rewrite this so each output element costs O(1) instead of O(`window_size`). What invariant do you maintain across iterations? _(Tests the running-sum optimization. Maintain `running = running + values[i] - values[i - window_size]` once `i >= window_size`. Each step is O(1), and the full pass becomes O(n).)_
- How does the algorithm change if the harness asked for the moving median instead of the moving average? _(Tests the two-heap or sorted-container pattern. A median needs O(log w) per step using a max-heap of the lower half and a min-heap of the upper half, with rebalancing on each slide.)_
- If `values` were 10M floats from a sensor stream, what breaks first and how do you defend against it? _(Tests numerical stability. With long streams of large floats, the running sum drifts due to floating-point error. Periodic full recomputation, Kahan summation, or chunked partial sums all bound the drift.)_

## Related

- [All practice problems](https://datadriven.io/problems)
- [Mock interview mode](https://datadriven.io/interview/the_narrow_lens)
- [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.