Clean and aggregate factory event data in Pandas
Company: Roblox
Role: Data Scientist
Category: Data Manipulation (SQL/Python)
Difficulty: medium
Interview Round: Online Assessment
You are given three Pandas DataFrames for a factory: (1) events[event_id, machine_id, ts_utc (datetime64[ns, UTC]), event_type in {'start','stop','fault'}, batch_id], (2) telemetry[machine_id, ts_local (datetime64[ns]), temperature_C, rpm, power_kW, timezone (IANA string like 'US/Pacific')], (3) calendar[date (YYYY-MM-DD), is_holiday (bool), shift in {'A','B','C'}]. Data issues: late-arriving events up to 48 hours late, duplicate events (same event_id with ts_utc differences up to ±2 seconds), daylight saving transitions, and missing telemetry rows. Memory budget is 1 GB, total rows ≈50M. Tasks: a) Normalize all time to a single axis; deduplicate events with a deterministic rule (state your rule) while preserving correct event order. b) For the last 7 calendar days up to and including today=2025-09-01 in each machine’s local time, compute per-machine hourly features: throughput (count of completed start→stop cycles), 95th percentile temperature, and a rolling 24-hour z-score of power_kW. Handle missing hours and DST gaps/overlaps correctly. c) Join features into a tidy machine-hour panel indexed by [machine_id, hour_start_utc); impute missing values robustly; flag anomalies where |z|>3. Provide Pandas code snippets and explain performance tactics (chunked IO, dtypes, categoricals, Parquet, vectorized ops) and how you would test correctness on edge cases.
Overview: This question evaluates a candidate's ability to clean and aggregate large-scale time-series data in Pandas, emphasizing timezone normalization, deterministic deduplication, DST-aware alignment, event-sequence aggregation, rolling statistical features, anomaly detection, and memory- and performance-conscious ETL techniques.
Deduplicate factory events while preserving time order
You are given factory event data with possible duplicate events for the same logical event_id. Duplicates are defined as rows that share the same event_id but may have slightly different ts_utc values (up to a couple of seconds apart). The goal is to deduplicate these events in a deterministic way while preserving chronological ordering.
Using the events table below:
- Treat ts_utc as the canonical timestamp (already in UTC).
- If multiple rows have the same event_id, keep only the row with the latest ts_utc.
- Return one row per event_id with the columns: event_id, machine_id, ts_utc, event_type, batch_id. Format `ts_utc` as `YYYY-MM-DDTHH24:MI:SSZ`.
- Order the result by machine_id ascending, then ts_utc ascending.
Write a SQL query to produce this deduplicated, time-ordered list of events.
Tables
events(event_row_id INT, event_id INT, machine_id INT, ts_utc TIMESTAMP WITH TIME ZONE, event_type VARCHAR(10), batch_id INT)
telemetry(id INT, machine_id INT, ts_local TIMESTAMP WITHOUT TIME ZONE, temperature_c DECIMAL(5,2), rpm INT, power_kw DECIMAL(6,2), timezone VARCHAR(50))
calendar(date DATE, is_holiday BOOLEAN, shift CHAR(1))
Hints
- Use `ROW_NUMBER` partitioned by `event_id` and ordered by `ts_utc DESC` to keep the latest duplicate.
- Format the UTC timestamp with a PostgreSQL `TO_CHAR` template containing literal `T` and `Z` characters.
Compute machine-hour throughput, temperature percentiles, and rolling power z-scores
You are building per-machine, per-hour features for a 4-hour window on a UTC time axis, combining deduplicated production events with machine telemetry.
There are three tables (`events`, `telemetry`, `calendar`). For this question only `events` and `telemetry` are needed.
Assume the window is the 4 hourly buckets from **2025-05-26 00:00:00 UTC** through **2025-05-26 03:00:00 UTC** (inclusive of both endpoints), so the hour-start timestamps are 00:00, 01:00, 02:00, and 03:00 on 2025-05-26.
Produce the feature table as follows:
1. **Deduplicate events.** The `events` table may contain duplicate rows for the same `event_id` (a late re-emission). For each `event_id`, keep only the row with the latest `ts_utc`.
2. **Normalize telemetry to UTC.** `telemetry.ts_local` is a wall-clock timestamp with no zone; the `timezone` column holds the IANA zone name (e.g. `'UTC'`, `'America/Los_Angeles'`). Convert each reading to a UTC instant before bucketing it.
3. **Build a complete hour grid.** Output one row for **every** machine and **every** one of the 4 hour buckets in the window — even hours with no events and no telemetry. The set of machines is the distinct machines that appear in either the deduplicated events or the telemetry.
4. **`throughput` (INT).** Using the deduplicated, time-ordered events per machine, a *completed production cycle* is a `'stop'` event whose immediately preceding event (by `ts_utc`, same machine) is a `'start'`. Count, per machine and hour, how many such completed cycles have their `'stop'` timestamp falling in that hour. Hours with no completed cycle have `throughput = 0`.
5. **`temp_p95`.** The 95th percentile (discrete, `percentile_disc`) of `temperature_c` over the normalized telemetry readings that fall in that machine/hour. NULL when the hour has no telemetry.
6. **`avg_power_kw`.** The average `power_kw` over the normalized telemetry readings in that machine/hour. NULL when the hour has no telemetry.
7. **`power_kw_z24`.** A rolling 24-hour z-score of `avg_power_kw` per machine, ordered by `hour_start_utc`, over the frame `ROWS BETWEEN 23 PRECEDING AND CURRENT ROW` (up to the previous 23 hours plus the current hour). The z-score is `(avg_power_kw - mean) / stddev`, where `mean` and the **sample** standard deviation (`stddev_samp`) are taken over the non-null hourly averages in that frame. If fewer than 2 non-null hourly averages are in the frame, or the sample standard deviation is 0, the z-score is NULL.
**Return exactly these columns**, one row per machine and hour:
`machine_id`, `hour_start_utc`, `throughput`, `temp_p95`, `avg_power_kw`, `power_kw_z24`.
**Sort the result by `machine_id`, then `hour_start_utc` ascending.**
Tables
events(event_row_id INT, event_id INT, machine_id INT, ts_utc TIMESTAMP WITH TIME ZONE, event_type VARCHAR(10), batch_id INT)
telemetry(id INT, machine_id INT, ts_local TIMESTAMP WITHOUT TIME ZONE, temperature_c DECIMAL(5,2), rpm INT, power_kw DECIMAL(6,2), timezone VARCHAR(50))
calendar(date DATE, is_holiday BOOLEAN, shift CHAR(1))
Hints
- Build the per-machine/per-hour skeleton first: cross every distinct machine with generate_series over the 4 hour-start timestamps, then LEFT JOIN events and telemetry onto it so empty hours survive.
- Convert telemetry to UTC with `ts_local AT TIME ZONE timezone` (use a real IANA zone like 'America/Los_Angeles', not 'US/Pacific'), and detect completed cycles with LAG(event_type) over each machine ordered by ts_utc.
Build a tidy machine-hour panel with imputation and anomaly flags
Two source tables capture factory activity over a fixed time window:
- **`events`** — machine lifecycle events. Each physical event may be **duplicated** (same `event_id` inserted more than once with a slightly different `ts_utc`); keep only **one** copy per `event_id` (the latest `ts_utc`). `ts_utc` is already in UTC (`TIMESTAMP WITH TIME ZONE`). `event_type` is one of `'start'`, `'stop'`, `'fault'`.
- **`telemetry`** — periodic sensor readings (`temperature_c`, `power_kw`) stored in **machine-local time** (`ts_local`, a naive timestamp) together with the IANA `timezone` of that machine. Normalize every reading to UTC with `ts_local AT TIME ZONE timezone`.
(The `calendar` table is provided for context only and is not required by this question.)
Build a **tidy machine-hour panel** over the window **2025-05-26 00:00:00 UTC through 2025-05-26 05:00:00 UTC**, inclusive of both endpoints — i.e. the **6 hourly buckets** `00:00, 01:00, 02:00, 03:00, 04:00, 05:00` (UTC). The grid must be **complete**: emit **one row per `machine_id` and `hour_start_utc`** even for hours with no events and no telemetry. A machine appears in the grid if it has at least one row in `events` or in `telemetry`.
**Step 1 — per machine and hour, compute the raw hourly metrics:**
- `throughput` = number of **completed cycles** ending in that hour. A completed cycle is a deduplicated `'stop'` event whose immediately preceding event for the same machine (ordered by `ts_utc`) is a `'start'`. Count a cycle in the hour bucket `[hour_start_utc, hour_start_utc + 1 hour)` that contains its `'stop'` timestamp.
- `temp_p95` = the 95th-percentile temperature (`percentile_disc(0.95)`) over telemetry readings whose UTC timestamp falls in that hour bucket; `NULL` if the hour has no telemetry.
- `avg_power_kw` = the average `power_kw` over telemetry readings in that hour bucket; `NULL` if none.
- `power_kw_z24` = the rolling 24-hour z-score of `avg_power_kw` within each machine, over the window frame `ROWS BETWEEN 23 PRECEDING AND CURRENT ROW` ordered by `hour_start_utc`. Define it as `(avg_power_kw - mean) / stddev_samp` over that frame, and leave it `NULL` when fewer than 2 non-null values are in the frame or the sample standard deviation is 0.
**Step 2 — impute missing values:**
- `throughput`: replace `NULL` with `0`.
- `temp_p95`: replace `NULL` with the **mean of `temp_p95` for that `machine_id` over the whole window** (ignoring `NULL`s) → `temp_p95_imputed`.
- `avg_power_kw`: replace `NULL` with the **mean of `avg_power_kw` for that `machine_id` over the whole window** (ignoring `NULL`s) → `avg_power_kw_imputed`.
- `power_kw_z24`: replace `NULL` with `0` → `power_kw_z24_imputed`.
**Step 3 — anomaly flag:** add a boolean `is_anomaly` that is `TRUE` when `ABS(power_kw_z24_imputed) > 3`, else `FALSE`.
**Output:** one row per `machine_id`, `hour_start_utc` with the columns, in this order:
`machine_id`, `hour_start_utc`, `throughput`, `temp_p95_imputed`, `avg_power_kw_imputed`, `power_kw_z24_imputed`, `is_anomaly`.
Round the three imputed numeric columns to **4 decimal places**. Order the result by `machine_id`, then `hour_start_utc` ascending.
Tables
events(event_row_id INT, event_id INT, machine_id INT, ts_utc TIMESTAMP WITH TIME ZONE, event_type VARCHAR(10), batch_id INT)
telemetry(id INT, machine_id INT, ts_local TIMESTAMP WITHOUT TIME ZONE, temperature_c DECIMAL(5,2), rpm INT, power_kw DECIMAL(6,2), timezone VARCHAR(50))
calendar(date DATE, is_holiday BOOLEAN, shift CHAR(1))
Hints
- Deduplicate events by event_id (keep latest ts_utc) BEFORE pairing start->stop with LAG, and convert telemetry to UTC with `ts_local AT TIME ZONE timezone`.
- Use generate_series cross-joined to every machine to build the complete hourly grid, then LEFT JOIN the throughput and telemetry aggregates so empty hours survive.