The request

From Megan Collins, Head of Sales
Your goal
Set result to a DataFrame with one row per channel and the columns channel, cancelled, completed, pending, refunded, where each status column holds the number of orders with that channel and status (an int, 0 when there are none). Sort by channel A to Z.
- Use the
ordersDataFramenot done yet - Show exactly
channelcancelledcompletedpendingrefundednot done yet - 3 rows, sorted as askednot done yet
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 Megan 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 |
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 |
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 |
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
Long tables and wide grids
A groupby on two keys gives you a long table: one row per combination.
orders.groupby(["channel", "status"]).size()
# channel status
# app cancelled 265
# completed 3885
# pending 50
# refunded 120
# marketplace cancelled 108
# ... (12 rows)
Every number is there, but it’s hard to compare across channels. A wide grid puts one key down
the side and the other across the top, so each cell is one combination. That is what people mean by
a pivot table in a spreadsheet, and pandas has the same tool: pivot_table.
Four arguments, four questions
Each argument answers one question about the grid you want:
orders.pivot_table(
values="order_id", # WHAT do I measure?
index="channel", # what goes DOWN the side (one row each)?
columns="status", # what goes ACROSS the top (one column each)?
aggfunc="count", # WHICH operation combines the rows in a cell?
)
valuesis the column you aggregate. For counting rows, an ID column with no gaps is clearest.indexbecomes the row labels, sorted A to Z;columnsthe column labels, also sorted.aggfuncis the calculation:"count","sum","mean","median","max","nunique".aggfunc="size"counts rows without needingvaluesat all.
Leave out columns and you get a one-dimensional summary: one row per channel, one column.
The result has channel as its index and the statuses as columns. When you check the table or
export it, the index name channel travels along as the first column.
Empty cells and fill_value
When a combination never happens, the cell is empty, and pandas shows it as NaN. Products by category and active flag is a good example: every Lighting product is still active.
products.pivot_table(index="category", columns="is_active",
values="product_id", aggfunc="count")
# is_active False True
# category
# Decor 3.0 17.0
# ...
# Lighting NaN 20.0
The NaN also turns the counts into floats (3.0, 17.0). For a count, “no rows” means 0, so pass
fill_value=0: the empty cell becomes 0 and the counts stay whole numbers.
products.pivot_table(index="category", columns="is_active",
values="product_id", aggfunc="count", fill_value=0)
# Lighting 0 20
Only fill with 0 when zero is the truth. For an average price in a cell with no products, 0 would be a lie; leave it NaN.
pivot_table or groupby?
Both compute the same numbers. Pick by the shape you need next:
pivot_tablewhen the result is a report: rows × columns for a person to read, export or chart on two dimensions. On the full orders table, completed orders read 5,367 for web, 3,885 for the app and 1,597 for the marketplace, side by side in one column.groupbywhen the result is a calculation: one value per group that you’ll filter, merge or feed into the next step.
Common mistakes
- Leaving out
aggfunc. The default is"mean", which averagesorder_idvalues: numbers, but nonsense. - Swapping
indexandcolumns, which gives statuses as rows and channels across the top. - Counting a column that has missing values.
"count"skips NaN, so use an ID column oraggfunc="size". - Filling with 0 when the aggregate is an average or a rate: an empty cell is unknown, not zero.
Lantern lit!
Practised: pivot tableaggfuncfill valuewide format
- Solved
- No hints
- First run
