1. Lighthouse

Level 1 · Gross margin by category

The request

From Laura Bennett, Head of Finance

“Thomas keeps saying Outdoor is our best category because it sells the most. I want to see what each category keeps after the cost of the goods, in dollars and as a percentage of sales, for the whole 2023 to 2025 history.”

Your goal

From lines (built in the starter: valid lines of booked orders, 2023 to 2025, with net and cogs), keep only lines with a known unit_cost. Set result to a DataFrame with one row per category and columns category, net_sales (sum of net), cogs (sum of cogs), gross_margin (net_sales − cogs) and margin_pct (gross_margin × 100 / net_sales), all rounded to 2, sorted by margin_pct from high to low.

  • Use the products DataFramenot done yet
  • Use the orders DataFramenot done yet
  • Show exactly category net_sales cogs gross_margin margin_pctnot 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

    What gross margin measures

    Gross margin is what is left of sales after paying for the goods that were sold:

    • Net sales: what customers paid for the items, after discounts (shipping is not included).
    • COGS (cost of goods sold): quantity × the unit cost of each product.
    • Gross margin = net sales − COGS, in dollars.
    • Gross margin % = gross margin ÷ net sales × 100.

    The dollar figure tells you how much a category contributes to paying salaries, rent and marketing. The percentage tells you how much of each sale the store keeps, which is what you compare across categories of very different size.

    Compute it from the sums, not the lines

    The margin % of a category is total margin divided by total sales, both summed first:

    g = costed.groupby("category")
    (g["net"].sum() - g["cogs"].sum()) * 100 / g["net"].sum()

    Averaging a per-line margin % would give every line the same weight, so a $12 candle would count as much as a $1,600 sofa. The sums weigh each line by its dollars, which is what finance means by the category’s margin.

    Missing costs are not zero costs

    Nine products have no unit_cost. If you leave them in, cogs is NaN on their lines and pandas sum() skips NaN, so their sales are counted with no cost against them. That turns 961 lines and $596,695 of net sales into pure profit. Drop those lines from both sides of the ratio instead, and write the coverage on the slide: “margin on 90% of net sales; 9 products have no cost on file”.

    lines["unit_cost"].isna().sum()   # lines with no cost

    Dollars and percent tell different stories

    On Harbor & Pine’s history Outdoor sells the most and keeps the least per dollar, yet still earns the most margin dollars. Textiles is the reverse: the smallest sales and the best rate. Neither number alone is “best category”. Put both on the table, sorted by the one the question is about.

    Common mistakes

    • Counting missing costs as zero. Margin looks higher than it is, and the error hides in a product that is easy to overlook.
    • Using list price instead of the price paid. products.unit_price is today’s price; the order line has what the customer actually paid.
    • Forgetting discounts. Margin on gross sales overstates what the store keeps on promotions.

    Interview note

    “What is the difference between gross margin and markup?” Margin divides profit by the selling price; markup divides it by the cost. A product bought for $50 and sold for $100 has a 50% margin and a 100% markup.

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