The request

From Angela Rivera, Head of Customer Support
Your goal
Step 1 of 3. Set result to a DataFrame with columns category and n_returns (number of returns whose order line is in that category), sorted by n_returns descending then category ascending, index reset.
- Use the
productsDataFramenot done yet - Use the
order_itemsDataFramenot done yet - Show exactly
categoryn_returnsnot done yet
Step 2 of 3. Set result to a DataFrame with columns category, n_lines (all order lines in that category), n_returns (returned lines in that category) and return_rate (n_returns / n_lines, rounded to 3), sorted by return_rate descending then category ascending, index reset.
- Use the
productsDataFramenot done yet - Use the
order_itemsDataFramenot done yet - Show exactly
categoryn_linesn_returnsreturn_ratenot done yet
Step 3 of 3. Set result to a DataFrame with columns channel and total_refund (sum of refund_amount, rounded to 2), using the channel of the order each returned line belongs to, sorted by total_refund descending, index reset.
- Use the
ordersDataFramenot done yet - Use the
order_itemsDataFramenot done yet - Show exactly
channeltotal_refundnot done yet
Step 1 · 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.
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.
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 Angela gets, and why
DataFrames
Loaded for you as pandas DataFrames, plus pd and np.
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 |
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 |
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 |
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
Map the path first
returns only knows the order_item_id that came back. Everything else is a join away:
returns --order_item_id--> order_items --product_id--> products (category)
--order_id----> orders (channel)
Sketching this path before you write code is a good habit for any multi-table question. Note the
grain of each hop: one return per line at most (one-to-one), many lines per product and per order
(many-to-one). Those are exactly the validate= values to use.
Step 1: counts by category
returned = (
returns
.merge(order_items[["order_item_id", "product_id"]], on="order_item_id", validate="one_to_one")
.merge(products[["product_id", "category"]], on="product_id", validate="many_to_one")
)
returned.groupby("category", as_index=False).agg(n_returns=("return_id", "count"))
len(returned) should equal len(returns). If it doesn’t, a join lost or duplicated returns.
Step 2: rates, not counts
Raw counts favour whatever sells most. A fair comparison divides by how much each category sold. That needs a denominator from all order lines, not just returned ones:
lines = order_items.merge(products[["product_id", "category"]], on="product_id")
lines = lines.assign(is_returned=lines["order_item_id"].isin(returns["order_item_id"]))
The flag is_returned keeps the table at the line grain, with no merge that could fan out. Then one
groupby gives both counts, since the sum of a boolean column counts its True values:
(
lines.groupby("category", as_index=False)
.agg(n_lines=("order_item_id", "count"), n_returns=("is_returned", "sum"))
.assign(return_rate=lambda d: (d["n_returns"] / d["n_lines"]).round(3))
)
assign with a lambda lets you compute a new column from the aggregated table in the same chain.
The ranking by rate can differ from the ranking by count, and that difference is the insight: a
category might generate many returns simply because it sells a lot, while another is returned
unusually often for its volume.
Returns here include every line of refunded orders, so the rate mixes two causes: whole-order refunds and individual returned items. A careful analysis would split them; mention it if you present this.
Step 3: money by channel
refunds = (
returns
.merge(order_items[["order_item_id", "order_id"]], on="order_item_id")
.merge(orders[["order_id", "channel"]], on="order_id")
)
refunds.groupby("channel", as_index=False).agg(total_refund=("refund_amount", "sum"))
refund_amount is a per-return value, so summing it on a table with one row per return is safe.
Compare the total with returns["refund_amount"].sum() to confirm.
Common mistakes
- Dividing returns by the number of products or orders instead of order lines.
- Getting return rates over 1, a sure sign of a fan-out in the numerator.
- Using
countonis_returned, which counts all lines, instead ofsum.
Lantern lit!
Practised: mergemulti tablegroupbyratesvalidate
- Solved
- No hints
- First run
