The request

From Daniel Foster, Mobile Product Manager
Your goal
Step 1 of 3. Check the data. For each channel, count all orders by status. Set result to a DataFrame with columns channel, completed, cancelled, refunded, pending, and pending_late (pending orders placed on or after 2025-12-22), sorted by channel, default index. Use the orders table alone.
- Use the
ordersDataFramenot done yet - Show exactly
channelcompletedcancelledrefundedpendingpending_latenot done yet - 3 rows, sorted as askednot done yet
Step 2 of 3. Measure the mix. Use completed orders and lines with quantity > 0. For each year (order_date year, as column year) and channel, compute the number of orders and the net revenue. Set result to a DataFrame with columns year, channel, orders, order_share_pct (the channel's share of that year's orders), revenue (rounded to 2) and revenue_share_pct (its share of that year's revenue), shares rounded to 1, sorted by year, then channel, default index.
- Use the
order_itemsDataFramenot done yet - Show exactly
yearchannelordersorder_share_pctrevenuerevenue_share_pctnot done yet - 9 rows, sorted as askednot done yet
Step 3 of 3. Separate mix from growth. Use completed orders and lines with quantity > 0. For each channel, set result to a DataFrame with columns channel, orders_2024 and orders_2025, orders_change_pct ((2025 - 2024) * 100 / 2024), aov_2025 (2025 net revenue per order) and shipping_2025 (the average shipping_fee of its 2025 orders). Round the change to 1 and the money to 2, sort by channel, default index.
- Use the
ordersDataFramenot done yet - Use the
order_itemsDataFramenot done yet - Show exactly
channelorders_2024orders_2025orders_change_pctaov_2025shipping_2025not 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_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.
What Daniel 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 |
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 |
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
What you are checking, and why
Before measuring shares, make sure the two columns the analysis rests on are clean: channel and
status. pd.crosstab counts every combination in one call:
pd.crosstab(orders["channel"], orders["status"])
Three things to read in the result:
- The number of rows. One per channel value. A fourth row such as
Appwould mean casing to fix. - Pending orders. Daniel warned that December is not settled. The extra
pending_latecolumn shows how many pending orders are simply recent ones. - Cancellations by channel. If one channel cancelled far more, using completed orders would tilt the mix. Here they are all close to 6%.
Assigning a Series to a new column lines it up on the index, so late.groupby("channel").size()
lands on the right rows even though it was computed separately.
Shares within each year
A share of total needs each row’s group total next to it. transform returns a result the same
length as the input, so it can sit in a new column:
g["orders"] * 100 / g.groupby("year")["orders"].transform("sum")
Each year’s three shares add up to 100%. Aggregate to year and channel first, then divide.
Mix or growth?
A rising share has two possible stories. Either the app grew while the others held steady, or the
app held steady while the others fell. Usually it is some of both, and only the counts tell them
apart. Step 3 rolls the lines up to orders, then uses pivot_table to put each year in its own
column, the pandas version of COUNT(CASE WHEN year = 2024 ...).
Reading the result
| channel | orders_2024 | orders_2025 | orders_change_pct | aov_2025 |
|---|---|---|---|---|
| app | 1239 | 1904 | 53.7 | 523.27 |
| marketplace | 543 | 630 | 16.0 | 515.01 |
| web | 1891 | 1761 | -6.9 | 557.03 |
The store grew in 2025 and the marketplace kept pace with it. The app grew far faster, and the website shrank. The data cannot say which customers moved; a customer-level follow-up would show how many 2024 web buyers ordered through the app in 2025.
Pitfalls
lines.groupby(...).size()counts lines, not orders. Usenuniqueon order_id or roll up first.- A share without the counts behind it can hide a shrinking market.
shipping_feeis what the customer paid for delivery, not the fee the marketplace charges us.
Lantern lit!
Practised: data validationcrosstabgroupbytransformshare of totalyoymix vs growth
- Solved
- No hints
- First run
