cswork / libraryOpen practice workspace

Coding questions · Meta

Write SQL/pandas for KPI anomaly

This question evaluates proficiency with SQL window functions and equivalent pandas operations for KPI anomaly detection, covering baseline computation using same-weekday medians over eight weeks, per-country and per-post_type Likes-per-DAU metrics with deduplication of users for DAU, outage filtering, attribution of declines between new and existing users, and time-series comparisons like friend-request acceptance rates while explicitly handling duplicates, sparse or missing DAU rows, and timezone issues. It is commonly asked in Data Manipulation (SQL/Python) interviews to assess practical data engineering and analytical skills, primarily testing practical application with an underlying need for conceptual understanding of baselining, attribution, and data-quality edge cases.

Write SQL (and outline equivalent pandas) for a KPI anomaly investigation. Assume today = '2025-09-01'. Schema:

Users(user_id INT, country TEXT, signup_date DATE)
Posts(post_id INT, user_id INT, post_type TEXT /* 'friend','page','event' */, created_at DATE)
Likes(like_id INT, user_id INT, post_id INT, created_at DATE)
FriendRequests(sender_id INT, receiver_id INT, created_at DATE, status TEXT /* 'sent','accepted','declined' */)
DailyActiveUsers(date DATE, country TEXT, dau INT)
Events(date DATE, outage_flag INT)
Sample rows:
Users
user_id | country | signup_date
1 | US | 2025-07-15
2 | US | 2025-08-20
3 | IN | 2025-08-01
4 | BR | 2025-07-01
5 | US | 2025-08-30
Posts
post_id | user_id | post_type | created_at
10 | 1 | friend | 2025-08-31
11 | 2 | page | 2025-08-31
12 | 3 | friend | 2025-09-01
13 | 4 | event | 2025-09-01
14 | 2 | friend | 2025-09-01
Likes
like_id | user_id | post_id | created_at
100 | 1 | 12 | 2025-09-01
101 | 2 | 10 | 2025-09-01
102 | 3 | 14 | 2025-09-01
103 | 4 | 10 | 2025-08-25
104 | 5 | 12 | 2025-09-01
FriendRequests
sender_id | receiver_id | created_at | status
1 | 2 | 2025-08-20 | accepted
2 | 3 | 2025-08-28 | sent
3 | 4 | 2025-08-29 | accepted
2 | 5 | 2025-08-31 | accepted
5 | 1 | 2025-09-01 | sent
DailyActiveUsers
date | country | dau
2025-08-25 | US | 3
2025-08-25 | IN | 1
2025-08-25 | BR | 1
2025-09-01 | US | 3
2025-09-01 | IN | 1
2025-09-01 | BR | 1
Events
date | outage_flag
2025-09-01 | 0

Tasks: (a) For each country and post_type on 2025-09-01, compute Likes-per-DAU and its percent change versus the median of the same weekday over the previous 8 weeks, excluding dates where outage_flag=1. Use window functions (e.g., PERCENTILE_CONT) and ensure users are counted once per day for DAU via DailyActiveUsers. (b) Flag the top 3 countries with the largest declines (≤ −10%) and, for each, attribute the decline between new users (signup_date ≥ '2025-08-02') and existing users. Return country, post_type, pct_change, share_of_decline_from_new_users. (c) For those flagged countries, compute the Friend Request acceptance rate in the last 14 days (2025-08-19 to 2025-09-01) and compare with the prior 14 days. Output the absolute and relative change, using window functions rather than correlated subqueries. (d) Provide a high-level pandas approach (groupby, merge, rolling/expanding, quantile) mirroring your SQL. Edge cases to handle explicitly: users with multiple Likes on the same day, countries with sparse DAU, missing DAU rows, and time zones (assume UTC).

Likes-per-DAU anomaly vs same-weekday median baseline

You are investigating a KPI anomaly for the date 2025-06-01 (UTC).

Compute, for each (country, post_type) on 2025-06-01:

  1. Likes-per-DAU = (number of distinct users who liked that post_type on that date in that country) / (DAU for that country and date).
    • Important: if a user likes multiple posts of the same post_type on the same day, count that user only once for the numerator.
    • Use DailyActiveUsers for DAU (do not recompute DAU from Likes).
  2. The percent change versus the median Likes-per-DAU of the same weekday over the previous 8 weeks.
    • The previous 8 same-weekday dates for 2025-06-01 are Sundays from 2025-04-06 through 2025-05-25 (inclusive).
    • Exclude any baseline dates where Events.outage_flag = 1.
    • Use a window function median (e.g., PERCENTILE_CONT).

Output columns: country, post_type, likes_per_dau, baseline_median_likes_per_dau, pct_change_percent.

Edge cases to handle:

  • Users with multiple likes on the same day (dedupe by user/day/post_type).
  • Countries with sparse data.
  • Missing DAU rows (exclude rows where DAU is missing or 0).
  • Assume all dates are UTC (no time zone conversion needed).

Table Users

columntypeconstraints
user_idINTPRIMARY KEY, NOT NULL
countryVARCHAR(2)NOT NULL
signup_dateDATENOT NULL

Sample data:

countrysignup_dateuser_id
US2025-03-151
US2025-05-252
US2025-05-315
US2025-05-108
US2025-01-059
US2025-02-2010
IN2025-05-203
IN2025-03-206
IN2025-05-0311
IN2025-02-1013
BR2025-02-014
BR2025-05-107
BR2025-05-0312

Table Posts

columntypeconstraints
post_idINTPRIMARY KEY, NOT NULL
user_idINTNOT NULL
post_typeVARCHAR(10)NOT NULL
created_atDATENOT NULL

Sample data:

created_atpost_idpost_typeuser_id
2025-03-1010friend1
2025-03-0511page9
2025-04-0112event4
2025-03-2513friend6
2025-05-1814page3
2025-05-1015event7

Table Likes

columntypeconstraints
like_idINTPRIMARY KEY, NOT NULL
user_idINTNOT NULL
post_idINTNOT NULL
created_atDATENOT NULL

Sample data:

created_atlike_idpost_iduser_id
2025-04-061001101
2025-04-061002109
2025-04-0610031010
2025-04-131004101
2025-04-131005109
2025-04-1310061010
2025-04-201007101
2025-04-201008109
2025-04-271009101
2025-04-271010109
2025-04-2710111010
2025-05-041012101
2025-05-041013102
2025-05-041014108
2025-05-111015101
2025-05-111016102
2025-05-111017108
2025-05-181018101
2025-05-181019102
2025-05-181020108
2025-05-251021101
2025-05-251022102
2025-05-251023108
2025-06-011024101
2025-04-061025111
2025-04-061026119
2025-04-131027111
2025-04-1310281110
2025-04-201029111
2025-04-201030119
2025-04-271031111
2025-05-041032112
2025-05-041033118
2025-05-111034112
2025-05-111035118
2025-05-181036111
2025-05-181037112
2025-05-251038112
2025-05-251039118
2025-05-251040142
2025-06-011041112
2025-04-061042121
2025-04-131043121
2025-04-201044121
2025-04-271045121
2025-05-041046121
2025-05-111047121
2025-05-181048121
2025-05-251049121
2025-06-011050125
2025-04-061051136
2025-04-0610521313
2025-04-131053136
2025-04-1310541313
2025-04-201055136
2025-04-2010561313
2025-04-271057136
2025-04-2710581313
2025-05-041059136
2025-05-0410601311
2025-05-111061136
2025-05-1110621311
2025-05-181063136
2025-05-1810641311
2025-05-251065136
2025-05-2510661311
2025-06-011067136
2025-04-061068116
2025-04-131069116
2025-04-201070116
2025-04-271071116
2025-05-0410721111
2025-05-1110731111
2025-05-1810741111
2025-05-2510751111
2025-04-061076126
2025-04-131077126
2025-04-201078126
2025-04-271079126
2025-05-041080126
2025-05-111081126
2025-05-181082126
2025-05-251083126
2025-06-011084123
2025-04-061085104
2025-04-131086104
2025-04-201087104
2025-04-271088104
2025-05-0410891012
2025-05-1110901012
2025-05-1810911012
2025-05-2510921012
2025-04-061093114
2025-04-131094114
2025-04-201095114
2025-04-271096114
2025-05-0410971112
2025-05-1110981112
2025-05-1810991112
2025-05-2511001112
2025-06-011101114
2025-04-061102124
2025-04-131103124
2025-04-201104124
2025-04-271105124
2025-05-041106124
2025-05-111107124
2025-05-181108124
2025-05-251109124
2025-06-011110157

Table FriendRequests

columntypeconstraints
sender_idINTNOT NULL
receiver_idINTNOT NULL
created_atDATENOT NULL
statusVARCHAR(10)NOT NULL

Sample data:

created_atreceiver_idsender_idstatus
2025-05-0691accepted
2025-05-0719accepted
2025-05-10108accepted
2025-05-1212declined
2025-05-1521sent
2025-05-06116accepted
2025-05-12613declined
2025-05-08124accepted
2025-05-14412accepted
2025-05-2081accepted
2025-05-23109declined
2025-05-2852accepted
2025-05-3018declined
2025-05-22136declined
2025-05-27611declined
2025-05-3163sent
2025-05-2174accepted
2025-05-29712declined

Table DailyActiveUsers

columntypeconstraints
dateDATENOT NULL
countryVARCHAR(2)NOT NULL
dauINTNOT NULL

Sample data:

countrydatedau
US2025-04-0610
IN2025-04-065
BR2025-04-064
US2025-04-1310
IN2025-04-135
BR2025-04-134
US2025-04-2010
IN2025-04-205
BR2025-04-204
US2025-04-2710
IN2025-04-275
BR2025-04-274
US2025-05-0410
IN2025-05-045
BR2025-05-044
US2025-05-1110
IN2025-05-115
BR2025-05-114
US2025-05-1810
IN2025-05-185
BR2025-05-184
US2025-05-2510
IN2025-05-255
BR2025-05-254
US2025-06-0110
IN2025-06-015
BR2025-06-014

Table Events

columntypeconstraints
dateDATEPRIMARY KEY, NOT NULL
outage_flagINTNOT NULL

Sample data:

dateoutage_flag
2025-04-060
2025-04-130
2025-04-200
2025-04-270
2025-05-040
2025-05-110
2025-05-151
2025-05-180
2025-05-250
2025-06-010

Expected output

One row per (country, post_type) for 2025-06-01 with the Likes-per-DAU, the median baseline Likes-per-DAU over the previous 8 Sundays, and the percent change (NULL if baseline median is 0/NULL).

baseline_median_likes_per_daucountrylikes_per_daupct_change_percentpost_type
0.25BR0.250event
0.25BR0-100friend
0.25BR0.250page
0.2IN0.20event
0.4IN0.2-50friend
0.2IN0-100page
0.1US0.10event
0.3US0.1-66.6666666667friend
0.2US0.1-50page

Hints

  1. Aggregate likes as COUNT(DISTINCT user_id) per (date, country, post_type) to handle multiple likes by the same user.
  2. Use DailyActiveUsers as the driving table and CROSS JOIN a post_type list to ensure 0-like combinations still appear.
  3. Compute the baseline median with PERCENTILE_CONT(0.5) WITHIN GROUP and then join it back to the target-day rows.

Top declining countries and cohort attribution (new vs existing users)

Using the same KPI definition as in Question 1 for 2025-06-01:

  1. Identify the top 3 countries with the largest KPI declines (pct_change_percent <= -10%), where each country is represented by its single worst post_type (most negative pct_change_percent).
  2. For each flagged (country, post_type), attribute the decline to new users vs existing users.
    • New users are those with signup_date >= '2025-05-02'.
    • Compute a baseline median Likes-per-DAU for the new-user cohort and for all users (same weekday Sundays from 2025-04-06 to 2025-05-25).
    • For 2025-06-01, compute Likes-per-DAU for the new-user cohort and for all users.
    • Define:
        decline_total = baseline_median_total - today_total
        decline_new   = baseline_median_new   - today_new
        share_of_decline_from_new_users = decline_new / decline_total
      
      Return NULL for share if decline_total <= 0.

Output columns: country, post_type, pct_change_percent, share_of_decline_from_new_users.

Table Users

columntypeconstraints
user_idINTPRIMARY KEY, NOT NULL
countryVARCHAR(2)NOT NULL
signup_dateDATENOT NULL

Sample data:

countrysignup_dateuser_id
US2025-03-151
US2025-05-252
US2025-05-315
US2025-05-108
US2025-01-059
US2025-02-2010
IN2025-05-203
IN2025-03-206
IN2025-05-0311
IN2025-02-1013
BR2025-02-014
BR2025-05-107
BR2025-05-0312

Table Posts

columntypeconstraints
post_idINTPRIMARY KEY, NOT NULL
user_idINTNOT NULL
post_typeVARCHAR(10)NOT NULL
created_atDATENOT NULL

Sample data:

created_atpost_idpost_typeuser_id
2025-03-1010friend1
2025-03-0511page9
2025-04-0112event4
2025-03-2513friend6
2025-05-1814page3
2025-05-1015event7

Table Likes

columntypeconstraints
like_idINTPRIMARY KEY, NOT NULL
user_idINTNOT NULL
post_idINTNOT NULL
created_atDATENOT NULL

Sample data:

created_atlike_idpost_iduser_id
2025-04-061001101
2025-04-061002109
2025-04-0610031010
2025-04-131004101
2025-04-131005109
2025-04-1310061010
2025-04-201007101
2025-04-201008109
2025-04-271009101
2025-04-271010109
2025-04-2710111010
2025-05-041012101
2025-05-041013102
2025-05-041014108
2025-05-111015101
2025-05-111016102
2025-05-111017108
2025-05-181018101
2025-05-181019102
2025-05-181020108
2025-05-251021101
2025-05-251022102
2025-05-251023108
2025-06-011024101
2025-04-061025111
2025-04-061026119
2025-04-131027111
2025-04-1310281110
2025-04-201029111
2025-04-201030119
2025-04-271031111
2025-05-041032112
2025-05-041033118
2025-05-111034112
2025-05-111035118
2025-05-181036111
2025-05-181037112
2025-05-251038112
2025-05-251039118
2025-05-251040142
2025-06-011041112
2025-04-061042121
2025-04-131043121
2025-04-201044121
2025-04-271045121
2025-05-041046121
2025-05-111047121
2025-05-181048121
2025-05-251049121
2025-06-011050125
2025-04-061051136
2025-04-0610521313
2025-04-131053136
2025-04-1310541313
2025-04-201055136
2025-04-2010561313
2025-04-271057136
2025-04-2710581313
2025-05-041059136
2025-05-0410601311
2025-05-111061136
2025-05-1110621311
2025-05-181063136
2025-05-1810641311
2025-05-251065136
2025-05-2510661311
2025-06-011067136
2025-04-061068116
2025-04-131069116
2025-04-201070116
2025-04-271071116
2025-05-0410721111
2025-05-1110731111
2025-05-1810741111
2025-05-2510751111
2025-04-061076126
2025-04-131077126
2025-04-201078126
2025-04-271079126
2025-05-041080126
2025-05-111081126
2025-05-181082126
2025-05-251083126
2025-06-011084123
2025-04-061085104
2025-04-131086104
2025-04-201087104
2025-04-271088104
2025-05-0410891012
2025-05-1110901012
2025-05-1810911012
2025-05-2510921012
2025-04-061093114
2025-04-131094114
2025-04-201095114
2025-04-271096114
2025-05-0410971112
2025-05-1110981112
2025-05-1810991112
2025-05-2511001112
2025-06-011101114
2025-04-061102124
2025-04-131103124
2025-04-201104124
2025-04-271105124
2025-05-041106124
2025-05-111107124
2025-05-181108124
2025-05-251109124
2025-06-011110157

Table FriendRequests

columntypeconstraints
sender_idINTNOT NULL
receiver_idINTNOT NULL
created_atDATENOT NULL
statusVARCHAR(10)NOT NULL

Sample data:

created_atreceiver_idsender_idstatus
2025-05-0691accepted
2025-05-0719accepted
2025-05-10108accepted
2025-05-1212declined
2025-05-1521sent
2025-05-06116accepted
2025-05-12613declined
2025-05-08124accepted
2025-05-14412accepted
2025-05-2081accepted
2025-05-23109declined
2025-05-2852accepted
2025-05-3018declined
2025-05-22136declined
2025-05-27611declined
2025-05-3163sent
2025-05-2174accepted
2025-05-29712declined

Table DailyActiveUsers

columntypeconstraints
dateDATENOT NULL
countryVARCHAR(2)NOT NULL
dauINTNOT NULL

Sample data:

countrydatedau
US2025-04-0610
IN2025-04-065
BR2025-04-064
US2025-04-1310
IN2025-04-135
BR2025-04-134
US2025-04-2010
IN2025-04-205
BR2025-04-204
US2025-04-2710
IN2025-04-275
BR2025-04-274
US2025-05-0410
IN2025-05-045
BR2025-05-044
US2025-05-1110
IN2025-05-115
BR2025-05-114
US2025-05-1810
IN2025-05-185
BR2025-05-184
US2025-05-2510
IN2025-05-255
BR2025-05-254
US2025-06-0110
IN2025-06-015
BR2025-06-014

Table Events

columntypeconstraints
dateDATEPRIMARY KEY, NOT NULL
outage_flagINTNOT NULL

Sample data:

dateoutage_flag
2025-04-060
2025-04-130
2025-04-200
2025-04-270
2025-05-040
2025-05-110
2025-05-151
2025-05-180
2025-05-250
2025-06-010

Expected output

Top 3 countries (each with its single worst post_type) with pct_change_percent <= -10%. Also show the share of the decline attributed to new users (signup_date >= 2025-05-02).

countrypct_change_percentpost_typeshare_of_decline_from_new_users
IN-100page0.5
BR-100friend0.5
US-66.6666666667friend0.5

Hints

  1. To pick one post_type per country, compute pct_change at (country, post_type) granularity and then use ROW_NUMBER() over country ordered by pct_change ascending.
  2. Compute new-user KPIs by filtering Users.signup_date in the likes aggregation (keep DAU unchanged).
  3. Use two medians: one over likes_per_dau (all users) and one over likes_per_dau_new (new users), then compute share = (median_new - today_new)/(median_total - today_total).

Friend request acceptance rate change for flagged countries

For the countries flagged in Question 2 (top 3 countries with the largest declines), compute the Friend Request acceptance rate in:

  • Last 14 days: 2025-05-19 to 2025-06-01 (inclusive)
  • Prior 14 days: 2025-05-05 to 2025-05-18 (inclusive)

Definitions:

  • Attribute a friend request to the sender's country.
  • Consider only decided requests where status IN ('accepted','declined'). Ignore status='sent'.
  • acceptance_rate = accepted_count / decided_count.

Output per flagged country: country, acceptance_rate_last_14d, acceptance_rate_prior_14d, abs_change, rel_change where:

  • abs_change = last - prior
  • rel_change = (last - prior) / prior (NULL if prior is 0 or NULL)

Constraint: compute the comparison using window functions (e.g., LAG) rather than correlated subqueries.

Table Users

columntypeconstraints
user_idINTPRIMARY KEY, NOT NULL
countryVARCHAR(2)NOT NULL
signup_dateDATENOT NULL

Sample data:

countrysignup_dateuser_id
US2025-03-151
US2025-05-252
US2025-05-315
US2025-05-108
US2025-01-059
US2025-02-2010
IN2025-05-203
IN2025-03-206
IN2025-05-0311
IN2025-02-1013
BR2025-02-014
BR2025-05-107
BR2025-05-0312

Table Posts

columntypeconstraints
post_idINTPRIMARY KEY, NOT NULL
user_idINTNOT NULL
post_typeVARCHAR(10)NOT NULL
created_atDATENOT NULL

Sample data:

created_atpost_idpost_typeuser_id
2025-03-1010friend1
2025-03-0511page9
2025-04-0112event4
2025-03-2513friend6
2025-05-1814page3
2025-05-1015event7

Table Likes

columntypeconstraints
like_idINTPRIMARY KEY, NOT NULL
user_idINTNOT NULL
post_idINTNOT NULL
created_atDATENOT NULL

Sample data:

created_atlike_idpost_iduser_id
2025-04-061001101

Table FriendRequests

columntypeconstraints
sender_idINTNOT NULL
receiver_idINTNOT NULL
created_atDATENOT NULL
statusVARCHAR(10)NOT NULL

Sample data:

created_atreceiver_idsender_idstatus
2025-05-0691accepted
2025-05-0719accepted
2025-05-10108accepted
2025-05-1212declined
2025-05-1521sent
2025-05-06116accepted
2025-05-12613declined
2025-05-08124accepted
2025-05-14412accepted
2025-05-2081accepted
2025-05-23109declined
2025-05-2852accepted
2025-05-3018declined
2025-05-22136declined
2025-05-27611declined
2025-05-3163sent
2025-05-2174accepted
2025-05-29712declined

Table DailyActiveUsers

columntypeconstraints
dateDATENOT NULL
countryVARCHAR(2)NOT NULL
dauINTNOT NULL

Sample data:

countrydatedau
US2025-06-0110

Table Events

columntypeconstraints
dateDATEPRIMARY KEY, NOT NULL
outage_flagINTNOT NULL

Sample data:

dateoutage_flag
2025-06-010

Expected output

One row per flagged country showing acceptance rate in the last vs prior 14-day windows, plus absolute and relative change.

abs_changeacceptance_rate_last_14dacceptance_rate_prior_14dcountryrel_change
-0.50.51BR-0.5
-0.500.5IN-1
-0.250.50.75US-0.3333333333

Hints

  1. Label each friend request row into 'prior_14d' vs 'last_14d' using a CASE expression on created_at.
  2. Compute acceptance_rate per (country, period) using conditional sums and a NULLIF denominator.
  3. Use LAG or a pivot-style aggregation to compare last vs prior without correlated subqueries.