Quick Overview

This question evaluates competence in designing panel-aware time-series cross-validation with embargo handling, grouped blocking, deterministic splitting, scalability analysis, and adversarial unit-test design for issues like duplicate timestamps and entity migration.

Implement blocked time-series cross-validation

Company: Freddie Mac

Role: Data Scientist

Category: Coding & Algorithms

Difficulty: medium

Interview Round: Onsite

Implement a panel-aware blocked time-series cross-validation splitter with an embargo. Input: DataFrame columns [loan_id, msa, month]. Requirements: (1) K=5 expanding-window folds with cutoffs t1<...<t5; for fold k, train on months <= tk−embargo and test on (tk−embargo, tk+1]; embargo = 90 days prevents any loan_id from appearing in both sets within the window. (2) Support grouped blocking so that in at least one fold, an entire MSA's months are held out as test while preserving temporal order within each MSA. (3) Ensure determinism and stability under shuffled input. (4) Output precise algorithm steps or pseudocode plus time complexity in N rows, G MSAs, and K folds; discuss memory characteristics. (5) Describe unit tests for adversarial cases: duplicate timestamps, missing months, loan migration across MSAs, and highly imbalanced MSAs.

Quick Answer: This question evaluates competence in designing panel-aware time-series cross-validation with embargo handling, grouped blocking, deterministic splitting, scalability analysis, and adversarial unit-test design for issues like duplicate timestamps and entity migration.

Implement the core of a panel-aware blocked time-series cross-validation splitter with an embargo gap. This is the executable kernel of the Freddie Mac data-science question (the open-ended complexity/memory discussion and the grouped-MSA blocking strategy are left as written follow-ups). You are given a panel of loan-month observations and must produce K expanding-window folds. Each row is `[loan_id, msa, month]` where `loan_id` is an int, `msa` is a string, and `month` is an integer month index (e.g. 1, 2, 3, ... where consecutive integers are one month apart, so an embargo of E integer units = E months). Write `solution(rows, k, embargo)` returning a list of exactly `k` folds (when `rows` is non-empty and `k > 0`), each a dict `{"train": [...rows...], "test": [...rows...]}`. Fold construction (expanding window): 1. Let `months` be the sorted list of DISTINCT month values present (size `m`). 2. The `k` cutoff months are taken at evenly spaced positions: for the j-th cutoff (j = 1..k), `idx = round(j * (m - 1) / (k + 1))`, clamped to `[0, m-1]`, and the cutoff is `months[idx]`. 3. For the j-th fold (0-based) with cutoff `t = cutoffs[j]`: - `train` = all rows with `month <= t - embargo` - `test` = all rows with `t - embargo < month <= next_cut`, where `next_cut = cutoffs[j+1]` for non-final folds, else the maximum month. 4. Within every fold, both `train` and `test` are sorted by `(month, loan_id, msa)`. Determinism (requirement 3): the result must be a pure function of the SET of input rows — shuffling the input list must not change the output. Always emit exactly `k` folds even if some train/test windows are empty. Return `[]` only when `rows` is empty or `k <= 0`.

Constraints

  • 0 <= N <= 10^5 rows; each row is [loan_id (int), msa (str), month (int)].
  • 1 <= k <= number of distinct months (k <= 0 or empty rows -> return []).
  • embargo is a non-negative integer number of month units; consecutive integer months are one unit apart.
  • month values may repeat (duplicate timestamps) and need not be contiguous (missing months).
  • The same loan_id may appear under different msa values (loan migration) — rows are treated independently by month.
  • Output must be invariant to the ordering of the input rows.

Examples

Input: ([[1, "A", 1], [1, "A", 2], [2, "A", 3], [2, "B", 4], [3, "B", 5], [3, "B", 6], [4, "C", 7], [4, "C", 8]], 2, 1)

Expected Output: [{"train": [[1, "A", 1], [1, "A", 2]], "test": [[2, "A", 3], [2, "B", 4], [3, "B", 5], [3, "B", 6]]}, {"train": [[1, "A", 1], [1, "A", 2], [2, "A", 3], [2, "B", 4], [3, "B", 5]], "test": [[3, "B", 6], [4, "C", 7], [4, "C", 8]]}]

Explanation: 8 distinct months. k=2 -> cutoffs at months[round(1*7/3)=2]=3 and months[round(2*7/3)=5]=6. Fold 0: train = month<=3-1=2; test = (2, 6]. Fold 1: train = month<=6-1=5; test = (5, 8]. The embargo of 1 removes month 2 from fold-1 test even though it would otherwise border the train edge.

Input: ([[4, "C", 8], [2, "A", 3], [1, "A", 1], [3, "B", 6], [1, "A", 2], [4, "C", 7], [3, "B", 5], [2, "B", 4]], 2, 1)

Expected Output: [{"train": [[1, "A", 1], [1, "A", 2]], "test": [[2, "A", 3], [2, "B", 4], [3, "B", 5], [3, "B", 6]]}, {"train": [[1, "A", 1], [1, "A", 2], [2, "A", 3], [2, "B", 4], [3, "B", 5]], "test": [[3, "B", 6], [4, "C", 7], [4, "C", 8]]}]

Explanation: The exact same rows as the previous case but shuffled. The output is identical, demonstrating determinism and stability under shuffled input (requirement 3).

Hints

  1. Sort the rows once by (month, loan_id, msa) up front; every fold then reads from this canonical order, which is what makes the output independent of input shuffling.
  2. Compute the K cutoff months from the DISTINCT sorted months using idx = round(j*(m-1)/(k+1)) for j = 1..k, then clamp to [0, m-1].
  3. For each fold, the train window ends at cutoff - embargo (inclusive) and the test window is the half-open band (cutoff - embargo, next_cut]; the last fold's next_cut is the maximum month.
  4. Always emit exactly k folds: a fold whose train or test band is empty still contributes an entry with an empty list.

Loading coding console...