1. Lighthouse

Level 6 · Boss: find the biggest baskets

The request

From Megan Collins, Head of Sales

“I want to understand our big baskets. Give me our average order value, the ten biggest orders ever, and how much of our line revenue was sold at a discount.”

Your goal

Step 1 of 3. Build revenue per order (the sum of line revenue for each order_id in order_items). Set result to the average revenue per order, a float rounded to 2.

  • Use the order_items DataFramenot done yet
  • Store a number in resultnot done yet

Step 1 · your code

Python

Loading the editor…

    ResultPreview of order_items · press Run to see yours
    Preview of order_items
    order_item_idorder_idproduct_idquantityunit_price
    11001121931.99
    21001154311.99
    31002154411.99
    410021941149.00

    Mote checks your result

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

    Step 2 · your code

    Python

    Loading the editor…

      ResultPreview of order_items · press Run to see yours
      Preview of order_items
      order_item_idorder_idproduct_idquantityunit_price
      11001121931.99
      21001154311.99
      31002154411.99
      410021941149.00

      Mote checks your result

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

      Step 3 · your code

      Python

      Loading the editor…

        ResultPreview of order_items · press Run to see yours
        Preview of order_items
        order_item_idorder_idproduct_idquantityunit_price
        11001121931.99
        21001154311.99
        31002154411.99
        410021941149.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.

        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
        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
        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

        Changing the grain

        order_items has one row per order line. Megan’s questions are about orders. Moving from one grain to a coarser one is what groupby is for: many lines in, one row per order out.

        items = order_items.assign(
            line_revenue=order_items["quantity"] * order_items["unit_price"]
            * (1 - order_items["discount_pct"] / 100)
        )
        per_order = items.groupby("order_id", as_index=False).agg(revenue=("line_revenue", "sum"))

        Check the grain afterwards: per_order["order_id"].is_unique should be True, and len(per_order) should equal order_items["order_id"].nunique(). Both are quick and they catch grouping on the wrong column.

        The mean of per_order["revenue"] is the average order value (AOV), one of the most watched numbers in e-commerce. Note that it’s the mean of order totals, not the mean of line revenue: items["line_revenue"].mean() is the average line, about two and a half times smaller.

        Each boss step runs on a fresh copy of the data, which is why every step computes line revenue again. In a notebook you’d compute it once and reuse it.

        Ranking the groups

        The ten biggest orders are a sort on the aggregated table:

        per_order.sort_values(["revenue", "order_id"], ascending=[False, True]).head(10)

        The biggest baskets at Harbor & Pine come from business accounts buying furniture in quantity. That’s a useful thing to say out loud: a “top orders” list is often a list of a few B2B customers, and averages across all orders hide that segment entirely. Your next question as an analyst would be who placed them, which needs a join with orders and customers (next chapter).

        A share of a total

        “How much revenue was sold at a discount?” is a ratio of two sums: discounted revenue over all revenue. You don’t need a groupby; a mask selects the discounted lines:

        line_revenue = order_items["quantity"] * order_items["unit_price"] * (1 - order_items["discount_pct"] / 100)
        share = line_revenue[order_items["discount_pct"] > 0].sum() / line_revenue.sum()

        The mask comes from order_items and is applied to line_revenue. That works because both share the same index, so pandas lines the rows up by label. This alignment is a pandas superpower and an occasional trap: if you’d filtered or re-sorted one of them, the labels would still match, but reset indexes would not.

        Notice this is a share of revenue, not of lines. A share of discounted lines, (order_items["discount_pct"] > 0).mean(), answers a different question. Here about 12.9% of lines carry a discount but they bring in only about 10.7% of revenue, partly because the discount itself shrinks them. Say which one you report.

        About the mess

        This level uses every line as it is, including a handful of lines with zero or negative quantities. Those are data-entry errors you’ll clean in chapter 8. Their effect here is tiny, but in a real report you’d clean first and say so.

        Common mistakes

        • Summing unit_price instead of line revenue.
        • Ranking order_items rows (single lines) instead of orders.
        • Taking the mean of discount_pct as “share of discounted revenue”.