1. Lighthouse

Level 2 · Revenue per category, several ways

The request

From Thomas Whitaker, Head of Merchandising

“For next year's range plan I need one row per category for 2025: total revenue, the typical (median) order line, and how many lines that is. Completed orders only.”

Your goal

Using order lines whose order has status 'completed' and whose order_date is in 2025, with line revenue = quantity * unit_price * (1 - discount_pct / 100) (unit_price from order_items), set result to a DataFrame with columns category, sum_revenue, median_revenue, count_revenue: the sum, the median and the number of line revenues per category, rounded to 2. One row per category, sorted A to Z.

  • Use the products DataFramenot done yet
  • Use the orders DataFramenot done yet
  • Show exactly category sum_revenue median_revenue count_revenuenot done yet

Your code

Python

Loading the editor…

    ResultPreview of products · press Run to see yours
    Preview of products
    product_idnamecategorysubcategoryunit_price
    101Vale Velvet LoveseatFurnitureSofas1659.00
    102Alder Oak Coffee TableFurnitureTables919.00
    103Orchard Rattan Dining ChairFurnitureChairs789.00
    104Upland Oak BookcaseFurnitureStorage429.00

    Mote checks your result

    Press Run. Each row that matches the goal becomes a firefly in your lantern.

    DataFrames

    Loaded for you as pandas DataFrames, plus pd and np.

    products120 rows · 8 columns
    • product_idinteger
    • namevarchar
    • categoryvarchar
    • subcategoryvarchar
    • unit_pricedecimal
    • unit_costdecimal
    • launched_ondate
    • is_activeboolean
    First 5 rows of products
    product_idnamecategorysubcategoryunit_priceunit_costlaunched_onis_active
    101Vale Velvet LoveseatFurnitureSofas1659.00NaN2023-09-11true
    102Alder Oak Coffee TableFurnitureTables919.00503.502024-05-16true
    103Orchard Rattan Dining ChairFurnitureChairs789.00429.872023-05-27true
    104Upland Oak BookcaseFurnitureStorage429.00214.702022-12-20true
    105Avery Leather Bed FrameFurnitureBeds449.00232.702021-08-31true
    orders12,000 rows · 7 columns
    • order_idinteger
    • customer_idinteger
    • order_datetimestamp
    • statusvarchar
    • channelvarchar
    • shipping_feedecimal
    • coupon_codevarchar
    First 5 rows of orders
    order_idcustomer_idorder_datestatuschannelshipping_feecoupon_code
    10011252023-01-01 20:51:19completedweb0.00NaN
    10021562023-01-02 07:58:46completedweb0.00NaN
    1003222023-01-02 10:25:27completedmarketplace4.99WELCOME10
    1004482023-01-02 11:21:51completedmarketplace4.99WELCOME10
    1005822023-01-02 11:43:36completedweb0.00WELCOME10
    order_items30,338 rows · 6 columns
    • order_item_idinteger
    • order_idinteger
    • product_idinteger
    • quantityinteger
    • unit_pricedecimal
    • discount_pctdecimal
    First 5 rows of order_items
    order_item_idorder_idproduct_idquantityunit_pricediscount_pct
    11001121931.990.00
    21001154311.990.00
    31002154411.990.00
    410021941149.000.00
    51002193219.990.00
    customers2,018 rows · 9 columns
    • customer_idinteger
    • first_namevarchar
    • last_namevarchar
    • emailvarchar
    • cityvarchar
    • statevarchar
    • signup_datedate
    • segmentvarchar
    • referral_sourcevarchar
    First 5 rows of customers
    customer_idfirst_namelast_nameemailcitystatesignup_datesegmentreferral_source
    1OwenHudsonowen.hudson@icloud.comSan FranciscoCA2022-06-02consumersearch
    2DouglasMendozadmendoza69@gmail.comAtlantaGA2022-06-02consumerNaN
    3HenryBrookshbrooks@lakesidecafe.comLos AngelesCA2022-06-04businessNaN
    4BrianGrahambgraham@bluecedarhomes.comSeattleWA2022-06-06businesspinterest
    5KyleJimenezkjimenez27@yahoo.comIndianapolisIN2022-06-07consumerNaN
    returns1,673 rows · 5 columns
    • return_idinteger
    • order_item_idinteger
    • returned_ondate
    • reasonvarchar
    • refund_amountdecimal
    First 5 rows of returns
    return_idorder_item_idreturned_onreasonrefund_amount
    1142023-01-10damaged83.69
    2422023-01-10wrong_item19.99
    31052023-01-14wrong_item10.99
    4242023-01-16changed_mind26.99
    51262023-01-16late20.99
    employees60 rows · 8 columns
    • employee_idinteger
    • first_namevarchar
    • last_namevarchar
    • departmentvarchar
    • titlevarchar
    • manager_idinteger
    • hire_datedate
    • salaryinteger
    First 5 rows of employees
    employee_idfirst_namelast_namedepartmenttitlemanager_idhire_datesalary
    1CatherineHaleExecutiveChief Executive OfficerNaN2016-03-01285000
    2RobertKimExecutiveChief Operating Officer12017-01-09235000
    3MeganCollinsSalesHead of Sales12018-04-16172000
    4ChloeGutierrezSalesAccount Executive32020-01-2172000
    5FrankBellSalesAccount Executive32023-05-2173000
    web_sessions40,000 rows · 7 columns
    • session_idinteger
    • customer_idinteger
    • started_attimestamp
    • devicevarchar
    • sourcevarchar
    • pages_viewedinteger
    • convertedboolean
    First 5 rows of web_sessions
    session_idcustomer_idstarted_atdevicesourcepages_viewedconverted
    1NaN2023-01-01 23:02:24mobilepaid1false
    2182023-01-01 15:07:49desktoppaid1false
    3NaN2023-01-01 16:55:46desktopdirect7false
    4NaN2023-01-01 12:43:28mobileorganic3false
    5NaN2023-01-01 13:06:21mobilesocial2false
    marketing_spend3,276 rows · 3 columns
    • spend_datedate
    • channelvarchar
    • amountdecimal
    First 5 rows of marketing_spend
    spend_datechannelamount
    2023-01-01paid492.99
    2023-01-01email37.38
    2023-01-01social200.53
    2023-01-02paid709.13
    2023-01-02email64.13

    The lesson

    Summing into a grid

    Counting is one use of pivot_table; summing a money column is the other everyday one. Only the answer to “what do I calculate?” (values) and “which operation?” (aggfunc) change:

    spend = marketing_spend.assign(year=marketing_spend["spend_date"].dt.year)
    spend.pivot_table(index="year", columns="channel", values="amount", aggfunc="sum").round(2)
    # channel      email       paid     social
    # year
    # 2023      28989.02  253948.40   99253.03
    # 2024      32876.47  294047.40  114970.96
    # 2025      38322.22  340778.33  131540.77

    The year comes from the date with .dt.year. The year labels are integers, so a column of a year-across grid is selected as grid[2025], not grid["2025"].

    Making the columns first

    A pivot can only use columns that exist. Revenue per category needs things the order lines don’t have yet: the category (in products), the order’s date and status (in orders) and the revenue itself. So the work happens before the pivot: merge, filter, compute the measure, then pivot.

    Several functions at once

    aggfunc also takes a list. Each function becomes its own block of columns:

    p = items.pivot_table(index="category", values="revenue",
                          aggfunc=["sum", "median", "count"])
    p.columns.tolist()
    # [('sum', 'revenue'), ('median', 'revenue'), ('count', 'revenue')]

    The column labels now have two levels: the function on top, the measured column underneath. When this game shows or checks such a table, the levels are joined with an underscore, so you see sum_revenue, median_revenue and count_revenue. In your own code you can do the same with p.columns = ["_".join(c) for c in p.columns], or select a block with p["median"].

    On all three years of completed orders, Decor has 7,407 lines with a median of $44.98, and Furniture only 1,450 lines with a median of $670.65. Sum alone would show two big categories; the median and the count show they are big in completely different ways.

    Why median next to sum

    The median is the middle line: half the lines are below it, half above. Unlike the mean, a handful of $10,000 business orders barely moves it, so it describes a typical purchase. Count tells you how much evidence each figure rests on. A total, a typical value and a count together answer most “how is this category doing?” questions.

    Common mistakes

    • Pricing lines with products["unit_price"]. That’s the current list price; the order line has the price the customer actually paid.
    • Forgetting aggfunc. The default is the mean, so each cell shows an average line.
    • Filtering on status or year after the pivot. The pivot no longer has those columns; filter first.
    • Expecting plain column names from a list of functions. They have two levels; join or select them.

    Pick what may run on this site. You can change this any time from "Cookie settings" at the bottom of every page.