The request

From Priya Shah, Head of Data
Your goal
Booked orders have status "completed" or "refunded". For each customer, quarter 0 is the quarter of their first booked order; quarter q is q quarters later. A customer is active in quarter q if they have a booked order in it. Set result to a DataFrame with one row for each q from 1 to 8 and columns quarter (q), cohorts (how many cohort quarters have reached q by 2025Q4), customers (customers in those cohorts) and retention_pct (active customers in quarter q × 100 / customers, rounded to 1), sorted by quarter, default index.
- Use the
customersDataFramenot done yet - Use the
ordersDataFramenot done yet - Show exactly
quartercohortscustomersretention_pctnot done yet
The lower line divides by every customer, including those whose quarter q has not happened yet.
Show the numbers
| Quarters since first order | Cohorts that reached it | Every customer (biased) |
|---|---|---|
| 1 | 44.9% | 38.3% |
| 2 | 46% | 35.5% |
| 3 | 42.5% | 29.4% |
| 4 | 40.9% | 25.9% |
| 5 | 39.7% | 20.3% |
| 6 | 41% | 18.4% |
| 7 | 40.2% | 15.6% |
| 8 | 36.2% | 12% |
Your code
Loading the editor…
Reference solution
| customer_id | first_name | last_name | city | |
|---|---|---|---|---|
| 1 | Owen | Hudson | owen.hudson@icloud.com | San Francisco |
| 2 | Douglas | Mendoza | dmendoza69@gmail.com | Atlanta |
| 3 | Henry | Brooks | hbrooks@lakesidecafe.com | Los Angeles |
| 4 | Brian | Graham | bgraham@bluecedarhomes.com | Seattle |
Mote checks your result
Press Run. Each row that matches the goal becomes a firefly in your lantern.
What Priya gets, and why
DataFrames
Loaded for you as pandas DataFrames, plus pd and np.
customers2,018 rows · 9 columns
customer_idintegerfirst_namevarcharlast_namevarcharemailvarcharcityvarcharstatevarcharsignup_datedatesegmentvarcharreferral_sourcevarchar
| customer_id | first_name | last_name | city | state | signup_date | segment | referral_source | |
|---|---|---|---|---|---|---|---|---|
| 1 | Owen | Hudson | owen.hudson@icloud.com | San Francisco | CA | 2022-06-02 | consumer | search |
| 2 | Douglas | Mendoza | dmendoza69@gmail.com | Atlanta | GA | 2022-06-02 | consumer | NaN |
| 3 | Henry | Brooks | hbrooks@lakesidecafe.com | Los Angeles | CA | 2022-06-04 | business | NaN |
| 4 | Brian | Graham | bgraham@bluecedarhomes.com | Seattle | WA | 2022-06-06 | business | |
| 5 | Kyle | Jimenez | kjimenez27@yahoo.com | Indianapolis | IN | 2022-06-07 | consumer | NaN |
orders12,000 rows · 7 columns
order_idintegercustomer_idintegerorder_datetimestampstatusvarcharchannelvarcharshipping_feedecimalcoupon_codevarchar
| order_id | customer_id | order_date | status | channel | shipping_fee | coupon_code |
|---|---|---|---|---|---|---|
| 1001 | 125 | 2023-01-01 20:51:19 | completed | web | 0.00 | NaN |
| 1002 | 156 | 2023-01-02 07:58:46 | completed | web | 0.00 | NaN |
| 1003 | 22 | 2023-01-02 10:25:27 | completed | marketplace | 4.99 | WELCOME10 |
| 1004 | 48 | 2023-01-02 11:21:51 | completed | marketplace | 4.99 | WELCOME10 |
| 1005 | 82 | 2023-01-02 11:43:36 | completed | web | 0.00 | WELCOME10 |
products120 rows · 8 columns
product_idintegernamevarcharcategoryvarcharsubcategoryvarcharunit_pricedecimalunit_costdecimallaunched_ondateis_activeboolean
| product_id | name | category | subcategory | unit_price | unit_cost | launched_on | is_active |
|---|---|---|---|---|---|---|---|
| 101 | Vale Velvet Loveseat | Furniture | Sofas | 1659.00 | NaN | 2023-09-11 | true |
| 102 | Alder Oak Coffee Table | Furniture | Tables | 919.00 | 503.50 | 2024-05-16 | true |
| 103 | Orchard Rattan Dining Chair | Furniture | Chairs | 789.00 | 429.87 | 2023-05-27 | true |
| 104 | Upland Oak Bookcase | Furniture | Storage | 429.00 | 214.70 | 2022-12-20 | true |
| 105 | Avery Leather Bed Frame | Furniture | Beds | 449.00 | 232.70 | 2021-08-31 | true |
order_items30,338 rows · 6 columns
order_item_idintegerorder_idintegerproduct_idintegerquantityintegerunit_pricedecimaldiscount_pctdecimal
| order_item_id | order_id | product_id | quantity | unit_price | discount_pct |
|---|---|---|---|---|---|
| 1 | 1001 | 121 | 9 | 31.99 | 0.00 |
| 2 | 1001 | 154 | 3 | 11.99 | 0.00 |
| 3 | 1002 | 154 | 4 | 11.99 | 0.00 |
| 4 | 1002 | 194 | 1 | 149.00 | 0.00 |
| 5 | 1002 | 193 | 2 | 19.99 | 0.00 |
returns1,673 rows · 5 columns
return_idintegerorder_item_idintegerreturned_ondatereasonvarcharrefund_amountdecimal
| return_id | order_item_id | returned_on | reason | refund_amount |
|---|---|---|---|---|
| 1 | 14 | 2023-01-10 | damaged | 83.69 |
| 2 | 42 | 2023-01-10 | wrong_item | 19.99 |
| 3 | 105 | 2023-01-14 | wrong_item | 10.99 |
| 4 | 24 | 2023-01-16 | changed_mind | 26.99 |
| 5 | 126 | 2023-01-16 | late | 20.99 |
employees60 rows · 8 columns
employee_idintegerfirst_namevarcharlast_namevarchardepartmentvarchartitlevarcharmanager_idintegerhire_datedatesalaryinteger
| employee_id | first_name | last_name | department | title | manager_id | hire_date | salary |
|---|---|---|---|---|---|---|---|
| 1 | Catherine | Hale | Executive | Chief Executive Officer | NaN | 2016-03-01 | 285000 |
| 2 | Robert | Kim | Executive | Chief Operating Officer | 1 | 2017-01-09 | 235000 |
| 3 | Megan | Collins | Sales | Head of Sales | 1 | 2018-04-16 | 172000 |
| 4 | Chloe | Gutierrez | Sales | Account Executive | 3 | 2020-01-21 | 72000 |
| 5 | Frank | Bell | Sales | Account Executive | 3 | 2023-05-21 | 73000 |
web_sessions40,000 rows · 7 columns
session_idintegercustomer_idintegerstarted_attimestampdevicevarcharsourcevarcharpages_viewedintegerconvertedboolean
| session_id | customer_id | started_at | device | source | pages_viewed | converted |
|---|---|---|---|---|---|---|
| 1 | NaN | 2023-01-01 23:02:24 | mobile | paid | 1 | false |
| 2 | 18 | 2023-01-01 15:07:49 | desktop | paid | 1 | false |
| 3 | NaN | 2023-01-01 16:55:46 | desktop | direct | 7 | false |
| 4 | NaN | 2023-01-01 12:43:28 | mobile | organic | 3 | false |
| 5 | NaN | 2023-01-01 13:06:21 | mobile | social | 2 | false |
marketing_spend3,276 rows · 3 columns
spend_datedatechannelvarcharamountdecimal
| spend_date | channel | amount |
|---|---|---|
| 2023-01-01 | paid | 492.99 |
| 2023-01-01 | 37.38 | |
| 2023-01-01 | social | 200.53 |
| 2023-01-02 | paid | 709.13 |
| 2023-01-02 | 64.13 |
The lesson
From a table to a curve
A cohort table has one row per cohort and one column per period, which is too much for a board slide. A retention curve squeezes it into one line: for each number of quarters since the first order, what share of customers placed an order in that quarter?
Quarter 0 is the first order itself, so it is always 100% and left off. Quarter 1 is the next quarter, quarter 2 the one after, and so on. A customer counts as active in a quarter if they have at least one booked order in it. They do not have to be active in every quarter in between, which is why the curve can rise from quarter 1 to quarter 2.
Quarter numbers make the arithmetic easy
Number the quarters in order, 2023Q1 = 0 up to 2025Q4 = 11:
qi = (booked["order_date"].dt.year - 2023) * 4 + booked["order_date"].dt.quarter - 1
A customer’s cohort is their smallest qi; how far each order is from it is plain subtraction:
first = b.groupby("customer_id")["qi"].min().rename("c")
b = b.join(first, on="customer_id")
b["q"] = b["qi"] - b["c"]
Right-censoring: only count who could have come back
The 2025Q4 cohort has no quarter 1 yet. The 2025Q1 cohort has quarters 1 to 3 and nothing after. If you divide quarter-8 activity by every customer, those young customers sit in the denominator with no chance of being in the numerator, and the curve bends down for a reason that has nothing to do with customers leaving.
The fix: for quarter q, the denominator is only the customers whose cohort is at least q quarters before the last quarter in the data:
fair = first[first + q <= last_q]
The chart shows both versions. The honest line flattens out near 40%. The biased line falls almost in a straight line, and every point of that fall is the calendar running out.
Pooled, not averaged
This curve pools customers: active customers across all fair cohorts divided by all customers in them. Averaging each cohort’s rate instead gives the 97-customer 2023Q3 cohort the same weight as the 181-customer 2023Q4 one. Both are defensible, but say which one you used, and the two will not match.
Common mistakes
- Every customer in the denominator. The curve sinks because time ran out, not customers.
- Forgetting how thin the tail is. Quarter 8 rests on four cohorts. Show the base.
- Reading the first cohort as new customers. 2023Q1 holds people who were regulars before the data begins, and it is in every point of the curve.
Lantern lit!
Practised: retention curvecohortright censoringperiodpooled rate
- Solved
- No hints
- First run
