1. Lighthouse

Level 2 · Build the monthly review in pandas

The request

From Laura Bennett, Head of Finance

“We agreed November 2025, next to October and to November last year. Before you build the table, show me in numbers that November has settled and December hasn't, so I can explain why we skipped the latest month.”

Your goal

Step 1 of 3. Check the months. For orders placed from September 1 to December 31, 2025, set result to a DataFrame with columns month, orders (all orders), completed, cancelled, refunded and pending (orders with each status), sorted by month, default index.

  • Use the orders DataFramenot done yet
  • Show exactly month orders completed cancelled refunded pendingnot done yet
  • 4 rows, sorted as askednot 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 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.

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

        A month is ready to report only when its orders have settled. Orders arrive as pending and become completed, cancelled or refunded over the following days. A cross-tabulation of month against status shows where the open orders sit:

        counts = pd.crosstab(o["month"], o["status"])

        November has 2 pending orders out of 715; December has 94 out of 777. That is the evidence behind reporting November.

        One table per source

        The KPIs come from three tables at three grains: order lines, returns and web sessions. The safe pattern is to bring each to one row per month, indexed by month, then join them:

        sales = lines.groupby("month").agg(revenue=("revenue", "sum"), orders=("order_id", "nunique"), ...)
        refunds = returns.groupby(returns["returned_on"].dt.strftime("%Y-%m"))["refund_amount"].sum()
        kpi = sales.join(refunds.rename("refunds")).join(web)

        New buyers need one more step. “New” means the customer’s first completed order falls in the month. transform("min") puts each customer’s first order date on every one of their lines, and .where() keeps the customer_id only on lines in that first month, so nunique() of the result counts new buyers.

        Reading the result

        month net_revenue orders avg_order_value buyers web_conversion_pct
        2024-11 319,297.22 564 566.13 357 3.50
        2025-10 174,599.43 333 524.32 261 2.33
        2025-11 292,364.40 651 449.10 442 4.45

        Against October everything is up, as it is every November. Against last year, volume grew and revenue fell, so the question becomes average order value. Step 3 answers it: seven bulk orders last November, one this November.

        Explaining the mover

        Step 3 works at the order level, because the size of an order is only known once its lines are added up. Group the lines by month and order, label each order with np.where, then group the orders by month and band. The result splits each November into a few large orders and many regular ones, which is the breakdown that turns “revenue fell” into a reason leadership can act on.

        Pitfalls

        • groupby on a computed Series (returns["returned_on"].dt.strftime(...)) works without adding a column first; the result is indexed by the month labels.
        • Summing buyers across months double counts people who bought in both.
        • .loc[["2024-11", ...]] raises a KeyError if a month is missing from one source; join keeps the sales months and leaves NaN where another source has none.

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