A portfolio project by Hamza Mehmood
Where do new users drop off, which channels bring users who pay, how well do cohorts stick, and what happened the week activity fell? Every number below comes from a query you can read.
A user counts at a step only after reaching the one before it.
-- Signup -> workspace -> upgrade funnel. A user counts at a step only if they
-- reached the previous step first, so the numbers can only go down.
WITH step AS (
SELECT user_id, event_name, MIN(event_ts) AS ts
FROM events
WHERE event_name IN ('signed_up', 'created_workspace', 'upgraded')
GROUP BY user_id, event_name
),
journey AS (
SELECT s1.ts AS t1, s2.ts AS t2, s3.ts AS t3
FROM step s1
LEFT JOIN step s2 ON s2.user_id = s1.user_id
AND s2.event_name = 'created_workspace' AND s2.ts >= s1.ts
LEFT JOIN step s3 ON s3.user_id = s1.user_id
AND s3.event_name = 'upgraded' AND s2.ts IS NOT NULL AND s3.ts >= s2.ts
WHERE s1.event_name = 'signed_up'
),
counts AS (
SELECT 1 AS step_no, 'Signed up' AS step, COUNT(t1) AS users FROM journey
UNION ALL SELECT 2, 'Created a workspace', COUNT(t2) FROM journey
UNION ALL SELECT 3, 'Upgraded to paid', COUNT(t3) FROM journey
)
SELECT step_no,
step,
users,
ROUND(100.0 * users / FIRST_VALUE(users) OVER (ORDER BY step_no), 1) AS pct_of_signups,
ROUND(100.0 * users / LAG(users) OVER (ORDER BY step_no), 1) AS pct_of_previous_step
FROM counts
ORDER BY step_no;| channel | users | activated | upgraded | activation rate | upgrade rate | rank by upgrade |
|---|---|---|---|---|---|---|
| referral | 766 | 535 | 131 | 69.8% | 17.1% | 1 |
| organic | 2,001 | 1,219 | 201 | 60.9% | 10.0% | 2 |
| paid_search | 1,234 | 687 | 118 | 55.7% | 9.6% | 3 |
| paid_social | 999 | 344 | 34 | 34.4% | 3.4% | 4 |
-- Which acquisition channel brings users who actually convert?
-- Ranked by upgrade rate (upgraded users / all signups from that channel).
WITH per_user AS (
SELECT u.user_id,
u.channel,
MAX(e.event_name = 'created_workspace') AS activated,
MAX(e.event_name = 'upgraded') AS upgraded
FROM users u
LEFT JOIN events e ON e.user_id = u.user_id
GROUP BY u.user_id, u.channel
),
by_channel AS (
SELECT channel,
COUNT(*) AS users,
SUM(activated) AS activated,
SUM(upgraded) AS upgraded
FROM per_user
GROUP BY channel
)
SELECT channel,
users,
activated,
upgraded,
ROUND(100.0 * activated / users, 1) AS activation_rate,
ROUND(100.0 * upgraded / users, 1) AS upgrade_rate,
RANK() OVER (ORDER BY 1.0 * upgraded / users DESC) AS rank_by_upgrade
FROM by_channel
ORDER BY rank_by_upgrade, channel;This is an association. People who invite teammates may already be the more committed ones, so the right next step is an experiment (nudge inviting, then compare), not a conclusion.
-- Among users who created a workspace: do those who invited a teammate
-- upgrade more often? (An association, not proof that inviting causes upgrades.)
WITH activated AS (
SELECT DISTINCT user_id FROM events WHERE event_name = 'created_workspace'
),
flags AS (
SELECT a.user_id,
EXISTS (SELECT 1 FROM events e
WHERE e.user_id = a.user_id AND e.event_name = 'invited_teammate') AS invited,
EXISTS (SELECT 1 FROM events e
WHERE e.user_id = a.user_id AND e.event_name = 'upgraded') AS upgraded
FROM activated a
)
SELECT CASE invited WHEN 1 THEN 'Invited a teammate' ELSE 'Did not invite' END AS segment,
COUNT(*) AS users,
SUM(upgraded) AS upgraded,
ROUND(100.0 * SUM(upgraded) / COUNT(*), 1) AS upgrade_rate
FROM flags
GROUP BY invited
ORDER BY invited DESC;The activity drop and rebound lines up with the planted bad release. Signups end on 29 June, so the last week falls for that reason and should not be read as churn.
-- Weekly active users (anyone with a session) and week-over-week change.
WITH weekly AS (
SELECT date(event_ts, 'weekday 0', '-6 days') AS week_start,
COUNT(DISTINCT user_id) AS wau
FROM events
WHERE event_name = 'session'
GROUP BY week_start
)
SELECT week_start,
wau,
ROUND(100.0 * (wau - LAG(wau) OVER (ORDER BY week_start))
/ LAG(wau) OVER (ORDER BY week_start), 1) AS wow_pct
FROM weekly
ORDER BY week_start;Share of each signup-week cohort with at least one session N weeks later. Look for the diagonal dip in the incident week.
| Cohort | Users | W0 | W1 | W2 | W3 | W4 | W5 | W6 | W7 | W8 | W9 | W10 | W11 | W12 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 2025-01-06 | 198 | 100 | 36 | 30 | 25 | 24 | 23 | 22 | 19 | 22 | 18 | 18 | 16 | 9 |
| 2025-01-13 | 199 | 100 | 38 | 30 | 29 | 30 | 23 | 23 | 24 | 19 | 17 | 19 | 18 | 14 |
| 2025-01-20 | 198 | 100 | 36 | 28 | 33 | 33 | 25 | 28 | 22 | 20 | 22 | 20 | 18 | 21 |
| 2025-01-27 | 195 | 100 | 33 | 22 | 27 | 25 | 23 | 27 | 23 | 20 | 19 | 12 | 18 | 16 |
| 2025-02-03 | 207 | 100 | 34 | 35 | 31 | 30 | 25 | 25 | 22 | 25 | 19 | 19 | 14 | 12 |
| 2025-02-10 | 190 | 100 | 36 | 34 | 31 | 32 | 24 | 26 | 22 | 19 | 22 | 21 | 18 | 15 |
| 2025-02-17 | 189 | 100 | 35 | 32 | 36 | 28 | 25 | 22 | 25 | 23 | 17 | 25 | 20 | 7 |
| 2025-02-24 | 203 | 100 | 36 | 32 | 34 | 27 | 24 | 26 | 21 | 22 | 17 | 20 | 11 | 11 |
| 2025-03-03 | 200 | 100 | 44 | 38 | 32 | 28 | 36 | 24 | 24 | 17 | 22 | 8 | 25 | 18 |
| 2025-03-10 | 228 | 100 | 39 | 33 | 32 | 32 | 25 | 25 | 24 | 21 | 9 | 19 | 13 | 18 |
| 2025-03-17 | 180 | 100 | 31 | 29 | 28 | 26 | 24 | 23 | 18 | 11 | 21 | 21 | 12 | 17 |
| 2025-03-24 | 200 | 100 | 34 | 30 | 31 | 28 | 28 | 22 | 13 | 20 | 16 | 15 | 15 | 16 |
| 2025-03-31 | 191 | 100 | 37 | 31 | 34 | 28 | 37 | 15 | 27 | 21 | 23 | 17 | 20 | 18 |
| 2025-04-07 | 208 | 100 | 32 | 24 | 33 | 31 | 14 | 21 | 17 | 18 | 23 | 16 | 17 | 11 |
| 2025-04-14 | 221 | 100 | 32 | 29 | 25 | 16 | 23 | 21 | 21 | 20 | 20 | 17 | 16 | |
| 2025-04-21 | 222 | 100 | 34 | 34 | 17 | 27 | 28 | 27 | 24 | 21 | 21 | 20 | ||
| 2025-04-28 | 195 | 100 | 35 | 20 | 32 | 34 | 22 | 20 | 22 | 18 | 22 | |||
| 2025-05-05 | 194 | 100 | 19 | 34 | 27 | 29 | 31 | 25 | 18 | 19 | ||||
| 2025-05-12 | 209 | 100 | 32 | 36 | 32 | 26 | 28 | 22 | 20 | |||||
| 2025-05-19 | 201 | 100 | 38 | 31 | 28 | 35 | 27 | 26 | ||||||
| 2025-05-26 | 187 | 100 | 36 | 36 | 34 | 34 | 26 | |||||||
| 2025-06-02 | 196 | 100 | 39 | 31 | 26 | 33 | ||||||||
| 2025-06-09 | 217 | 100 | 36 | 40 | 29 | |||||||||
| 2025-06-16 | 161 | 100 | 34 | 34 | ||||||||||
| 2025-06-23 | 211 | 100 | 33 |
-- Weekly cohort retention. A cohort is everyone who signed up in the same
-- Monday-to-Sunday week; "retained in week N" means at least one session
-- N weeks after the cohort week started. Weeks that have not happened yet
-- (relative to the last event in the data) are left out, not shown as zero.
WITH cohort AS (
SELECT user_id, date(event_ts, 'weekday 0', '-6 days') AS cohort_week
FROM events
WHERE event_name = 'signed_up'
),
data_end AS (
SELECT MAX(date(event_ts)) AS last_day FROM events
),
sizes AS (
SELECT cohort_week, COUNT(*) AS cohort_size
FROM cohort
GROUP BY cohort_week
),
weeks(n) AS (
SELECT 0
UNION ALL
SELECT n + 1 FROM weeks WHERE n < 30
),
grid AS (
SELECT s.cohort_week, s.cohort_size, w.n AS week_no
FROM sizes s
CROSS JOIN weeks w
CROSS JOIN data_end d
WHERE date(s.cohort_week, '+' || (w.n * 7) || ' days') <= d.last_day
),
active AS (
SELECT c.cohort_week,
CAST((julianday(date(e.event_ts)) - julianday(c.cohort_week)) / 7 AS INTEGER) AS week_no,
COUNT(DISTINCT e.user_id) AS retained
FROM events e
JOIN cohort c ON c.user_id = e.user_id
WHERE e.event_name = 'session'
GROUP BY c.cohort_week, week_no
)
SELECT g.cohort_week,
g.cohort_size,
g.week_no,
COALESCE(a.retained, 0) AS retained,
ROUND(100.0 * COALESCE(a.retained, 0) / g.cohort_size, 1) AS retention_pct
FROM grid g
LEFT JOIN active a ON a.cohort_week = g.cohort_week AND a.week_no = g.week_no
ORDER BY g.cohort_week, g.week_no;