Assumptions:- Table events(user_id, event_time, event_type) where event_time is UTC timestamp.- "Active in week" = any event (or purchase) within ISO week starting Monday; week granularity aligned to business definition.- Exclude bots/test accounts via a flag or known ID list; handle cross-device via deterministic user_id (or merge on persistent_id).Approach (high level):1. Bucket each user event into a week (year-week).2. For each week, compute the set/count of unique active users.3. Compute retention: percent of users active in week N who also have any event in week N+1.SQL (window + aggregation):sql
WITH user_weeks AS (
SELECT
user_id,
DATE_TRUNC('week', event_time AT TIME ZONE 'UTC') AS week_start
FROM events
WHERE is_bot = false AND is_test = false
GROUP BY 1,2
),
week_indexed AS (
SELECT
user_id,
week_start,
LEAD(week_start) OVER (PARTITION BY user_id ORDER BY week_start) AS next_week_start
FROM user_weeks
GROUP BY user_id, week_start
),
cohort_counts AS (
SELECT
w.week_start AS cohort_week,
COUNT(DISTINCT w.user_id) AS cohort_users,
COUNT(DISTINCT CASE WHEN uw.user_id IS NOT NULL THEN w.user_id END) AS retained_next_week
FROM user_weeks w
LEFT JOIN user_weeks uw
ON w.user_id = uw.user_id
AND uw.week_start = w.week_start + INTERVAL '7 days'
GROUP BY 1
)
SELECT
cohort_week,
cohort_users,
retained_next_week,
SAFE_DIVIDE(retained_next_week, cohort_users) AS retention_rate
FROM cohort_counts
ORDER BY cohort_week;
Pandas steps:python
import pandas as pd
df['event_time'] = pd.to_datetime(df['event_time'], utc=True)
df = df[~df['is_bot'] & ~df['is_test']]
df['week_start'] = df['event_time'].dt.to_period('W').apply(lambda p: p.start_time)
user_weeks = df[['user_id','week_start']].drop_duplicates()
# build cross-join per user for next week
user_weeks['next_week'] = user_weeks['week_start'] + pd.Timedelta(days=7)
merged = user_weeks.merge(user_weeks[['user_id','week_start']], left_on=['user_id','next_week'],
right_on=['user_id','week_start'], how='left', suffixes=('','_next'))
cohorts = user_weeks.groupby('week_start').user_id.nunique().rename('cohort_users')
retained = merged.groupby('week_start').user_id_next.nunique().rename('retained_next_week')
result = pd.concat([cohorts, retained], axis=1).fillna(0)
result['retention_rate'] = result['retained_next_week'] / result['cohort_users']
Validation & edge cases:- Time zones: normalize all times to UTC or business-local timezone before bucketing; verify week boundaries against product behavior (use ISO week if needed).- Cross-device users: ensure user_id is consistent (use deterministic hashing of login/email or merge device_ids to user profiles). If only device-level IDs exist, report metric as device-retention and note limitations.- Bots/test accounts: exclude via flags, user agent heuristics, low-entropy ID detection, or activity thresholds. Run sensitivity checks: compute retention with/without suspicious accounts.- Sparse activity: users active near week boundaries—test window shifts (Sunday vs Monday) to measure sensitivity.- Missing data & deduplication: ensure dedupe of events per user-week to avoid inflated cohorts.- Statistical validation: bootstrap confidence intervals, plot weekly time series, and compare to rolling 4-week retention to detect anomalies.- Instrumentation changes: annotate metric time series with deploy/analytics changes and run backfill checks.This yields a clear, reproducible weekly retention metric and procedures to validate robustness.