Where we start
The running data is Apex, a constructed group of four entities whose staff table was generated with a seeded script to look like what an HR system exports: one row per person, a join date, a last day if they left, a department, a free-text reason. Nobody in it is real and every page says so. What is real is the shape, because it is the shape of nearly every people dataset an analyst is handed, and the shape decides more than the numbers do.
★ What you walk out with tonight. The three-table model and the test for which one you have been given, a headcount function that takes a date, the joiners and leavers between two dates with the identity asserted in code, and the three bases printed side by side so that the choice is visible.
Employee, position, event
5 min liveThree tables answer every people question between them. The employee table has one row per person and says who they are. The position table has one row per seat and says what the organisation has decided it needs. The event table has one row per thing that happened to a person on a date: joined, moved, promoted, went on leave, left. Attrition, tenure, time-in-grade and every flow live in the third table, and the third table is the one you almost never get.
LiveWhich table have you been given, and the test that tells you3 min▶
The test is one question: can a person appear more than once? If every person is exactly one row, you have an employee snapshot, and every flow will have to be reconstructed from date columns. If a person can appear many times with a date and a type on each row, you have an event log, and flows are counts. Apex is the first kind: 1,071 rows, 1,071 people, a join date on every row and a last day on 210 of them.
A snapshot with a last day column can rebuild one event per person: the join, and the exit if there was one. It cannot rebuild a transfer three years ago, a promotion, a period of leave, or the exit of anybody who left before the snapshot was taken and was therefore never in it. Apex marks transfers with a flag and a "pre-transfer years" column, which is the snapshot's way of stapling a second event on, and it is a sign the table was built by somebody who knew what was missing.
The consequence for every later session: attrition, tenure and survival are event questions asked of a snapshot, and the honest analysis says which events the snapshot can see and which it cannot.
Self-studyThe id that is the person, and the one that is the seat2 min▶
Apex's Staff ID is one per person. Some systems issue a new id on every rehire or every transfer between legal entities, and then a person is two rows, a transfer looks like an exit and a hire, and the identity breaks by exactly the number of transfers. Check the cardinality before anything else: the number of distinct ids against the number of rows, and both against what the payroll says. The retention course found the same trap in a customer table, where an order-side key made every customer a one-time buyer; the people version makes every transferee a leaver.
The event log that existed and was never exported. Most HR systems record every change to a person's record with a timestamp, in an audit table nobody has been asked for. An analyst who asks for "the employee export" gets the snapshot. An analyst who asks for "the change history for the join date, department, grade and status fields" gets an event log, from the same system, for the same people. The second request is the one that makes deep-dive sessions 2 and 8 possible. Constructed setting; the mechanism is common.
A snapshot is a photograph, an event log is a film
5 min liveApex's table holds everyone employed on 1 January 2026 and the 210 people who left between then and 17 June. Nobody who left in 2025 is in it. Ask it "how many people left in 2024" and it will answer zero, confidently, because the people who did are not there to be counted. That is not a bug in the table; it is what a snapshot is.
LiveWhat a snapshot can and cannot answer, in one table3 min▶
Can answer: headcount on the snapshot date; headcount on any earlier date for people still present at the snapshot (a biased count, because the earlier leavers are absent); joiners in any period, if the join date is kept for everyone; leavers in the window after the snapshot, if the export includes them, which Apex's does for six months.
Cannot answer: leavers before the snapshot; the true tenure distribution of everyone who ever joined; how many people left in their first year, historically; any flow before the photograph. Ask any of these of a snapshot and it will return a number, and the number will describe the survivors.
The rule for the rest of the track: every flow computed from Apex is a flow inside the window January to June 2026, the only window the table can see. Foundations session 1 taught the same lesson with a camera drawing; here it becomes a filter in the code, and the filter is written at the top.
Self-studyBuilding an event log from snapshots you already have2 min▶
If the system will not give you the change history, monthly snapshots will. Keep every month's export with the date in the filename, and the difference between two consecutive snapshots is the event log for that month: ids present in the second and not the first joined, ids present in the first and not the second left, ids whose department changed moved. It is coarse, one event per person per month at most, and it is the single most valuable habit a people data team can adopt, because it turns the photograph problem into a film from the day you start.
The identity, and the base under the rate
5 min liveWith a snapshot that includes six months of leavers, the bridge for that window can be computed exactly: 1,022 people on 1 January, 49 joined, 210 left, 861 on 1 July. The identity holds by construction, and asserting it in code is the cheapest test a people pipeline can have. Then the same 210 over three bases: 20.5, 22.3 and 24.4 percent, printed side by side so the choice is a choice.
LiveHeadcount as a function of a date, and why that shape matters3 min▶
Headcount is not a column; it is a function: the number of rows whose join date is on or before the date and whose last day is either missing or after it. Written that way once, it gives a consistent stock for any date, and the bridge becomes three calls: headcount at the start, headcount at the end, and the two flows as date-range counts. Written any other way, as a column somebody filled in, a total on a slide, or a figure from a different system, it drifts from the flows and the identity breaks.
Two details decide whether the identity holds. The boundary: a person whose last day is exactly the end date is in the end stock or not, and a joiner on the start date is in the start stock or not; pick one convention and use it in both the stock and the flow. The window: only events inside the window count, so a person who left after the end date is still in the end headcount. Get both right and the assert passes; get either wrong and it fails by exactly the number of boundary cases, which is a useful number to see.
Self-studySix decisions, written at the top of the script2 min▶
Which id is the person. Which rows are in the stock (payroll employees; contractors; people on leave). Which date is the join (offer, start, first payroll). Which date is the exit (notice, last day, last payroll). How transfers are treated on this bridge. Which base the rate uses. Six lines of comment above the code, and when a colleague's bridge differs from yours the diff between the two comment blocks is the answer. The same discipline the retention and RFM courses use for a customer table, applied to people, where the disagreements are more emotional and the fix is the same.
The bridge in pandas, on 1,071 constructed rows
★ 22 min · everyone buildsSeven steps on materials/apex-staff.csv, the constructed Apex table that ships with these pages. Every printed number is what the code produces on that table; on your own export the numbers will differ and the decisions will not.
Load, and check that the id is the person. Rows and distinct ids should agree; if they do not, somebody is two rows.
The six decisions, as comments. Written before any number, so that a colleague's different number is a diff and not an argument.
Headcount as a function of a date. On the payroll on that date: joined on or before it, and not yet left (no last day, or a last day after it).
The two flows, inside the window. Joiners with a join date on or after the start; leavers with a last day inside the window. Nothing before the start can be seen, and the code says so.
Assert the identity. One line. If it fails, the difference is the number of boundary cases or double counts, and that is the next thing to look at.
Three bases, printed together. The choice is the average base, and the other two are printed beside it so the choice is visible. Annualised by doubling, with the assumption in the label.
Split by entity and department, start base. The base is printed under every rate, because the next session is about what a base of 75 does to a rate.
Keep the assert. A people pipeline with the identity asserted in it fails loudly the day a system starts issuing new ids on transfer, a contractor feed is added to the export, or a last day is entered in the wrong year. Without it, the rate quietly moves and somebody presents the movement as a trend.
Before deep-dive session 2
30 min- Run the seven steps on Apex, then on your own export. Write the six decisions as comments before any code below them.
- Move the window to 1 February to 1 June and re-run. Write which of the four bridge numbers changed and why the identity still holds.
- Answer "which table is this" for your export: can a person appear twice? What events can it rebuild, and what can it not?
- Bring the table. Session 2 fits a survival curve to it and shows how far the photograph problem bends the curve.
Three questions
Clear all three and deep-dive session 1 gets stamped in your passport.
1 · You are handed a table with one row per person, a join date, and a last day that is empty for most rows. Which table is it, and what follows?
The test is whether a person can appear more than once. One row per person is a photograph; the last day column is the final event only. Apex can see exits from 1 January to 17 June 2026 and nothing before.
2 · Why is headcount written as a function of a date rather than read from a column or a slide?
The boundary convention and the window convention live inside the function once, and both stock and flow use them. That is what lets one assert line catch double counts and boundary errors.
3 · The script prints 20.5, 22.3 and 24.4 percent for the same 210 leavers and then chooses one. Why print all three?
None of the three is wrong; an undeclared one is. The same habit applies to the annualised figure, which carries the words "assumes H2 repeats H1" in its label.
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.
Deep-dive session 1 cheat sheet · pin this
You have a bridge that reconciles and a table you know to be a photograph. Deep-dive session 2 asks the photograph a question it cannot honestly answer, how long do people stay, fits a Kaplan-Meier survival curve to it by hand, gets 94 percent at one year and 78 at five, and then shows why every one of those numbers is too high: nobody who left before 2026 is in the table. The fix is an event log, and the session ends with what one would have to contain.