learn-product-analytics-with-phoebe / Analyst session 4 of 8
Learn Product Analytics with Phoebe · Analyst track · Session 4 of 8

Hunting the activation moment

An activation moment is a claim: people who do X early come back more than people who do not. The claim is cheap to make and cheap to test, and most teams only ever do the first half. Tonight we load a cohort of 8,348 real new GitHub repositories, write down every early behaviour we can count, sweep the thresholds, and read three numbers per candidate: how many it calls activated, how much more they came back, and how many of the returners it caught. Then we face the part the numbers cannot settle: a behaviour that predicts return is not a behaviour you can push people into.

🟡 Core Product and data analysts Python, standard library only 45 min
0-3 · Welcome 3-18 · Candidates, lift, cause 18-40 · Build-along: sweep the cohort 40-45 · Q&A
Part 0

Where we start

Session 3 settled what "active" means. Tonight's question is earlier in a life: is there something a new project does in its first hours or days that separates the ones that come back from the ones that do not? GH Archive cannot see sign-ups, so the unit is a birth it can see: a public repository created in one hour, 2024-03-04 15:00 UTC, the first new repository of each non-bot account. 8,348 of them. Their first seven days were sampled every second hour, and "came back" means at least one non-bot event in the sampled hours of days 28 to 41, weeks five and six. 325 of 8,348 came back, 3.9 percent. Most new projects are never touched again, and that is the first finding before any definition is written.

🎫 Clear all three quiz questions to stamp your passport Live = presented in the 45 minutes Self-study = the full depth, after class ★ Build-along = run the code yourself

★ What you walk out with tonight. A candidate matrix of six behaviours by three windows, one function that scores any cell of it against the same outcome, the eight preset rows the bench in session 8 uses, and a sweep loop you run yourself. Plus the sentence that keeps the whole exercise honest: "seen on two days" predicts return at 5.44x because it is a small copy of return, not because it can be assigned.

Part 1 · covers the candidate list and the outcome

Write down every early behaviour you can count

5 min live

A hunt needs a list. Six behaviours can be counted for every repository in its early life: events of any kind, pushes of code, separate days seen, issues or pull requests opened, different people active, and stars received. Three windows: the birth hour, the first day, the first seven days. That is eighteen cells, each of which becomes a candidate once you pick a threshold, and every one of them is scored against the same outcome, came back in weeks five and six. Eight cells have been scored and checked twice, once in Python and once in the browser engine, and they agree on every count. The rest are yours to run tonight.

SIX BEHAVIOURS, THREE WINDOWS, ONE OUTCOME lift = return rate of the activated / return rate of the rest came back wk 5-6: 325 of 8,348 the birth hour the first day the first 7 days events of any kind 10+ · 0.88x 2+ · 1.49x · 5+ · 1.58x try it 3+ · 2.93x pushes of code try it 1+ · 1.74x 1+ · 2.84x separate days seen always 1, by construction always 1, by construction 2+ · 5.44x issues or PRs opened try it try it 1+ · 4.01x different people active try it try it 2+ · 5.45x stars received try it try it 1+ · 3.35x Eight cells are checked canon, agreed by two engines. A lift is a correlation on one hour's cohort; the pale cells are the sweep you run tonight.
🔍 Click to zoom - the grid is the whole hunt: pick a cell, pick a threshold, score it against the same 325 returners
LiveThe outcome comes first, and it is fixed2 min▶

Every candidate is scored against one outcome that never moves: a non-bot event on the repository in the sampled hours of days 28 to 41. Fixing it before the hunt is what stops the hunt from cheating. If you let the outcome drift with each candidate, "pushed on day one" gets scored against "pushed again in week five" and "opened an issue" against "issues in week five", and every candidate looks like the one. One outcome, one denominator of 8,348, one count of 325 returners, and every definition has to earn its lift against the same bar.

The 3.9 percent is low and that is expected: public repositories are cheap to create and most are a test, a fork of a tutorial or a one-off script. A workspace-based product would see a higher base rate, but the shape of the hunt is identical: a birth event the stream can see, a short early window, a later window that says "still here".

Self-studyWhat the six behaviours can and cannot see3 min▶

Each count is "seen in the sampled hours": every second hour of the first seven days, 84 files, then every second hour of days 28 to 41, 168 files. A push at 15:30 in a sampled hour is seen; a push at 16:30 is not. That undercounts every behaviour equally, so lifts between candidates stay comparable while every absolute count is a floor. Events by [bot] logins are dropped everywhere; bots without the suffix cannot be told apart from people by name and stay in.

"Separate days seen" is only meaningful in the seven-day window: in the birth hour and the first day it is 1 for every repository that has any event at all, so those two cells are not candidates and the grid says so. Stars are the odd one out in the list, and Part 3 says why.

Real world

Slack's 2,000 messages. Stewart Butterfield told First Round Review in 2015 that a team which has exchanged 2,000 messages has "really tried" Slack, and that 93 percent of such teams were still using it. Read the sentence the way tonight's grid reads a cell: the threshold was chosen "based on experience of which companies stuck with us and which didn't", and a team that reaches 2,000 messages has, by definition, already stayed long enough to send them. It is a real finding about the teams that kept going and a weak one about what makes a new team keep going. The 93 percent is the return rate of the activated; the number the interview does not give is the return rate of the rest, which is what a lift needs.

Part 2 · covers lift, difference and returners caught

Three numbers per candidate, and none of them is enough alone

6 min live

Take "pushed code on the first day". 2,779 repositories did, 33.3 percent of the cohort. Of those, 151 came back, 5.4 percent. Of the other 5,569, 174 came back, 3.1 percent. Lift is the ratio, 1.74x. The difference is 2.3 points. And of all 325 returners, this definition caught 151, 46.5 percent. Move the same behaviour to a seven-day window and every number changes: 3,268 activated, 6.4 percent came back against 2.3 percent, lift 2.84x, 64.6 percent of returners caught. Same behaviour, different threshold, a different story. That is why a single hand-picked definition is a guess, and a sweep is the job.

EIGHT PRESETS: HOW MANY YOU CALL ACTIVATED, AGAINST HOW MUCH THEY RETURN lift 6x 5x 4x 3x 2x 1x 0% 10% 20% 30% 40% 50% share of the 8,348 called activated ppl2 · 5.45x · caught 16.3% days2 · 5.44x · caught 47.4% work1 · 4.01x · caught 9.5% star1 · 3.35x · caught 7.4% vol3 · 2.93x · caught 71.4% push7 · 2.84x · caught 64.6% push1 · 1.74x · caught 46.5% burst10 · 0.88x · caught 1.2% · 117 rows, small shuffled lifts 0.85x to 1.33x Up and left is the frontier: few called activated, high lift. Right is breadth: most returners caught, less signal per head.
🔍 Click to zoom - no point wins on all three numbers; the choice between days2 and vol3 is a decision about what the definition is for
LiveReading four canon rows4 min▶

push1, pushed on day one. 2,779 activated, 33.3 percent. 151 came back, 5.4 percent, against 174 of 5,569, 3.1 percent. Lift 1.74x, difference 2.3 points, 46.5 percent of returners caught. The definition most teams write first, and it is barely better than the base rate.

push7, pushed in week one. 3,268 activated, 39.1 percent. 210 came back, 6.4 percent, against 115 of 5,080, 2.3 percent. Lift 2.84x, difference 4.2 points, 64.6 percent caught. A wider window catches more returners and separates them better, because a push on day four is a return before the return window opens.

days2, seen on two or more days in week one. 1,186 activated, 14.2 percent. 154 came back, 13.0 percent, against 171 of 7,162, 2.4 percent. Lift 5.44x, difference 10.6 points, 47.4 percent caught. Far better per head than either push rule, at the cost of missing half the returners.

ppl2, a second person in week one. 288 activated, 3.4 percent, a small group. 53 came back, 18.4 percent, against 272 of 8,060, 3.4 percent. Lift 5.45x, difference 15.0 points, 16.3 percent caught. The highest return rate in the table and the smallest reach; with 288 rows the lift is real but the second decimal is not worth arguing over.

Self-studyWhy a sweep, not a pick3 min▶

The three numbers pull in different directions. Raising a threshold makes the activated group smaller and usually purer, so lift rises and returners caught falls. Lowering it does the opposite. "Three or more events in week one" calls 46.0 percent of the cohort activated and catches 71.4 percent of returners at 2.93x; "seen on two days" calls 14.2 percent and catches 47.4 percent at 5.44x. Neither is wrong. A definition meant to trigger an onboarding nudge wants reach, so the broad one; a definition meant to flag which new projects are worth a human's attention wants purity, so the narrow one.

The sweep also shows you what the data does not do. The brief for the bench expected "total events" to reverse at every window, a pure volume trap. The rows do not agree: volume reverses only in the birth hour. Ten or more events in the birth hour, 117 repositories, has a lift of 0.88x with 4 returners, while 2 or more in the same hour is 1.49x and 5 or more is 1.58x. A sweep prints what is there, and what is there is the lesson.

Real world

Facebook's 7 friends in 10 days, and what it hides. The famous line is reported: a transcript of Chamath Palihapitiya's 2012 talk has him saying the biggest thing the growth team realised was to get any individual to 7 friends in 10 days. Nothing about the analysis behind it was ever published. Andrew Chen is reported, via Mixpanel's write-up, to have said the number could as easily have been 10 in 12 or 5 in 1, and Benn Stancil at Mode called it a rally cry rather than a threshold. Tonight's scatter is the reason those caveats matter: on this cohort, the neighbouring thresholds of one behaviour sit at 1.74x and 2.84x. A single cell quoted without its neighbours is a slogan, and a slogan is not a definition.

Part 3 · covers correlation is not cause

The best predictor is usually the outcome arriving early

4 min live

Look at what won. "Seen on two or more days" is 5.44x because being seen on a second day in week one is a small return, measured before the return window opens. "A second person showed up" is 5.45x because a project two people touch is already a different kind of project. "Received a star" is 3.35x and the owner did not do it; someone else did. None of these is a button. A lift tells you which early rows look like the late rows. It does not tell you that making a quiet project look like a busy one will make it come back.

BEFORE · a predictor read as a lever AFTER · a predictor read as a signal days2 · lift 5.44x nudge every new project back on day 2 expect 5.44x more returns? never promised the lift compares rows that already differed day 0 · born day 2 · seen again weeks 5-6 · came back the predictor is a small early copy of the outcome star1 · given by someone else the owner cannot do it; 3.35x is about the project, not a step to prescribe to prove a lever: run an experiment A lift is a correlation on one cohort. The claim "pushing people into X raises return" needs a random assignment, which is another course.
🔍 Click to zoom - the left panel is the sentence a product review will say out loud; the right panel is what the rows actually support
LiveThree ways a strong predictor fools you3 min▶

It is the outcome, early. Coming back on a second day and coming back in week five are the same behaviour at two distances. Of course they correlate. The right reading is "the projects that will return start returning immediately", which is useful for forecasting and useless as an instruction.

Someone else did it. A star is given by another account. A second person showing up is, by definition, not the owner. Both say "this project matters to more than one person", a property of the project, not an action you can ask the owner to take.

The group is small. 288 repositories for ppl2, 214 for work1, 194 for star1, 117 for burst10. The bench's break button shuffles each definition across the cohort with a fixed seed so the group keeps its size and loses its link to the repository; the shuffled lifts land between 0.85x and 1.33x, and the small groups wander furthest from 1. A 3.35x on 194 rows is a real signal; a 1.3x on 117 rows is indistinguishable from nothing.

To move from "predicts" to "causes", assign the behaviour at random and measure return in both arms. That is the whole topic of Experimentation, and it is the only route.

Self-studyWhat you can honestly do with a predictor2 min▶

A predictor without a cause is still worth having. It scores new projects on day seven so a team can look at the ones likely to stay, it gives a leading indicator that moves weeks before the retention number does, and it names a candidate for the experiment. What it cannot do is set the target: "get 40 percent of new projects to a second day" turns a measurement into a quota, and a quota changes the behaviour it measures. Write the definition down with its lift, its reach and its group size, and label it a signal until an experiment says otherwise.

Real world

The busy birth hour. Ten or more events in the first hour looks like enthusiasm. In this cohort it is 117 repositories, 4 of which came back, lift 0.88x. The count is noise-sized, so the honest claim is "no signal", never "harm". What the row is showing is setup: a template pushed, branches created, a workflow firing, a lot of activity that is all one act. A common pattern in workspace products is the same: an import that creates fifty objects in a minute reads as a power user on any volume metric and is one click. Setup activity is not engagement, and a sweep that includes the birth hour is how you find out where your product's setup burst ends.

Build-along

Score the cohort, reproduce the canon, then sweep

★ 22 min · everyone builds

Standard-library Python and one file, the same cohort the bench in session 8 runs in the browser. The file is real GH Archive rows, one entry per repository, already reduced to the six behaviours in three windows plus the outcome; it is 285,995 bytes. Every number in the comments below is what this code prints. The sweep at the end prints far more rows than fit on a page, so it carries no comments: compare your printout with the bench.

FIVE STEPS, ONE FILE, TWO ENGINES THAT AGREE 1 · download pa-sample.js 2 · strip the JS wrapper 3 · json.loads 17 arrays, 1 meta 4 · evaluate (feature, window, k) 5 · canon, then sweep compare with the bench 285,995 bytes 8,348 rows · 325 back one function, any cell 8 rows agreed materials/pa_reference.py and assets/pa-bench.js agree on every count of the eight presets. Your printout should match both.
🔍 Click to zoom - step 4 is the whole method; steps 1 to 3 are plumbing and step 5 is where you learn something

Download the cohort and strip the wrapper. The file is JavaScript that assigns one object to window.PA_SAMPLE. Cut the assignment and the trailing semicolon and what is left is JSON.

★ Step 1 · one file, then JSONimport json, urllib.request URL = "https://phoebefu6.github.io/learn-product-analytics-with-phoebe/assets/pa-sample.js" raw = urllib.request.urlopen(URL).read().decode("utf-8") head = "window.PA_SAMPLE = " body = raw[raw.index(head) + len(head):].rstrip().rstrip(";") S = json.loads(body) N, RET = S["meta"]["n"], S["ret"] print(sorted(S)) # ['days_w7', 'ev_w0', 'ev_w1', 'ev_w7', 'meta', 'ppl_w0', 'ppl_w1', 'ppl_w7', # 'push_w0', 'push_w1', 'push_w7', 'ret', 'star_w0', 'star_w1', 'star_w7', # 'work_w0', 'work_w1', 'work_w7'] print(N, sum(RET), round(100 * sum(RET) / N, 1)) # 8348 325 3.9

Write evaluate. One function scores any cell of the grid at any threshold: flag the rows at or above k, count who came back on each side, divide. "Separate days" only exists as an array for the seven-day window; in the shorter windows it is 1 for any row with an event, which the helper derives so the sweep can still ask.

★ Step 2 · evaluate(feature, window, k)def column(f, w): if f == "days" and w != "w7": return [1 if x > 0 else 0 for x in S["ev_" + w]] return S[f + "_" + w] def evaluate(f, w, k): flag = [x >= k for x in column(f, w)] a = sum(flag) # called activated back_a = sum(r for r, g in zip(RET, flag) if g) # of those, came back back_r = sum(RET) - back_a # of the rest, came back lift = (back_a / a) / (back_r / (N - a)) return a, back_a, N - a, back_r, lift

Print the eight canon rows. These are the bench presets, and the comments are what the code prints. The same eight counts come out of the browser engine.

★ Step 3 · the canon, reproducedPRESETS = [("push1", "push", "w1", 1), ("push7", "push", "w7", 1), ("days2", "days", "w7", 2), ("ppl2", "ppl", "w7", 2), ("work1", "work", "w7", 1), ("star1", "star", "w7", 1), ("vol3", "ev", "w7", 3), ("burst10", "ev", "w0", 10)] for pid, f, w, k in PRESETS: a, ba, r, br, lift = evaluate(f, w, k) print(f"{pid:8}activated {a:5} ({100*a/N:4.1f}%) back {ba:3} = {100*ba/a:4.1f}%" f" rest {br:3}/{r} = {100*br/r:3.1f}% lift {lift:.2f}x caught {100*ba/sum(RET):4.1f}%") # push1 activated 2779 (33.3%) back 151 = 5.4% rest 174/5569 = 3.1% lift 1.74x caught 46.5% # push7 activated 3268 (39.1%) back 210 = 6.4% rest 115/5080 = 2.3% lift 2.84x caught 64.6% # days2 activated 1186 (14.2%) back 154 = 13.0% rest 171/7162 = 2.4% lift 5.44x caught 47.4% # ppl2 activated 288 ( 3.4%) back 53 = 18.4% rest 272/8060 = 3.4% lift 5.45x caught 16.3% # work1 activated 214 ( 2.6%) back 31 = 14.5% rest 294/8134 = 3.6% lift 4.01x caught 9.5% # star1 activated 194 ( 2.3%) back 24 = 12.4% rest 301/8154 = 3.7% lift 3.35x caught 7.4% # vol3 activated 3839 (46.0%) back 232 = 6.0% rest 93/4509 = 2.1% lift 2.93x caught 71.4% # burst10 activated 117 ( 1.4%) back 4 = 3.4% rest 321/8231 = 3.9% lift 0.88x caught 1.2%

Check the birth-hour neighbours. The two thresholds either side of burst10 show that volume reverses only at the top of the birth hour, not across it.

★ Step 4 · the neighbours of the anti-presetfor k in (2, 5): a, ba, r, br, lift = evaluate("ev", "w0", k) print(k, round(100 * a / N, 1), round(lift, 2)) # 2 69.5 1.49 # 5 10.8 1.58

Sweep every cell. Six behaviours, three windows, thresholds 1 to 10, skipping a cell once nobody is left in it. This prints well over a hundred rows and none of them is quoted here: compare your printout with the bench, and bring the row you would defend to session 8.

★ Step 5 · the sweep - compare your printout with the benchFEATURES = ["ev", "push", "days", "work", "ppl", "star"] WINDOWS = ["w0", "w1", "w7"] for f in FEATURES: for w in WINDOWS: if f == "days" and w != "w7": continue # always 1, not a candidate for k in range(1, 11): a = sum(x >= k for x in column(f, w)) if a == 0: break # nobody left in the cell if a == N: continue # everybody qualifies, no rest to compare a, ba, r, br, lift = evaluate(f, w, k) if br == 0: continue # every returner is inside; no rate for the rest small = " (small)" if a < 300 else "" print(f"{f:5}{w} k>={k:2} activated {a:5} lift {lift:.2f}x" f" caught {100*ba/sum(RET):4.1f}%{small}") # compare your printout with the bench: the eight canon rows must match it exactly, # every other row is yours; flag any group under 300 as small before quoting its lift

Print the group size next to every lift. A sweep hands you a hundred lifts and the highest one is nearly always on the smallest group. The bench flags groups under 300 as small; do the same in your own printout, and read the shuffled band in Part 2 before you get excited about a 1.3x.

Homework

Before session 5

30 min
  1. Pick the row you would defend. From your sweep, choose one definition for "this new project is worth a human's attention" and one for "this new project should get the onboarding nudge". Write each as a sentence with its activated share, lift, returners caught and group size. Bring both to session 8; the bench will score them.
  2. Find the setup burst. In the birth-hour column, print lift for every threshold from 1 to 10 of events of any kind and write down where it stops rising. That is where setup ends in this cohort; describe what the same boundary would look like in a product you know.
  3. Argue the other side. For your strongest row, write three sentences: one explaining it as the outcome arriving early, one as something someone other than the owner did, one as a real lever. Say which you believe and what experiment would settle it.
  4. Keep the file. Session 5 goes back to accounts rather than repositories and builds the power-user curve from real rows; the same standard-library pattern loads that data too.
Check yourself

Three questions

Clear all three and analyst session 4 gets stamped in your passport.

1 · "Pushed code in week one" catches 64.6 percent of returners at 2.84x; "seen on two or more days" catches 47.4 percent at 5.44x. Which is the better activation definition?

Lift, difference and returners caught pull in different directions, and a threshold moves all three at once. The sweep exists so the choice is made with all three numbers in view and named for the decision it serves.

2 · "Seen on two or more days in week one" has a lift of 5.44x on this cohort. What does that support?

A lift compares rows that already differed. Coming back on day two and coming back in week five are the same behaviour at two distances; proving that pushing people into the first changes the second needs random assignment.

3 · Ten or more events in the birth hour: 117 repositories, 4 came back, lift 0.88x. Its shuffled version reads 1.32x. What is the honest sentence?

The shuffle keeps the group size and removes the link, and small groups land furthest from 1.0x. A 0.88x and a 1.32x on 117 rows are the same finding: nothing measurable. "Harm" would need a group large enough to be sure, and this one is not.

Sources covered

What this session teaches, and where it came from

✓The cohort: 8,348 new repositories, 325 came backMeasured from GH Archive hourly files (https://www.gharchive.org/), cohort hour 2024-03-04 15:00 UTC, by build-activation-sample.py; the bench page explains the cohort decision
✓Every count in the eight canon rowsMeasured; pa_reference.py and the browser engine agree on every count, 8 presets x 2
✓Birth-hour neighbours: 2+ events 69.5 percent at 1.49x, 5+ 10.8 percent at 1.58xMeasured with the same rules, exploration log 2026-09-29
✓Shuffled lifts 0.85x to 1.33xMeasured; seeded shuffle, mulberry32 and Fisher-Yates, seed 20240304, the bench's break button
✓Slack's 2,000 messages and 93 percentButterfield, S., interviewed in First Round Review (2015). "From 0 to $1B - Slack's Founder Shares Their Epic Launch Strategy." Read at source; taught with its survivorship problem
◐Facebook's 7 friends in 10 daysReported: Palihapitiya, C. (2012), "How we put Facebook on the path to 1 billion users" (talk), via transcript excerpts; Stancil, B. (2015), Mode; Andrew Chen's "10 in 12" reported via Mixpanel. Video not watched; the analysis behind it was never published

Every fact on this page, and its verification tier, is recorded in the course's source map.

Analyst session 4 cheat sheet · pin this

Fix the outcome firstOne later window, one denominator. 325 of 8,348 came back, 3.9 percent.
Six behaviours x three windowsEvery cell plus a threshold is a candidate. Score them all with one function.
Three numbers per rowLift, difference in points, returners caught. A threshold moves all three.
Sweep, never pickpush1 1.74x and push7 2.84x are neighbours; a single cell quoted alone is a slogan.
Predictor is not leverdays2 5.44x is the outcome arriving early; star1 3.35x is done by others. An experiment proves a lever.
Print the group sizeUnder 300 is small. Shuffled lifts land 0.85x to 1.33x; burst10 at 0.88x on 117 rows is no signal.