The request

From Laura Bennett, Head of Finance
Your goal
Step 1 of 3. Check the months. For orders placed from September 1 to December 31, 2025, set result to a DataFrame with columns month, orders (all orders), completed, cancelled, refunded and pending (orders with each status), sorted by month, default index.
- Use the
ordersDataFramenot done yet - Show exactly
monthorderscompletedcancelledrefundedpendingnot done yet - 4 rows, sorted as askednot done yet
Step 2 of 3. Build the KPI table for 2024-11, 2025-10 and 2025-11. Set result to a DataFrame with columns month, net_revenue (rounded to 2), orders (distinct completed orders with a counted line), avg_order_value (revenue / orders, rounded to 2), buyers (distinct customers who ordered), new_buyers (buyers whose first completed order, ever, falls in that month), returning_buyers (buyers - new_buyers), refunds (refund_amount from returns, by the month of returned_on, rounded to 2) and web_conversion_pct (converted sessions * 100 / all sessions that month, by started_at, rounded to 2). Sort by month, default index.
- Use the
order_itemsDataFramenot done yet - Use the
returnsDataFramenot done yet - Show exactly
monthnet_revenueordersavg_order_valuebuyersnew_buyersreturning_buyersrefundsweb_conversion_pctnot done yet
Step 3 of 3. Explain the year-over-year gap. For completed orders (lines with quantity > 0) in 2024-11 and 2025-11, total each order's net revenue, then label it size_band 'large' if that total is 5000 or more, else 'regular'. Set result to a DataFrame with columns month, size_band, orders, net_revenue (rounded to 2) and avg_order_value (rounded to 2), sorted by month, then size_band, default index.
- Use the
order_itemsDataFramenot done yet - Show exactly
monthsize_bandordersnet_revenueavg_order_valuenot done yet - 4 rows, sorted as askednot done yet
This case unlocks when you finish any one of:
The ticket and lesson are open to read now. Solve every level in one of those chapters, in whichever track you prefer, and this case opens.
Step 1 · your code
Loading the editor…
Reference solution
| order_id | customer_id | order_date | status | channel |
|---|---|---|---|---|
| 1001 | 125 | 2023-01-01 20:51:19 | completed | web |
| 1002 | 156 | 2023-01-02 07:58:46 | completed | web |
| 1003 | 22 | 2023-01-02 10:25:27 | completed | marketplace |
| 1004 | 48 | 2023-01-02 11:21:51 | completed | marketplace |
Mote checks your result
Press Run. Each row that matches the goal becomes a firefly in your lantern.
Step 2 · your code
Loading the editor…
Reference solution
| order_item_id | order_id | product_id | quantity | unit_price |
|---|---|---|---|---|
| 1 | 1001 | 121 | 9 | 31.99 |
| 2 | 1001 | 154 | 3 | 11.99 |
| 3 | 1002 | 154 | 4 | 11.99 |
| 4 | 1002 | 194 | 1 | 149.00 |
Mote checks your result
Press Run. Each row that matches the goal becomes a firefly in your lantern.
Step 3 · your code
Loading the editor…
Reference solution
| order_item_id | order_id | product_id | quantity | unit_price |
|---|---|---|---|---|
| 1 | 1001 | 121 | 9 | 31.99 |
| 2 | 1001 | 154 | 3 | 11.99 |
| 3 | 1002 | 154 | 4 | 11.99 |
| 4 | 1002 | 194 | 1 | 149.00 |
Mote checks your result
Press Run. Each row that matches the goal becomes a firefly in your lantern.
What Laura gets, and why
DataFrames
Loaded for you as pandas DataFrames, plus pd and np.
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 |
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 |
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 |
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 |
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 |
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 |
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
What you are checking, and why
A month is ready to report only when its orders have settled. Orders arrive as pending and become
completed, cancelled or refunded over the following days. A cross-tabulation of month
against status shows where the open orders sit:
counts = pd.crosstab(o["month"], o["status"])
November has 2 pending orders out of 715; December has 94 out of 777. That is the evidence behind reporting November.
One table per source
The KPIs come from three tables at three grains: order lines, returns and web sessions. The safe
pattern is to bring each to one row per month, indexed by month, then join them:
sales = lines.groupby("month").agg(revenue=("revenue", "sum"), orders=("order_id", "nunique"), ...)
refunds = returns.groupby(returns["returned_on"].dt.strftime("%Y-%m"))["refund_amount"].sum()
kpi = sales.join(refunds.rename("refunds")).join(web)
New buyers need one more step. “New” means the customer’s first completed order falls in the
month. transform("min") puts each customer’s first order date on every one of their lines, and
.where() keeps the customer_id only on lines in that first month, so nunique() of the result
counts new buyers.
Reading the result
| month | net_revenue | orders | avg_order_value | buyers | web_conversion_pct |
|---|---|---|---|---|---|
| 2024-11 | 319,297.22 | 564 | 566.13 | 357 | 3.50 |
| 2025-10 | 174,599.43 | 333 | 524.32 | 261 | 2.33 |
| 2025-11 | 292,364.40 | 651 | 449.10 | 442 | 4.45 |
Against October everything is up, as it is every November. Against last year, volume grew and revenue fell, so the question becomes average order value. Step 3 answers it: seven bulk orders last November, one this November.
Explaining the mover
Step 3 works at the order level, because the size of an order is only known once its lines are
added up. Group the lines by month and order, label each order with np.where, then group the
orders by month and band. The result splits each November into a few large orders and many regular
ones, which is the breakdown that turns “revenue fell” into a reason leadership can act on.
Pitfalls
groupbyon a computed Series (returns["returned_on"].dt.strftime(...)) works without adding a column first; the result is indexed by the month labels.- Summing
buyersacross months double counts people who bought in both. .loc[["2024-11", ...]]raises aKeyErrorif a month is missing from one source;joinkeeps the sales months and leaves NaN where another source has none.
Lantern lit!
Practised: data validationgroupbynamed aggregationcrosstabkpi definitionmomyoy
- Solved
- No hints
- First run
