1. Lighthouse

Level 2 · Check and compare the price rise in pandas

The request

From Thomas Whitaker, Head of Merchandising

“We agreed the question: for the products raised on March 1, units and revenue from March to August against the same months last year, next to the products we left alone. First show me you've found the right products and that the change really happened on March 1.”

Your goal

Step 1 of 2. Check the change. For every order line of the products whose price rose, label period ('before' if order_date < '2025-03-01', else 'after') and price_paid ('old' if the line's unit_price equals the product's lowest price, else 'new'). Set result to a DataFrame with columns period, price_paid and lines (the number of order lines), sorted by period descending, then price_paid descending, with a default index. Use all order lines, whatever the status.

  • Use the orders DataFramenot done yet
  • Use the order_items DataFramenot done yet
  • Show exactly period price_paid linesnot 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.

      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 you compare anything, prove that you are looking at the right products and the right date. Thomas says “about 30 products, March 1”. The data can confirm both, because order_items stores the price paid on every line, while products["unit_price"] only holds today’s list price.

      A product whose price changed has two different prices across its order lines. Named aggregation gives both in one pass, and a boolean mask keeps the products where they differ:

      prices = order_items.groupby("product_id", as_index=False).agg(
          old_price=("unit_price", "min"), new_price=("unit_price", "max")
      )
      prices = prices[prices["old_price"] != prices["new_price"]]
      len(prices)   # 29

      Step 1 then labels each of their lines twice, by date and by price, with np.where, and counts the combinations. If the change happened cleanly on March 1, only two combinations exist: before with the old price, after with the new one.

      Like for like, with a control group

      Step 2 answers the agreed question. Three choices make the comparison fair:

      • Same months, a year apart. March to August 2025 against March to August 2024.
      • Like-for-like products. Only products launched before January 1, 2024, so every product was on sale for the whole of both windows.
      • A control group. Every other product is labelled unchanged. Its growth is what the store did anyway.

      isin labels the groups in one line, and pivot_table puts each year in its own column, the pandas version of SUM(CASE WHEN ...):

      lines["price_group"] = np.where(lines["product_id"].isin(raised), "raised", "unchanged")
      u = lines.pivot_table(index="price_group", columns="yr", values="quantity", aggfunc="sum")

      The columns of u are the years as integers, so u[2025] - u[2024] subtracts row by row. Compute the percentages on these unrounded totals and round last.

      Reading the result

      price_group units_change_pct revenue_change_pct
      raised 21.0 44.3
      unchanged 9.2 13.4

      If the rise had hurt demand, raised products would grow less than unchanged ones. They grew more. Revenue grew faster than units because each unit earned about 6% more and because sales shifted towards the dearer items in the group.

      Pitfalls

      • Filter with < on the day after the window ends: d < "2025-09-01" keeps all of August 31.
      • Forgetting .copy() after filtering and then adding columns gives a SettingWithCopyWarning.
      • pivot_table sorts its index, so the result is already in price_group order; the explicit sort_values makes that a promise rather than a coincidence.

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