Drill — Solutions Hidden by Default

Coding Problems

Drillable problems for full-stack DS loops — heavy on feature engineering, evaluation, and production patterns; lighter on classical LeetCode. Multiple approaches per problem.

🎯 10 problems ⏱ ~20 min each 📊 Progress saved
0 / 10 practiced

A · Feature engineering

P1. Compute entity-velocity features

Given events(user_id, ssn, device_id, applied_at), for each row compute: distinct SSNs touched by this device in the trailing 24h; distinct devices touched by this SSN in the trailing 24h.

Show solution
velocity_features.py
import pandas as pd

def velocity_features(events: pd.DataFrame) -> pd.DataFrame:
    events = events.sort_values('applied_at').reset_index(drop=True)
    results = events.copy()
    results['distinct_ssns_per_device_24h'] = 0
    results['distinct_devices_per_ssn_24h'] = 0

    # For each row, find rows within 24h trailing per entity
    for i, row in events.iterrows():
        window_start = row['applied_at'] - pd.Timedelta('24h')
        in_window = events[
            (events['applied_at'] >= window_start) &
            (events['applied_at'] <= row['applied_at'])
        ]
        same_device = in_window[in_window['device_id'] == row['device_id']]
        same_ssn = in_window[in_window['ssn'] == row['ssn']]
        results.at[i, 'distinct_ssns_per_device_24h'] = same_device['ssn'].nunique()
        results.at[i, 'distinct_devices_per_ssn_24h'] = same_ssn['device_id'].nunique()
    return results

Production discussion: the naive solution is O(n²). At scale, push to SQL (with windowed self-joins or partitioned analytics) or to a precomputed feature store. Leakage: the window must be strictly < current event for serving correctness — here we include current row, which is fine for training but would mean "looking at self" in production.

P2. Time-since-last-event

Given events(user_id, event_at), compute time_since_last_event_minutes for each row (NULL for first event per user).

Show solution
time_since_last.py
import pandas as pd

def time_since_last(df: pd.DataFrame) -> pd.DataFrame:
    df = df.sort_values(['user_id', 'event_at']).reset_index(drop=True)
    prev = df.groupby('user_id')['event_at'].shift(1)
    df['time_since_last_minutes'] = (df['event_at'] - prev).dt.total_seconds() / 60
    return df

One line if you're fluent with groupby + shift. The interviewer will probably ask to extend: rolling window stats (events in last hour), or vectorize without pandas for streaming.

P3. Detect stuck-sensor runs

Given a series of sensor readings (timestamp, value), flag any run of 10+ consecutive samples with identical value.

Show solution
stuck_detection.py
import pandas as pd

def flag_stuck(series: pd.Series, run_threshold: int = 10) -> pd.Series:
    # A new run starts wherever the value differs from the previous sample
    run_id = (series != series.shift()).cumsum()
    # Length of the run each sample belongs to; flag every sample in long runs
    return series.groupby(run_id).transform('size') >= run_threshold

Trap: counting diff() == 0 counts repeats, so a run of 10 identical values has only 9 zero differences and a threshold of 10 misses it. Measuring each run's length directly avoids the off-by-one and flags the whole run, leading samples included.

B · Evaluation

P4. Lift at top K%

Given scores and labels (0/1), compute the lift at the top 5%: ratio of positive rate among top 5% scored to overall positive rate.

Show solution
lift_at_k.py
import numpy as np

def lift_at_k(scores: np.ndarray, labels: np.ndarray, k_pct: float = 0.05) -> float:
    n = len(scores)
    k = int(np.ceil(n * k_pct))
    # Indices of top k by score (descending)
    top_idx = np.argpartition(-scores, k - 1)[:k]
    top_rate = labels[top_idx].mean()
    base_rate = labels.mean()
    return top_rate / base_rate if base_rate > 0 else float('inf')

argpartition is O(n) for picking top k; argsort would be O(n log n) — both work, but argpartition matters at scale.

P5. Pick threshold for fixed precision

Given scores and labels, find the lowest threshold such that precision ≥ 0.8 among flagged.

Show solution
fixed_precision_threshold.py
import numpy as np

def threshold_for_precision(scores, labels, target_precision=0.8):
    # Sort descending by score
    order = np.argsort(-scores)
    sorted_labels = labels[order]
    sorted_scores = scores[order]
    # Cumulative TP at each cutoff
    cum_tp = np.cumsum(sorted_labels)
    n_flagged = np.arange(1, len(scores) + 1)
    precision = cum_tp / n_flagged
    # Find the largest n_flagged where precision >= target (lowest threshold)
    valid = precision >= target_precision
    if not valid.any():
        return None  # impossible
    last_valid_idx = np.where(valid)[0].max()
    return float(sorted_scores[last_valid_idx])

Edge case: when no threshold achieves target precision, return None. Trap: with ties in scores, the threshold isn't unique — pick the lower one and document.

P6. PSI between two distributions

Compute the Population Stability Index between a baseline distribution and a current distribution of a feature.

Show solution
psi.py
import numpy as np

def psi(baseline: np.ndarray, current: np.ndarray, n_bins: int = 10) -> float:
    # Use baseline's quantiles to define bin edges
    edges = np.quantile(baseline, np.linspace(0, 1, n_bins + 1))
    edges[0], edges[-1] = -np.inf, np.inf
    base_counts, _ = np.histogram(baseline, bins=edges)
    curr_counts, _ = np.histogram(current,  bins=edges)
    # Avoid log(0)
    base_pct = (base_counts + 1) / (base_counts.sum() + n_bins)
    curr_pct = (curr_counts + 1) / (curr_counts.sum() + n_bins)
    return float(np.sum((curr_pct - base_pct) * np.log(curr_pct / base_pct)))

Common thresholds: PSI < 0.1 = no shift; 0.1–0.25 = moderate; > 0.25 = significant shift requiring attention. The +1 Laplace smoothing prevents the log(0) trap on sparse bins.

C · Production patterns

P7. LRU feature cache with TTL

Implement a cache that evicts the least-recently-used entry when capacity is exceeded, and treats entries older than TTL as missing.

Show solution
ttl_lru.py
from collections import OrderedDict
import time

class TTLLRUCache:
    def __init__(self, capacity: int, ttl_seconds: float):
        self.capacity = capacity
        self.ttl = ttl_seconds
        self.data: OrderedDict = OrderedDict()

    def get(self, key):
        if key not in self.data:
            return None
        value, ts = self.data[key]
        if time.time() - ts > self.ttl:
            del self.data[key]
            return None
        self.data.move_to_end(key)
        return value

    def set(self, key, value):
        self.data[key] = (value, time.time())
        self.data.move_to_end(key)
        if len(self.data) > self.capacity:
            self.data.popitem(last=False)  # evict LRU

P8. Rate limiter for an inference service

Implement a per-customer rate limiter that allows N requests per minute, sliding window.

Show solution
rate_limiter.py
from collections import defaultdict, deque
import time

class SlidingWindowLimiter:
    def __init__(self, max_per_minute: int):
        self.limit = max_per_minute
        self.windows: dict[str, deque] = defaultdict(deque)

    def allow(self, customer_id: str) -> bool:
        now = time.time()
        window = self.windows[customer_id]
        # Drop entries older than 60s
        while window and window[0] < now - 60:
            window.popleft()
        if len(window) >= self.limit:
            return False
        window.append(now)
        return True

Tradeoff discussion to mention unprompted: this is in-memory per-process. At scale you'd want Redis with sorted sets, or a token bucket — both more memory-efficient at high QPS. The sliding-window-log is exact but uses O(N) memory per customer.

D · Classic algorithms (lighter)

P9. Two-sum

Given an array of ints and a target, return indices of two elements summing to target.

Show solution
two_sum.py
def two_sum(nums: list[int], target: int) -> tuple[int, int] | None:
    seen = {}
    for i, n in enumerate(nums):
        if target - n in seen:
            return (seen[target - n], i)
        seen[n] = i
    return None

P10. Subarray sum equals K

Return the number of contiguous subarrays whose sum equals K. Negative numbers allowed.

Show solution
subarray_sum.py
def subarray_sum_eq_k(nums: list[int], k: int) -> int:
    seen = {0: 1}
    running = 0
    answer = 0
    for n in nums:
        running += n
        answer += seen.get(running - k, 0)
        seen[running] = seen.get(running, 0) + 1
    return answer

Prefix-sum + hashmap trick. O(n) time, O(n) space. Handles negatives — that's the difference from a sliding-window approach.

Drill protocol

How to use this page

Enable drill mode. Read each problem, give yourself 20 minutes on paper or in a notebook. Speak through approach + complexity out loud before writing. Reveal the solution. Compare. Mark "practiced." Aim for 6–7 cleared before any onsite — particularly the feature-engineering ones if you're targeting fraud-domain roles.

✓ Check yourself

Answer from memory before opening each one.

  1. A stuck-sensor flag counts consecutive zero differences and compares the count with a threshold of 10. Feed it a run of exactly 10 identical readings. Does it fire, and why?

    Show answer

    No. Ten identical readings give only nine zero differences, so the count never reaches 10. Number each run and measure its length directly, which also flags the whole run instead of its tail. Run it on a series with one run of 10 to confirm. See the feature problems.

  2. The loop-based velocity solution works on 10,000 rows. Why does it fail at 50 million, and what do you do instead?

    Show answer

    Every row filters the whole table, so work grows with the square of the row count. Push the windowed distinct counts into SQL with a time-bounded self-join or window, or precompute them into a feature store keyed by entity. See the feature problems.

  3. The overall fraud rate is 1% and the top 5% of scores holds 6% fraud. What are the lift and the recall at 5%?

    Show answer

    Lift is 6, the top-slice rate divided by the base rate. In 1,000 rows, the top 50 hold 3 frauds out of 10 in total, so recall is 30%. Lift describes concentration and recall describes coverage, and the business needs both. See the evaluation problems.

  4. A feature’s PSI against last month’s baseline is 0.31. What does that tell you, and what does it leave open?

    Show answer

    The feature’s distribution shifted materially, above the 0.25 attention line, so investigate the source. It does not say model performance fell, and it cannot see changes in the relationship between features and the label. Bins come from baseline quantiles and a small count keeps empty bins from breaking the log. See the evaluation problems.

  5. Your sliding-window rate limiter passes its tests. In production, 12 instances sit behind a load balancer. What goes wrong, and what are the options?

    Show answer

    Each process keeps its own windows, so a customer can send up to twelve times the limit. Share state in a store such as Redis with sorted sets, or use a token bucket that needs less memory per customer. The exact log costs memory proportional to the limit for each customer. See the production problems.

  6. Why does the prefix-sum and hashmap solution count subarrays with a target sum when negative numbers are present, while a sliding window does not?

    Show answer

    A sliding window assumes that adding an element only raises the sum and removing one only lowers it, which fails with negatives. The hashmap stores how many times each running total has occurred, so the number of earlier prefixes equal to the current total minus K is the number of subarrays ending here. See the classic problems.

Takeaway

  • Most problems in a full-stack loop are domain-flavoured: entity velocity, time since last event, run detection, lift, thresholds, PSI, caches and rate limiters.
  • The naive loop is a fine first answer if you say why it is quadratic and how SQL, a feature store or a sorted structure would replace it.
  • Check run-length and window logic for off-by-one errors on a case with exactly the threshold value.
  • Report lift together with recall, and choose thresholds from the sorted cumulative precision, handling ties explicitly.
  • Name production trade-offs unprompted: in-process state breaks behind a load balancer, and an exact log costs memory.
  • Prefix sums with a hashmap handle negatives where sliding windows cannot.

Code that works in a notebook still runs on infrastructure, so the next chapter covers the stack behind it. → AWS Data Stack

Further reading