1. Lighthouse

Level 7 · Debug: 30,338 orders by segment?

The request

From Laura Bennett, Head of Finance

“Your shipping table by segment shows 30,338 orders and almost $59,000 in fees. We've had 12,000 orders in total, so something is double counting. Can you fix it?”

Your goal

The starter runs but counts some orders several times. Fix it so result is a DataFrame with one row per customer segment and columns segment, n_orders (number of orders), total_fees (sum of shipping_fee, rounded to 2) and avg_fee (mean shipping_fee per order, rounded to 2), over all orders, sorted by segment A to Z, index reset.

  • Use the customers DataFramenot done yet
  • Use the orders DataFramenot done yet
  • Show exactly segment n_orders total_fees avg_feenot done yet

Your code

Python

Loading the editor…

    ResultPreview of customers · press Run to see yours
    Preview of customers
    customer_idfirst_namelast_nameemailcity
    1OwenHudsonowen.hudson@icloud.comSan Francisco
    2DouglasMendozadmendoza69@gmail.comAtlanta
    3HenryBrookshbrooks@lakesidecafe.comLos Angeles
    4BrianGrahambgraham@bluecedarhomes.comSeattle

    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.

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

    Spot the fan-out from the row count

    Laura’s clue is a count: 30,338 “orders” when the store has 12,000. 30,338 is exactly the number of rows in order_items. That’s the fingerprint of a fan-out: the table was merged with order lines, so each order now appears once per line, and every order-level column, including shipping_fee, is repeated.

    len(orders)                                        # 12000
    len(orders.merge(order_items, on="order_id"))      # 30338

    The report never uses a column from order_items. The merge was probably copied from another script “just in case”, and it silently changed the grain from orders to order lines.

    Fix the grain

    Keep the table at one row per order. The segment comes from customers, and each order has exactly one customer, so that merge is many-to-one and doesn’t add rows:

    full = orders.merge(customers[["customer_id", "segment"]], on="customer_id", validate="many_to_one")
    len(full)   # 12000

    validate="many_to_one" makes pandas raise an error if a customer id appeared twice in customers, which would be a different fan-out. It costs nothing and documents what you expect.

    Why even the average was wrong

    You might think a mean survives the fan-out, since every fee is repeated. It doesn’t. Orders with many lines are repeated more often, and big multi-line baskets usually ship free, so the fanned-out mean leans toward $0. The starter gave $2.40 for consumers instead of $3.20, and $1.06 for business instead of $1.27. A fan-out quietly turns a plain average into a weighted one.

    The fixed table also tells a real story: business customers place bigger orders that mostly clear the free-shipping threshold, so they pay $1.27 per order on average against $3.20 for consumers. And the two order counts, 3,305 and 8,695, add up to exactly 12,000, the reconciliation Laura was missing.

    Common mistakes

    • Merging every table “in case it’s needed”. Each merge with a many-side table changes the grain.
    • Switching size to nunique for the order count but keeping the inflated sums.
    • Trusting an average after a fan-out: repeated rows change the weights.
    • Not comparing len() before and after a merge.

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