The request

From Thomas Whitaker, Head of Merchandising
Your goal
Step 1 of 2. Check the change. For every order line of the products whose price rose, label period ('before' if order_date < '2025-03-01', else 'after') and price_paid ('old' if the line's unit_price equals the product's lowest price, else 'new'). Set result to a DataFrame with columns period, price_paid and lines (the number of order lines), sorted by period descending, then price_paid descending, with a default index. Use all order lines, whatever the status.
- Use the
ordersDataFramenot done yet - Use the
order_itemsDataFramenot done yet - Show exactly
periodprice_paidlinesnot done yet
Step 2 of 2. Compare like for like. Use completed orders, lines with quantity > 0, and only products launched before 2024-01-01. Label each line price_group 'raised' or 'unchanged'. Set result to a DataFrame with columns price_group, units_2024 and units_2025 (total quantity from March 1 to August 31 of each year), units_change_pct, revenue_2024 and revenue_2025 (net revenue, rounded to 2) and revenue_change_pct. Both change columns are (2025 - 2024) * 100 / 2024 from unrounded sums, rounded to 1. Sort by price_group, default index.
- Use the
productsDataFramenot done yet - Use the
ordersDataFramenot done yet - Show exactly
price_groupunits_2024units_2025units_change_pctrevenue_2024revenue_2025revenue_change_pctnot done yet
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
| product_id | name | category | subcategory | unit_price |
|---|---|---|---|---|
| 101 | Vale Velvet Loveseat | Furniture | Sofas | 1659.00 |
| 102 | Alder Oak Coffee Table | Furniture | Tables | 919.00 |
| 103 | Orchard Rattan Dining Chair | Furniture | Chairs | 789.00 |
| 104 | Upland Oak Bookcase | Furniture | Storage | 429.00 |
Mote checks your result
Press Run. Each row that matches the goal becomes a firefly in your lantern.
What Thomas 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 |
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 |
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 |
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 you compare anything, prove that you are looking at the right products and the right date.
Thomas says “about 30 products, March 1”. The data can confirm both, because order_items stores
the price paid on every line, while products["unit_price"] only holds today’s list price.
A product whose price changed has two different prices across its order lines. Named aggregation gives both in one pass, and a boolean mask keeps the products where they differ:
prices = order_items.groupby("product_id", as_index=False).agg(
old_price=("unit_price", "min"), new_price=("unit_price", "max")
)
prices = prices[prices["old_price"] != prices["new_price"]]
len(prices) # 29
Step 1 then labels each of their lines twice, by date and by price, with np.where, and counts
the combinations. If the change happened cleanly on March 1, only two combinations exist: before
with the old price, after with the new one.
Like for like, with a control group
Step 2 answers the agreed question. Three choices make the comparison fair:
- Same months, a year apart. March to August 2025 against March to August 2024.
- Like-for-like products. Only products launched before January 1, 2024, so every product was on sale for the whole of both windows.
- A control group. Every other product is labelled
unchanged. Its growth is what the store did anyway.
isin labels the groups in one line, and pivot_table puts each year in its own column, the
pandas version of SUM(CASE WHEN ...):
lines["price_group"] = np.where(lines["product_id"].isin(raised), "raised", "unchanged")
u = lines.pivot_table(index="price_group", columns="yr", values="quantity", aggfunc="sum")
The columns of u are the years as integers, so u[2025] - u[2024] subtracts row by row. Compute
the percentages on these unrounded totals and round last.
Reading the result
| price_group | units_change_pct | revenue_change_pct |
|---|---|---|
| raised | 21.0 | 44.3 |
| unchanged | 9.2 | 13.4 |
If the rise had hurt demand, raised products would grow less than unchanged ones. They grew more. Revenue grew faster than units because each unit earned about 6% more and because sales shifted towards the dearer items in the group.
Pitfalls
- Filter with
<on the day after the window ends:d < "2025-09-01"keeps all of August 31. - Forgetting
.copy()after filtering and then adding columns gives aSettingWithCopyWarning. pivot_tablesorts its index, so the result is already inprice_grouporder; the explicitsort_valuesmakes that a promise rather than a coincidence.
Lantern lit!
Practised: data validationgroupbynamed aggregationpivot tablelike for likecontrol groupyoy
- Solved
- No hints
- First run
