cswork / libraryOpen practice workspace

Coding questions · Snapchat

Compute CTR and metrics with pandas

This question evaluates proficiency in time-series and event-level data manipulation with pandas, including CTR computation, event denoising and deduplication, sessionization, cohort analysis, temporal joins, and performance-aware vectorized operations within the Data Manipulation (SQL/Python) domain.

Using pandas only, compute banner and story metrics. Assume today is 2025-09-01 and 'last 7 days' means 2025-08-26 to 2025-09-01 inclusive. You are given two DataFrames: events and users. events columns: event_id (int), user_id (int), ts (UTC datetime), event_type (string in {'banner_impression','banner_click','story_view','story_post','back_navigation'}), banner_id (nullable int), story_id (nullable int), group_id (nullable int), dwell_ms (nullable int). users columns: user_id (int), is_bot (bool), signup_dt (date). A tiny sample of events (chronologically):

1, 10, 2025-08-31T12:00:00Z, banner_impression, 7, null, null, null
2, 10, 2025-08-31T12:00:01Z, banner_click, 7, null, null, 200
3, 10, 2025-08-31T12:00:02Z, back_navigation, null, null, null, null
4, 11, 2025-08-31T13:00:00Z, banner_impression, 7, null, null, null
5, 11, 2025-08-31T13:00:05Z, banner_click, 7, null, null, 1200
6, 12, 2025-08-29T09:00:00Z, story_post, null, 101, 5, null

Tasks:

  • Define accidental clicks as banner_click events with dwell_ms < 500 OR followed by a back_navigation by the same user within 2 seconds; exclude these from CTR.
  • Compute daily per-banner CTR over the last 7 days, excluding bots (users.is_bot = True) and accidental clicks. Output: date, banner_id, impressions, valid_clicks, ctr.
  • Compute user-level 7-day CTR (same exclusions) and the distribution by signup cohort (signup_dt week).
  • For the Group Story feature, compute per-user change in average session duration between pre-period 2025-08-18..2025-08-24 and post-period 2025-08-25..2025-09-01. Define sessions as gaps of >30 minutes between any two events by the same user. Output: user_id, pre_avg_minutes, post_avg_minutes, delta_minutes, stories_posted_change.
  • Performance: write vectorized pandas code (groupby, rolling/window, merge_asof) without Python loops; ensure stable dtypes and avoid SettingWithCopy warnings. Mention any indices you would set and how you would test correctness on the sample.

Daily per-banner CTR (exclude bots and accidental clicks)

You are given two tables: events and users.

Compute daily per-banner CTR for the date range 2025-08-26 to 2025-09-01 (inclusive).

Rules:

  1. Only include events from non-bot users (users.is_bot = FALSE).
  2. Impressions are event_type = 'banner_impression'.
  3. Clicks are event_type = 'banner_click', but exclude accidental clicks.
  4. Define an accidental click as a banner_click event that satisfies either condition:
    • dwell_ms < 500, OR
    • it is followed by a back_navigation event by the same user within 2 seconds.

Output columns:

  • event_date (DATE)
  • banner_id
  • impressions
  • valid_clicks
  • ctr = valid_clicks / impressions (0 when impressions = 0)

Order results by event_date, banner_id.

Table users

columntypeconstraints
user_idINTPRIMARY KEY, NOT NULL
is_botBOOLEANNOT NULL
signup_dtDATENOT NULL

Sample data:

is_botsignup_dtuser_id
false2025-08-1010
false2025-08-2811
false2025-08-1512
true2025-08-0513

Table events

columntypeconstraints
event_idINTPRIMARY KEY, NOT NULL
user_idINTNOT NULL
tsTIMESTAMPTZNOT NULL
event_typeVARCHAR(40)NOT NULL
banner_idINT
story_idINT
group_idINT
dwell_msINT

Sample data:

banner_iddwell_msevent_idevent_typegroup_idstory_idtsuser_id
NULLNULL101story_view52012025-08-20T10:00:00Z10
NULLNULL102story_post52012025-08-20T10:05:00Z10
NULLNULL103story_view52022025-08-20T10:40:00Z10
8NULL104banner_impressionNULLNULL2025-08-27T12:00:00Z10
8700105banner_clickNULLNULL2025-08-27T12:00:05Z10
NULLNULL106story_post52032025-08-27T12:10:00Z10
NULLNULL107story_view52032025-08-27T13:00:00Z10
7NULL108banner_impressionNULLNULL2025-08-31T12:00:00Z10
7200109banner_clickNULLNULL2025-08-31T12:00:01Z10
NULLNULL110back_navigationNULLNULL2025-08-31T12:00:02Z10
NULLNULL201story_view53012025-08-23T09:00:00Z11
NULLNULL202back_navigationNULLNULL2025-08-23T09:20:00Z11
8NULL203banner_impressionNULLNULL2025-08-27T12:05:00Z11
7NULL204banner_impressionNULLNULL2025-08-31T13:00:00Z11
71200205banner_clickNULLNULL2025-08-31T13:00:05Z11
NULLNULL206story_post53022025-08-31T13:15:00Z11
NULLNULL207story_view53022025-08-31T13:50:00Z11
7NULL208banner_impressionNULLNULL2025-09-01T08:00:00Z11
7400209banner_clickNULLNULL2025-09-01T08:00:02Z11
NULLNULL301story_post54012025-08-19T15:00:00Z12
NULLNULL302story_post54022025-08-19T15:25:00Z12
7NULL303banner_impressionNULLNULL2025-08-30T10:00:00Z12
7800304banner_clickNULLNULL2025-08-30T10:00:03Z12
NULLNULL305back_navigationNULLNULL2025-08-30T10:00:04Z12
7NULL306banner_impressionNULLNULL2025-08-30T10:20:00Z12
NULLNULL307story_post54032025-08-30T10:45:00Z12
7NULL401banner_impressionNULLNULL2025-08-29T14:00:00Z13
7900402banner_clickNULLNULL2025-08-29T14:00:10Z13
NULLNULL403story_post55012025-08-29T14:20:00Z13

Expected output

One row per (date, banner_id) in 2025-08-26..2025-09-01 where there is at least one non-bot banner impression. valid_clicks excludes accidental clicks. ctr is valid_clicks/impressions.

banner_idctrevent_dateimpressionsvalid_clicks
80.52025-08-2721
702025-08-3020
70.52025-08-3121
702025-09-0110

Hints

  1. Use LEAD() to check whether a click is followed by back_navigation within 2 seconds.
  2. Compute impressions and valid clicks separately, then join on (date, banner_id).

7-day user CTR and signup-week cohort distribution

Using the same events and users tables, compute 7-day user-level CTR and then aggregate it by signup cohort week.

Date range: 2025-08-26 to 2025-09-01 (inclusive).

Use the same rules as in Question 1:

  • Exclude bot users (is_bot = FALSE).
  • Accidental clicks are clicks with dwell_ms < 500 OR followed by back_navigation within 2 seconds.

Definitions:

  • user impressions = count of banner_impression
  • user valid clicks = count of non-accidental banner_click
  • user CTR = valid_clicks / impressions (NULL if impressions = 0)
  • cohort week = DATE_TRUNC('week', signup_dt)::date

Output one row per cohort week:

  • cohort_week_start
  • user_count (number of non-bot users who had at least one impression in the 7-day window)
  • total_impressions
  • total_valid_clicks
  • avg_user_ctr (average of user CTRs)
  • p50_user_ctr (median user CTR within the cohort)

Order by cohort_week_start.

Table users

columntypeconstraints
user_idINTPRIMARY KEY, NOT NULL
is_botBOOLEANNOT NULL
signup_dtDATENOT NULL

Sample data:

is_botsignup_dtuser_id
false2025-08-1010
false2025-08-2811
false2025-08-1512
true2025-08-0513

Table events

columntypeconstraints
event_idINTPRIMARY KEY, NOT NULL
user_idINTNOT NULL
tsTIMESTAMPTZNOT NULL
event_typeVARCHAR(40)NOT NULL
banner_idINT
story_idINT
group_idINT
dwell_msINT

Sample data:

banner_iddwell_msevent_idevent_typegroup_idstory_idtsuser_id
NULLNULL101story_view52012025-08-20T10:00:00Z10
NULLNULL102story_post52012025-08-20T10:05:00Z10
NULLNULL103story_view52022025-08-20T10:40:00Z10
8NULL104banner_impressionNULLNULL2025-08-27T12:00:00Z10
8700105banner_clickNULLNULL2025-08-27T12:00:05Z10
NULLNULL106story_post52032025-08-27T12:10:00Z10
NULLNULL107story_view52032025-08-27T13:00:00Z10
7NULL108banner_impressionNULLNULL2025-08-31T12:00:00Z10
7200109banner_clickNULLNULL2025-08-31T12:00:01Z10
NULLNULL110back_navigationNULLNULL2025-08-31T12:00:02Z10
NULLNULL201story_view53012025-08-23T09:00:00Z11
NULLNULL202back_navigationNULLNULL2025-08-23T09:20:00Z11
8NULL203banner_impressionNULLNULL2025-08-27T12:05:00Z11
7NULL204banner_impressionNULLNULL2025-08-31T13:00:00Z11
71200205banner_clickNULLNULL2025-08-31T13:00:05Z11
NULLNULL206story_post53022025-08-31T13:15:00Z11
NULLNULL207story_view53022025-08-31T13:50:00Z11
7NULL208banner_impressionNULLNULL2025-09-01T08:00:00Z11
7400209banner_clickNULLNULL2025-09-01T08:00:02Z11
NULLNULL301story_post54012025-08-19T15:00:00Z12
NULLNULL302story_post54022025-08-19T15:25:00Z12
7NULL303banner_impressionNULLNULL2025-08-30T10:00:00Z12
7800304banner_clickNULLNULL2025-08-30T10:00:03Z12
NULLNULL305back_navigationNULLNULL2025-08-30T10:00:04Z12
7NULL306banner_impressionNULLNULL2025-08-30T10:20:00Z12
NULLNULL307story_post54032025-08-30T10:45:00Z12
7NULL401banner_impressionNULLNULL2025-08-29T14:00:00Z13
7900402banner_clickNULLNULL2025-08-29T14:00:10Z13
NULLNULL403story_post55012025-08-29T14:20:00Z13

Expected output

One row per signup-week cohort with aggregated 7-day user CTR metrics for non-bot users who had at least one impression in the window.

avg_user_ctrcohort_week_startp50_user_ctrtotal_impressionstotal_valid_clicksuser_count
0.52025-08-040.5211
02025-08-110201
0.33333333332025-08-250.3333333333311

Hints

  1. Compute user-level impressions and valid clicks first, then compute user CTR.
  2. Use DATE_TRUNC('week', signup_dt) for cohorts and PERCENTILE_CONT for the median.

Sessionization and pre/post change in average session duration (Group Story feature)

Using the same tables, compute per-user change in average session duration between:

  • Pre-period: 2025-08-18 to 2025-08-24 (inclusive)
  • Post-period: 2025-08-25 to 2025-09-01 (inclusive)

Session definition:

  • A user's events belong to the same session until there is a gap of more than 30 minutes between consecutive events (any event type counts).
  • Session duration = (max(ts) - min(ts)) in minutes.
  • Assign a session to pre/post based on session_start::date.

Also compute stories_posted_change for the Group Story feature:

  • Count event_type = 'story_post' with group_id IS NOT NULL in the pre-period and post-period.
  • stories_posted_change = post_count - pre_count.

Exclude bot users.

Output columns:

  • user_id
  • pre_avg_minutes
  • post_avg_minutes
  • delta_minutes = post_avg_minutes - pre_avg_minutes
  • stories_posted_change

Order by user_id.

Table users

columntypeconstraints
user_idINTPRIMARY KEY, NOT NULL
is_botBOOLEANNOT NULL
signup_dtDATENOT NULL

Sample data:

is_botsignup_dtuser_id
false2025-08-1010
false2025-08-2811
false2025-08-1512
true2025-08-0513

Table events

columntypeconstraints
event_idINTPRIMARY KEY, NOT NULL
user_idINTNOT NULL
tsTIMESTAMPTZNOT NULL
event_typeVARCHAR(40)NOT NULL
banner_idINT
story_idINT
group_idINT
dwell_msINT

Sample data:

banner_iddwell_msevent_idevent_typegroup_idstory_idtsuser_id
NULLNULL101story_view52012025-08-20T10:00:00Z10
NULLNULL102story_post52012025-08-20T10:05:00Z10
NULLNULL103story_view52022025-08-20T10:40:00Z10
8NULL104banner_impressionNULLNULL2025-08-27T12:00:00Z10
8700105banner_clickNULLNULL2025-08-27T12:00:05Z10
NULLNULL106story_post52032025-08-27T12:10:00Z10
NULLNULL107story_view52032025-08-27T13:00:00Z10
7NULL108banner_impressionNULLNULL2025-08-31T12:00:00Z10
7200109banner_clickNULLNULL2025-08-31T12:00:01Z10
NULLNULL110back_navigationNULLNULL2025-08-31T12:00:02Z10
NULLNULL201story_view53012025-08-23T09:00:00Z11
NULLNULL202back_navigationNULLNULL2025-08-23T09:20:00Z11
8NULL203banner_impressionNULLNULL2025-08-27T12:05:00Z11
7NULL204banner_impressionNULLNULL2025-08-31T13:00:00Z11
71200205banner_clickNULLNULL2025-08-31T13:00:05Z11
NULLNULL206story_post53022025-08-31T13:15:00Z11
NULLNULL207story_view53022025-08-31T13:50:00Z11
7NULL208banner_impressionNULLNULL2025-09-01T08:00:00Z11
7400209banner_clickNULLNULL2025-09-01T08:00:02Z11
NULLNULL301story_post54012025-08-19T15:00:00Z12
NULLNULL302story_post54022025-08-19T15:25:00Z12
7NULL303banner_impressionNULLNULL2025-08-30T10:00:00Z12
7800304banner_clickNULLNULL2025-08-30T10:00:03Z12
NULLNULL305back_navigationNULLNULL2025-08-30T10:00:04Z12
7NULL306banner_impressionNULLNULL2025-08-30T10:20:00Z12
NULLNULL307story_post54032025-08-30T10:45:00Z12
7NULL401banner_impressionNULLNULL2025-08-29T14:00:00Z13
7900402banner_clickNULLNULL2025-08-29T14:00:10Z13
NULLNULL403story_post55012025-08-29T14:20:00Z13

Expected output

One row per non-bot user with average session duration in minutes for pre and post periods, the delta (post - pre), and the change in Group Story posts (post_count - pre_count).

delta_minutespost_avg_minutespre_avg_minutesstories_posted_changeuser_id
0.84444444443.34444444442.5010
-16.24166666673.758333333320111
204525-112

Hints

  1. Use LAG() to compute gaps between consecutive events; start a new session when the gap is > 30 minutes.
  2. Aggregate sessions into pre vs post using session_start::date, then compute per-user averages with FILTER.