Where we start
Sessions 1 and 2 worked on one hour. Tonight needs time, because "active" is a word about repetition: a person seen on one day and again on another. So the dataset changes shape. Instead of every event in one hour, we follow a fixed set of accounts, every account whose GitHub id is divisible by 20, across two hours a day (03:00 and 15:00 UTC) for the 28 days from 2024-03-04 to 2024-03-31. That is 56 hourly files streamed and thrown away, keeping only who was seen and what they did. 79,555 accounts appear at least once; 98 of them carry the [bot] suffix and are dropped. Everything on this page is computed from that sample, and every count is "seen in the sampled hours", never "active on GitHub".
★ What you walk out with tonight. A script that turns a month of sampled events into per-day "seen" sets under three definitions of active, prints MAU, average DAU and average WAU for each, and the two ratios (DAU/MAU, DAU/WAU) that boards ask for. Plus the argument you will need the first time someone changes a definition and the stickiness number stays flat: 79,555 / 48,946 / 8,993 monthly actives, 6.4 / 6.6 / 5.0 percent DAU/MAU, same accounts, same month.
Three definitions, three monthly actives
5 min liveHold the accounts fixed and change only the verb. "Active" as any event at all gives 79,555 monthly actives, which is every account in the sample by construction. "Active" as pushed code gives 48,946. "Active" as opened an issue or a pull request gives 8,993. Nothing about the users changed between those three sentences; a definition did. Whoever writes the definition is choosing the number, and the choice should be written down next to the number every time it is reported.
LiveActive is a verb, not a status3 min▶
In the script, each definition is one test on the per-hour record of an account: any is "at least one event", push is "at least one PushEvent", open is "at least one issue or pull request opened". An account is active on a day under a definition if the test passes in either sampled hour of that UTC day. The three tests are one line each, and that line is the whole product decision. "Any event" includes starring someone else's repository and commenting on a thread; "pushed code" is closer to the work; "opened an issue or PR" is the narrowest, the moment an account asks the project for something or offers it something.
None of the three is the right one. The right one depends on the question. A capacity planner wants "any event" because servers do not care about intent. A team asking "is our product being used for its purpose" wants the narrow verb, and has to accept a number one ninth the size. Reporting the wide number to answer the narrow question is the most common quiet lie in product analytics, and it is rarely told on purpose.
Self-studyWhy 79,555 is not "GitHub's MAU"2 min▶
Two reasons, both worth saying out loud whenever the number is quoted. First, the sample: only accounts whose id is divisible by 20, so about one account in twenty, kept deterministically so the same accounts are followed through every hour. Second, the clock: two hours out of 24, every day. An account that works from 09:00 to 17:00 Central European time and never touches GitHub at 03:00 or 15:00 UTC does not exist in this dataset. 79,555 is "accounts in the sample seen in the sampled hours during March 2024", and the build-along prints it with that sentence attached. Part 3 is about what the two-hour clock does to every count on the page.
The 98 dropped accounts are the ones whose login ends in [bot]. Automation that does not label itself stays in the sample, exactly as in session 2's hour, where the busiest account had no suffix at all. Session 5 goes looking for it in the right tail of the days-active histogram.
Two decks, two MAUs, one product. A common pattern: the growth team's deck counts a user active if they opened the app, because the login event is the one that is never missing. The product team's deck counts a user active if they completed the core action, because that is what the roadmap is trying to move. Both numbers are correct. Then a board member reads both decks in the same afternoon and asks why the company cannot count its own users. The fix is not to pick a winner; it is a one-line definition printed under every chart, with the verb in it.
The ratio that hides the choice
6 min liveStickiness, DAU divided by MAU, is the number that survives every meeting. Here it is for the three definitions: 6.4 percent, 6.6 percent, 5.0 percent. The monthly count moved ninefold between the first and third definition; the ratio moved by less than two points. A reader given only the ratios would conclude the three definitions describe nearly the same engagement. They do not describe the same anything. The ratio normalises away the size of the population, and with it the entire definition question.
LiveHow the three counts are built, and what each averages3 min▶
MAU here is the number of accounts active on at least one of the 28 days, so it is one count for the whole window. DAU is a count per day, 28 of them, and the page reports the average. WAU is a count per calendar week (the window starts on a Monday, so the four weeks are Monday to Sunday), and the page reports the average of the four. A single account can therefore be in MAU once, in WAU up to four times and in DAU up to 28 times. DAU/MAU asks: of the people who showed up at all this month, what share shows up on a typical day. DAU/WAU asks the same over a week, and comes out around three times larger because a week is a smaller net.
Li Jin and Andrew Chen put the objection precisely in their power-user curve essay: DAU/MAU "is a single number and so blurs this variance". They meant the variance between users who come once and users who come daily. Tonight's data adds a second blur: the ratio also hides which verb was used to count anyone at all. Session 5 unpacks the first blur with the L28 histogram, on these same accounts.
Self-studyWhy the ratio barely moved2 min▶
It is tempting to read 6.4 versus 6.6 versus 5.0 as "engagement is about the same however you define it", and that reading is exactly backwards. When the definition narrows from any event to pushing code, both numerator and denominator shrink, and they shrank in almost the same proportion, so the ratio held. The narrowest verb has the lowest ratio, 5.0 percent, which is at least in the expected direction: opening issues and pull requests is a rarer daily habit than pushing. But a ratio that sits between 5 and 7 percent for three populations that differ ninefold is telling you it is a poor instrument for this question, not that the question has one answer.
A working rule: never report a ratio without the two counts that made it, on the same line. "DAU/MAU 6.6 percent (3,218 of 48,946, pushed code, sampled hours)" cannot be misread. "Stickiness 6.6 percent" can be misread in a dozen ways, and will be.
The redefinition nobody noticed. A common pattern: a team tightens its definition of active mid-quarter, from "opened the app" to "performed the core action", for good reasons. MAU falls by a large fraction on the chart and there is a difficult meeting. DAU/MAU, on the slide beside it, hardly moves, and someone says "at least engagement is stable". Both slides are computed correctly. The second one is answering a question nobody asked, and its stability is a property of the arithmetic rather than of the users.
A day the data only half sees
4 min liveEvery count tonight says "active on a day", and the data behind it saw two hours of that day. An account that pushes at 10:00 UTC every single morning is, in this sample, never active. An account that pushes once at 15:20 is active for the day. "Active on a day" here means "seen in the 03:00 or the 15:00 hour of that UTC day", and the honest sentence is longer than the convenient one every time. The same problem hides inside your own product's numbers, in gentler forms: which timezone's midnight ends the day, which events the client failed to send, and what a verb like "pushed" is allowed to count.
LiveThree ways "a day" goes wrong2 min▶
The clock. Two hours of 24 is the extreme case, chosen because 56 files can be streamed on a laptop while 672 cannot. But even complete data has a clock problem: a "day" is a UTC day here, and an account in Sydney whose working afternoon is 03:00 to 07:00 UTC has its Tuesday counted as Wednesday. Choose the day boundary and write it down; the same person can be a one-day user or a two-day user depending on it.
The verb. Session 1 found 234 PushEvents in a single hour that carried zero commits: a branch moved, a force push, a ref updated with nothing new in it. The push test tonight is "at least one PushEvent", so every one of those 234 makes its account "pushed code" active for the day. A stricter verb, "pushed at least one commit", needs the payload's commit count, which the sampler kept as a per-event count but not per commit. The definition you can compute is bounded by what you chose to keep.
The gap. An event that never arrived looks identical to an action that never happened. In this sample the gap is 22 hours a day and it is known. In a product it is a client that dropped events on a bad connection, and it is not known unless someone counts arrivals against expectations, which is what the session 1 validator's "not in the plan" tray was for.
Self-studyWhich direction the sampling bends each number2 min▶
The two-hour clock undercounts everything, but not evenly. MAU is hurt least: an account needs to be seen in just one of 56 sampled hours across the month to make it in. DAU is hurt most: an account has to land in one of two specific hours on that specific day. So the DAU/MAU ratio is biased low by construction, and by an unknown amount, because the true distribution of activity across hours for these accounts is not in the data. That is why the page reports 6.4 percent without ever calling it GitHub's stickiness, and why session 5's days-active histogram will lean left partly for a reason that has nothing to do with the users.
A fair question is whether any of this is worth computing. Yes, for the comparison: all three definitions were sampled by the same clock, so the ninefold gap between them is real even though each absolute number is low. Relative claims survive a shared bias; absolute claims do not.
The dashboard that sampled without saying so. A common pattern: a high-volume product turns on event sampling in its analytics tool to control cost, and the tool scales the counts back up so the charts look complete. Months later an analyst compares the dashboard's daily actives against the billing database and the two disagree, most for the smallest segments, where a scaled-up handful of sampled users stands in for thousands. Nothing was wrong with the tool. The word "sampled" had simply fallen off the chart title, and every reader assumed a census. The fix is a label, not a recompute: "sampled hours" or "sampled users", in the same font as the number.
DAU, WAU and MAU from real rows, three ways
★ 22 min · everyone buildsStandard-library Python and 56 GH Archive files, streamed and discarded. Step 1 is the slow part, about 37 files' worth of gzip per hour of download depending on your connection, and it produces a small JSON cache; everything after it runs in seconds. Every number printed below is what this code prints on the cache for 2024-03-04 to 2024-03-31, hours 03:00 and 15:00 UTC, accounts with id divisible by 20.
Stream the sample. Each hourly file is gunzipped on the fly and every line is tested with a regex before any JSON is parsed, because parsing 95 percent of the lines just to throw them away is the slow way. An actor id divisible by 20 is kept; the rest are skipped. Bot logins stay in the file so the drop is visible later.
Combine and load. One dictionary keyed by hour, each value the per-actor record [login, n_events, n_push, n_opened]. Then drop bots and print the two sizes. The print statement is the first line of every report built from this cache.
Write the definitions. Three lambdas on the per-hour record. This is the step a product manager should be able to read, and the one that changes when the definition of active changes. Run it before step 2 so the loop can use it.
Count. MAU is the number of accounts with a non-empty set. DAU is the count per day, averaged over 28. WAU is the count per calendar week (the window opens on a Monday), averaged over four. The ratios use the averages. One extra line reports how concentrated the sampled events are, for session 5.
Read the output. These are the lines the cache gives. Say the long sentence when you quote them: accounts in the 5 percent sample, seen in the two sampled hours, March 2024.
Keep the cache and the sets. seen[aid][d] is a set of days per account per definition. Session 5 turns the size of each set into the L28 histogram, the power-user curve, without touching GH Archive again. If you change a definition, change the lambda in step 3 and re-run steps 2 and 4; the fetch never needs to repeat.
Before session 4
30 min- Add a fourth definition to step 3, "busy": at least two events in a sampled hour (
v[1] > 1). Re-run steps 2 and 4 and write down where its MAU and DAU/MAU land relative to the three on this page. Your numbers will differ from nothing here; this is the same cache, so they are new canon for you. - Compute DAU/MAU per calendar week instead of for the whole month, for the "push" definition, and note whether it moves more between weeks than between definitions. One sentence on what that says about the ratio as an instrument.
- Write your own product's three definitions of active, one sentence each with the verb in it, and predict their ordering before you count. Then count, if you have the events. Keep the prediction; being wrong about the ordering is the useful part.
- Keep
actor-days.jsonand theseensets. Session 5 builds the days-active histogram from them, and session 4 switches to a different sample: 8,348 new repositories followed for six weeks.
Three questions
Clear all three and analyst session 3 gets stamped in your passport.
1 · The same 79,555 sampled accounts give MAU 79,555 under "any event", 48,946 under "pushed code" and 8,993 under "opened an issue or PR". What changed between the three counts?
Hold everything fixed and change the verb, and "MAU" moves ninefold. That is why a monthly active number reported without its definition cannot be compared with anything, including last quarter's.
2 · DAU/MAU reads 6.4, 6.6 and 5.0 percent for the three definitions. A colleague says "so engagement is about the same however you define active". What is wrong with that reading?
Numerator and denominator shrank in almost the same proportion, so the ratio held while the counts collapsed. Report a ratio with the two counts that made it on the same line, and the verb beside them.
3 · The sample watches the 03:00 and 15:00 UTC hours only. An account that pushes code at 10:00 UTC every weekday in March 2024 appears in the cache as:
Two hours of 24 cannot see a user who works at other hours. MAU is hurt least and DAU most, so the ratio is biased low by an unknown amount. Relative claims between the three definitions survive the shared bias; absolute claims about GitHub do not.
What this session teaches, and where it came from
Every fact on this page, and its verification tier, is recorded in the course's source map.
Analyst session 3 cheat sheet · pin this
Tonight counted who is active. Session 4 asks which early behaviour predicts coming back at all. The sample changes to 8,348 new public repositories born in one hour and followed for six weeks, of which 325, 3.9 percent, were ever touched again. We sweep candidate behaviours and thresholds, meet lift and returners caught, and find that "seen on a second day" at 5.44x beats "pushed on day one" at 1.74x, while a busy birth hour predicts nothing. Then the hard part: a predictor is not a lever, and the experiment that would prove one belongs to another course.