1. Lighthouse

Level 2 · Find the slow movers in pandas

The request

From Thomas Whitaker, Head of Merchandising

“We agreed it: active products on sale all of 2025, each one against its own category, the weakest three per category. First show me which products you're judging and which you're leaving out, and whether anything didn't sell at all.”

Your goal

Step 1 of 3. Check which products can be judged. Label every product product_group: 'retired' if is_active is False, else 'new in 2025' if launched_on >= '2025-01-01', else 'established'. Set result to a DataFrame with columns product_group, products (the number of products), sold_in_2025 (how many had any 2025 sales) and missing_cost (how many have a missing unit_cost), sorted by product_group, with a default index.

  • Use the orders DataFramenot done yet
  • Use the order_items DataFramenot done yet
  • Show exactly product_group products sold_in_2025 missing_costnot done yet

Step 1 · your code

Python

Loading the editor…

    ResultPreview of orders · press Run to see yours
    Preview of orders
    order_idcustomer_idorder_datestatuschannel
    10011252023-01-01 20:51:19completedweb
    10021562023-01-02 07:58:46completedweb
    1003222023-01-02 10:25:27completedmarketplace
    1004482023-01-02 11:21:51completedmarketplace

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

      Step 3 · 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.

        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
        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 you are checking, and why

        Before ranking anything, decide which products can fairly be judged. Three groups behave differently:

        • Retired products are already leaving the range; they belong to a different review.
        • New in 2025 products had only part of the year on sale. A lantern launched in August cannot be compared with one that sold all summer.
        • Established products were on sale for all twelve months: these are the ones to rank.

        Step 1 starts from products and merges the 2025 sales onto it with how="left", so a product that sold nothing would still be there, with NaN units. np.select then labels each product, taking the first condition that is true:

        p["product_group"] = np.select(
            [~p["is_active"], p["launched_on"] >= "2025-01-01"],
            ["retired", "new in 2025"],
            default="established",
        )

        In the aggregation, ("units", "count") counts the products that sold, and a small lambda counts missing unit costs, because a slow product with an unknown cost is one whose margin nobody can judge.

        Ranking inside each category

        Step 2 needs the category’s total on every product row. transform returns one value per row, aligned with the original table, where a plain sum() would return one row per category:

        t["share"] = t["revenue"] * 100 / t.groupby("category")["revenue"].transform("sum")
        low = t.sort_values(["category", "revenue"]).groupby("category").head(3)

        head(3) after a sort is the pandas way of saying “the first three per group”. Step 3 uses the same trick, then sums the weakest three and divides by the category total.

        Reading the result

        category weakest product units_2025 category_share_pct
        Furniture Upland Oak Bookcase 9 0.7
        Textiles Spruce Wool Duvet Cover 25 0.7
        Outdoor Orchard Aluminum Adirondack Chair 93 1.0
        Lighting Fern Rattan Desk Lamp 13 1.2

        Revenue share and units together tell the story. The bookcase and the desk lamp sold almost nothing; the Adirondack chair sold 93 units but at $51.99 each, so it barely registers in a category of patio sets and grills.

        Pitfalls

        • is_active is a boolean column, so ~p["is_active"] flips it; p["is_active"] == "false" matches nothing.
        • groupby().head(3) keeps the first three rows in the current order, so sort first.
        • A share of revenue says nothing about profit. Seven established products have no unit cost, so their margin is unknown.

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