Coding questions · Uber
Compute ETA shift and conversion uplift
This question evaluates proficiency in SQL and Python for time‑series data manipulation and experimental analysis, covering window functions (7‑day percentile_cont medians), date spines and timezone‑aware timestamp truncation, conversion-rate aggregation, difference‑in‑differences uplift estimation, and CUPED adjustment with cluster‑robust standard errors. It is commonly asked to assess practical data engineering and applied statistics skills in the Data Manipulation (SQL/Python) domain, testing applied implementation and causal inference ability rather than only conceptual understanding.
Use PostgreSQL (SQL) and brief Python pseudocode. Assume 'today' is 2025-09-01.
Schema:
- trips(trip_id BIGINT, request_ts TIMESTAMP, city_id INT, rider_id BIGINT, driver_id BIGINT, shown_eta_sec INT, pickup_eta_sec INT, is_completed BOOLEAN, is_canceled BOOLEAN, surge_multiplier NUMERIC, is_treatment BOOLEAN, experiment_id INT)
- riders(rider_id BIGINT, signup_dt DATE, city_id INT)
- city_dim(city_id INT, city_name TEXT, tier TEXT)
- incentives(rider_id BIGINT, offer_start_dt DATE, offer_end_dt DATE, percent_off INT, cap_usd NUMERIC)
- day_weather(city_id INT, dt DATE, precip_mm NUMERIC, temp_c NUMERIC)
Small ASCII samples:
trips
trip_id | request_ts | city_id | rider_id | driver_id | shown_eta_sec | pickup_eta_sec | is_completed | is_canceled | surge_multiplier | is_treatment | experiment_id
1 | 2025-08-01 08:01:00 | 1 | 101 | 9001 | 420 | 480 | t | f | 1.0 | t | 42
2 | 2025-08-01 08:03:00 | 1 | 102 | 9002 | 360 | 360 | f | t | 1.2 | f | 42
3 | 2025-08-15 18:30:00 | 2 | 103 | 9003 | 300 | 420 | t | f | 1.1 | f | 42
4 | 2025-08-20 09:00:00 | 1 | 101 | 9001 | 330 | 360 | t | f | 1.0 | t | 42
5 | 2025-08-28 22:10:00 | 2 | 104 | 9004 | 600 | 660 | f | t | 1.5 | t | 42
riders
rider_id | signup_dt | city_id
101 | 2025-05-01 | 1
102 | 2025-01-15 | 1
103 | 2025-06-20 | 2
104 | 2025-03-10 | 2
city_dim
city_id | city_name | tier
1 | Alpha | T1
2 | Beta | T2
incentives
rider_id | offer_start_dt | offer_end_dt | percent_off | cap_usd
101 | 2025-08-15 | 2025-08-31 | 20 | 10
Tasks:
- SQL: For experiment_id=42, compute for each city_id and calendar date the 7-day rolling median of shown_eta_sec and the request→trip conversion rate (completed/requests) from 2025-08-01 to 2025-09-01. Include days with zero requests (show conversion as NULL) by generating a date spine. Restrict to riders with signup_dt < 2025-07-01. Use percentile_cont(0.5) OVER a 7-day window for the median; clearly handle time zones by truncating request_ts at UTC midnight.
- SQL: At city level, estimate a difference‑in‑differences conversion uplift between treated (is_treatment=true) and control for post=2025-08-15..2025-09-01 vs pre=2025-08-01..2025-08-14. Output: city_id, pre_treat_conv, pre_ctrl_conv, post_treat_conv, post_ctrl_conv, did_uplift = (post_treat_conv - pre_treat_conv) - (post_ctrl_conv - pre_ctrl_conv). Ensure riders with mixed treatment exposure are counted per their trip‑level exposure.
- Python (pseudocode ok): Compute CUPED‑adjusted conversion at the rider_id level with X = pre‑period conversion and Y = post‑period conversion; estimate theta = cov(Y, X)/var(X), compute Y_cuped = Y - theta*(X - E[X]). Then estimate adjusted uplift between treated and control, with its standard error via cluster‑robust SEs at rider_id or city_id.
7-Day Rolling ETA Median and Daily Conversion with Date Spine
Using PostgreSQL, you are analyzing an A/B experiment (experiment_id = 42) on rider-facing ETA estimates. For each city_id and each calendar date from 2025-08-01 to 2025-09-01 (inclusive), compute two daily metrics:
- 7-day rolling median of
shown_eta_sec(in seconds) ending on that date, and - The same-day request-to-trip conversion rate (completed trips / total requests on that date).
Input tables
trips- one row per ride request. Relevant columns:trip_id,request_ts(TIMESTAMP, treat as UTC),city_id,rider_id,shown_eta_sec(INT seconds),is_completed(BOOLEAN),experiment_id(INT).riders- one row per rider. Relevant columns:rider_id,signup_dt(DATE).
Rules
- Consider only rows with
experiment_id = 42. - Derive
request_dateasdate_trunc('day', request_ts AT TIME ZONE 'UTC')::date. - Restrict to eligible riders whose
signup_dt < 2025-07-01(jointripstoridersonrider_id). - Restrict the analysis window to requests whose
request_dateis between 2025-08-01 and 2025-09-01 (inclusive). - Rolling median: for each date that actually has eligible trips for a city, the 7-day rolling median is the
percentile_cont(0.5)(continuous median) ofshown_eta_secover all eligible trips of that city whoserequest_datefalls in the 7-calendar-day window[request_date - 6 days, request_date]. - Conversion: for each date that has eligible trips,
daily_conversion = (# completed trips) / (total requests)on that exact date. - Date spine: every
city_idthat has any eligible trips inexperiment_id = 42must appear once for every date from 2025-08-01 to 2025-09-01, even on days with zero requests. On a day with zero requests for a city, bothmedian_shown_eta_7d_secanddaily_conversionmust be NULL.
Required output
One row per (city_id, date), with columns in this order:
city_idrequest_date(DATE)median_shown_eta_7d_sec- the 7-day rolling median ofshown_eta_sec, or NULL on zero-request daysdaily_conversion- same-day completed/total requests, or NULL on zero-request days
Sort the result by city_id, then request_date ascending.
Table trips
| column | type | constraints |
|---|---|---|
| trip_id | BIGINT | PRIMARY KEY |
| request_ts | TIMESTAMP | NOT NULL |
| city_id | INT | NOT NULL |
| rider_id | BIGINT | NOT NULL |
| driver_id | BIGINT | |
| shown_eta_sec | INT | |
| pickup_eta_sec | INT | |
| is_completed | BOOLEAN | |
| is_canceled | BOOLEAN | |
| surge_multiplier | NUMERIC | |
| is_treatment | BOOLEAN | |
| experiment_id | INT |
Sample data:
| city_id | driver_id | experiment_id | is_canceled | is_completed | is_treatment | pickup_eta_sec | request_ts | rider_id | shown_eta_sec | surge_multiplier | trip_id |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 9001 | 42 | false | true | true | 480 | 2025-08-01 08:01:00 | 101 | 420 | 1 | 1 |
| 1 | 9002 | 42 | true | false | false | 360 | 2025-08-01 08:03:00 | 102 | 360 | 1.2 | 2 |
| 2 | 9003 | 42 | false | true | false | 420 | 2025-08-15 18:30:00 | 103 | 300 | 1.1 | 3 |
| 1 | 9001 | 42 | false | true | true | 360 | 2025-08-20 09:00:00 | 101 | 330 | 1 | 4 |
| 2 | 9004 | 42 | true | false | true | 660 | 2025-08-28 22:10:00 | 104 | 600 | 1.5 | 5 |
Table riders
| column | type | constraints |
|---|---|---|
| rider_id | BIGINT | PRIMARY KEY |
| signup_dt | DATE | NOT NULL |
| city_id | INT | NOT NULL |
Sample data:
| city_id | rider_id | signup_dt |
|---|---|---|
| 1 | 101 | 2025-05-01 |
| 1 | 102 | 2025-01-15 |
| 2 | 103 | 2025-06-20 |
| 2 | 104 | 2025-03-10 |
Expected output
One row per (city_id, calendar date 2025-08-01..2025-09-01): the 7-day rolling median of shown_eta_sec and the same-day completed/total conversion rate, with NULLs on zero-request days, ordered by city_id then request_date.
| city_id | daily_conversion | median_shown_eta_7d_sec | request_date |
|---|---|---|---|
| 1 | 0.5 | 390 | 2025-08-01 |
| 1 | NULL | NULL | 2025-08-02 |
| 1 | NULL | NULL | 2025-08-03 |
| 1 | NULL | NULL | 2025-08-04 |
| 1 | NULL | NULL | 2025-08-05 |
| 1 | NULL | NULL | 2025-08-06 |
| 1 | NULL | NULL | 2025-08-07 |
| 1 | NULL | NULL | 2025-08-08 |
| 1 | NULL | NULL | 2025-08-09 |
| 1 | NULL | NULL | 2025-08-10 |
| 1 | NULL | NULL | 2025-08-11 |
| 1 | NULL | NULL | 2025-08-12 |
| 1 | NULL | NULL | 2025-08-13 |
| 1 | NULL | NULL | 2025-08-14 |
| 1 | NULL | NULL | 2025-08-15 |
| 1 | NULL | NULL | 2025-08-16 |
| 1 | NULL | NULL | 2025-08-17 |
| 1 | NULL | NULL | 2025-08-18 |
| 1 | NULL | NULL | 2025-08-19 |
| 1 | 1 | 330 | 2025-08-20 |
| 1 | NULL | NULL | 2025-08-21 |
| 1 | NULL | NULL | 2025-08-22 |
| 1 | NULL | NULL | 2025-08-23 |
| 1 | NULL | NULL | 2025-08-24 |
| 1 | NULL | NULL | 2025-08-25 |
| 1 | NULL | NULL | 2025-08-26 |
| 1 | NULL | NULL | 2025-08-27 |
| 1 | NULL | NULL | 2025-08-28 |
| 1 | NULL | NULL | 2025-08-29 |
| 1 | NULL | NULL | 2025-08-30 |
| 1 | NULL | NULL | 2025-08-31 |
| 1 | NULL | NULL | 2025-09-01 |
| 2 | NULL | NULL | 2025-08-01 |
| 2 | NULL | NULL | 2025-08-02 |
| 2 | NULL | NULL | 2025-08-03 |
| 2 | NULL | NULL | 2025-08-04 |
| 2 | NULL | NULL | 2025-08-05 |
| 2 | NULL | NULL | 2025-08-06 |
| 2 | NULL | NULL | 2025-08-07 |
| 2 | NULL | NULL | 2025-08-08 |
| 2 | NULL | NULL | 2025-08-09 |
| 2 | NULL | NULL | 2025-08-10 |
| 2 | NULL | NULL | 2025-08-11 |
| 2 | NULL | NULL | 2025-08-12 |
| 2 | NULL | NULL | 2025-08-13 |
| 2 | NULL | NULL | 2025-08-14 |
| 2 | 1 | 300 | 2025-08-15 |
| 2 | NULL | NULL | 2025-08-16 |
| 2 | NULL | NULL | 2025-08-17 |
| 2 | NULL | NULL | 2025-08-18 |
| 2 | NULL | NULL | 2025-08-19 |
| 2 | NULL | NULL | 2025-08-20 |
| 2 | NULL | NULL | 2025-08-21 |
| 2 | NULL | NULL | 2025-08-22 |
| 2 | NULL | NULL | 2025-08-23 |
| 2 | NULL | NULL | 2025-08-24 |
| 2 | NULL | NULL | 2025-08-25 |
| 2 | NULL | NULL | 2025-08-26 |
| 2 | NULL | NULL | 2025-08-27 |
| 2 | 0 | 600 | 2025-08-28 |
| 2 | NULL | NULL | 2025-08-29 |
| 2 | NULL | NULL | 2025-08-30 |
| 2 | NULL | NULL | 2025-08-31 |
| 2 | NULL | NULL | 2025-09-01 |
Hints
- percentile_cont is an ordered-set aggregate - PostgreSQL does NOT permit it as a window function (... OVER (...)). Compute the rolling median with a correlated subquery (or LATERAL) over the 7-day window instead.
- Build a date spine with CROSS JOIN generate_series('2025-08-01','2025-09-01', INTERVAL '1 day') against each experiment city, then LEFT JOIN the metrics so zero-request days stay NULL.
- Cast the completed count to numeric (completed::numeric / total) so the conversion rate isn't integer-divided to 0.
City-Level Difference-in-Differences Conversion Uplift
Using the same trips and riders tables, compute a city-level difference-in-differences (DiD) estimate of conversion uplift between treated trips (is_treatment = TRUE) and control trips (is_treatment = FALSE) for experiment_id = 42.
Define periods:
- Pre-period: 2025-08-01 to 2025-08-14 (inclusive)
- Post-period: 2025-08-15 to 2025-09-01 (inclusive)
Details:
- Treat request_ts as UTC and truncate to calendar dates via date_trunc('day', request_ts AT TIME ZONE 'UTC')::date.
- Restrict to riders with signup_dt < 2025-07-01.
- Conversion is defined as completed trips / total requests.
- For each city_id, compute:
- pre_treat_conv: average conversion rate for treated trips in the pre-period.
- pre_ctrl_conv: average conversion rate for control trips in the pre-period.
- post_treat_conv: average conversion rate for treated trips in the post-period.
- post_ctrl_conv: average conversion rate for control trips in the post-period.
- Riders who have both treated and control trips should contribute to each group according to their trip-level exposure (i.e., aggregate at the trip level, not rider level).
- Then compute the DiD uplift as: did_uplift = (post_treat_conv - pre_treat_conv) - (post_ctrl_conv - pre_ctrl_conv).
Output columns:
- city_id
- pre_treat_conv
- pre_ctrl_conv
- post_treat_conv
- post_ctrl_conv
- did_uplift
Table trips
| column | type | constraints |
|---|---|---|
| trip_id | BIGINT | PRIMARY KEY |
| request_ts | TIMESTAMP | NOT NULL |
| city_id | INT | NOT NULL |
| rider_id | BIGINT | NOT NULL |
| driver_id | BIGINT | |
| shown_eta_sec | INT | |
| pickup_eta_sec | INT | |
| is_completed | BOOLEAN | |
| is_canceled | BOOLEAN | |
| surge_multiplier | NUMERIC | |
| is_treatment | BOOLEAN | |
| experiment_id | INT |
Sample data:
| city_id | driver_id | experiment_id | is_canceled | is_completed | is_treatment | pickup_eta_sec | request_ts | rider_id | shown_eta_sec | surge_multiplier | trip_id |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 9001 | 42 | false | true | true | 480 | 2025-08-01 08:01:00 | 101 | 420 | 1 | 1 |
| 1 | 9002 | 42 | true | false | false | 360 | 2025-08-01 08:03:00 | 102 | 360 | 1.2 | 2 |
| 2 | 9003 | 42 | false | true | false | 420 | 2025-08-15 18:30:00 | 103 | 300 | 1.1 | 3 |
| 1 | 9001 | 42 | false | true | true | 360 | 2025-08-20 09:00:00 | 101 | 330 | 1 | 4 |
| 2 | 9004 | 42 | true | false | true | 660 | 2025-08-28 22:10:00 | 104 | 600 | 1.5 | 5 |
Table riders
| column | type | constraints |
|---|---|---|
| rider_id | BIGINT | PRIMARY KEY |
| signup_dt | DATE | NOT NULL |
| city_id | INT | NOT NULL |
Sample data:
| city_id | rider_id | signup_dt |
|---|---|---|
| 1 | 101 | 2025-05-01 |
| 1 | 102 | 2025-01-15 |
| 2 | 103 | 2025-06-20 |
| 2 | 104 | 2025-03-10 |
Expected output
For each city_id, the query returns pre- and post-period conversion rates for treated and control trips, plus the difference-in-differences uplift. On the sample data, some combinations are NULL because there are no trips of that type in that period.
| city_id | did_uplift | post_ctrl_conv | post_treat_conv | pre_ctrl_conv | pre_treat_conv |
|---|---|---|---|---|---|
| 1 | NULL | NULL | 1 | 0 | 1 |
| 2 | NULL | 1 | 0 | NULL | NULL |
Hints
- Start by labeling each trip as pre or post based on the truncated request_date, and as treatment or control using is_treatment.
- Aggregate conversion rates by city_id, period, and is_treatment, then pivot these into columns and compute the DiD formula using simple arithmetic.