The request

From Angela Rivera, Head of Customer Support
Your goal
Set result to a DataFrame with one row per order line of orders 1001 to 1010 (inclusive): columns order_item_id, order_id, name, category, quantity (name and category come from products, matched on product_id), sorted by order_item_id ascending, index reset.
- Use the
productsDataFramenot done yet - Use the
order_itemsDataFramenot done yet - Show exactly
order_item_idorder_idnamecategoryquantitynot done yet
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 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 |
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 |
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
Why tables are split
order_items stores a product_id on each line instead of the product’s name, category and price.
That’s deliberate: the product’s details live in one place, products, so renaming a product means
changing one row, not 30,000. The cost is that you have to join the tables back together when you
need both.
merge
order_items.merge(products, on="product_id")
For each order line, pandas finds the product row with the same product_id and puts their columns
side by side. The column you match on is the key. If the key has different names in the two
tables, use left_on= and right_on=. For example, each employee’s manager_id points at another
employee’s employee_id, so this puts every employee next to their manager’s details:
employees.merge(employees, left_on="manager_id", right_on="employee_id", suffixes=("", "_manager"))
That gives 59 rows, not 60: the CEO has no manager, so the inner merge leaves that row out.
The how= argument decides what happens to rows without a match:
| how | keeps |
|---|---|
"inner" (default) |
only rows with a match in both tables |
"left" |
every row from the left table; NaN where the right has no match |
"right" |
every row from the right table |
"outer" |
everything from both |
Every order line here has a valid product, so inner and left give the same result. Write how=
explicitly anyway; it tells the reader you thought about it.
Overlapping column names
Both tables have a unit_price column, and they mean different things: the price paid on the line
versus today’s list price. pandas keeps both and adds suffixes, unit_price_x and unit_price_y.
Those names are easy to mix up, so set your own:
order_items.merge(products, on="product_id", suffixes=("_paid", "_list"))
Or select only the columns you need from the right table before merging, which also keeps the result narrow:
order_items.merge(products[["product_id", "name", "category"]], on="product_id")
Check the row count
The single most useful habit with merges: compare row counts before and after.
len(order_items) # lines before
len(order_items.merge(products, on="product_id")) # should be the same
Each line has exactly one product, so the count must not change. If it grew, the right table had duplicate keys and some lines were copied. If it shrank, some lines had no match and an inner join dropped them. You’ll see both problems later in this chapter.
pandas can check this for you. validate="many_to_one" says “many lines may share a product, but
each product appears once in products”, and raises an error if that’s not true:
order_items.merge(products, on="product_id", how="inner", validate="many_to_one")
Other options are "one_to_one" and "one_to_many". Adding validate costs nothing and turns a
silent mistake into a loud one.
Common mistakes
- Merging without
on=. pandas then joins on every column name the tables share, hereproduct_idandunit_price, and most lines fail to match because prices changed. - Using
unit_price_xlater without remembering which table it came from. - Not checking the row count after a merge.
If you know SQL
This is SELECT ... FROM order_items JOIN products USING (product_id), and how= is the join
type.
Lantern lit!
Practised: mergeinner joinonsuffixesrow count check
- Solved
- No hints
- First run
