1. Lighthouse

Level 9 · Boss: the channel report for the board

The request

From Laura Bennett, Head of Finance

“For the board pack I need completed revenue by sales channel for 2023 to 2025 with 2025 growth, then a 2025 grid of orders by month and channel, January first. The board will ask two things: which channel led in January, and which month was busiest.”

Your goal

Step 1 of 4. Set result to a DataFrame with columns channel, 2023, 2024, 2025: the sum of completed line revenue per channel and year of order_date, rounded to 2. One row per channel, sorted A to Z.

  • Use the orders DataFramenot done yet
  • Use the order_items DataFramenot done yet
  • Show exactly channel 2023 2024 2025not 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 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 3 · 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 4 · 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.

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

          Two grids, one meeting

          This boss uses the chapter’s grid tools on a real board request. The data starts long: one row per order line or per order. The board reads grids, and asks questions about them.

          Each step runs on a fresh copy of the tables, so each one builds what it needs again:

          items = order_items.merge(orders[["order_id", "order_date", "status", "channel"]], on="order_id")
          items = items[items["status"] == "completed"].copy()
          items["revenue"] = items["quantity"] * items["unit_price"] * (1 - items["discount_pct"] / 100)
          items["year"] = items["order_date"].dt.year

          Growth in wide form

          With channel down the side and year across, completed revenue totals 1,600,511.41 for 2023, 1,993,605.32 for 2024 and 2,300,170.84 for 2025. Each year is a column, so growth is column arithmetic, computed for every channel at once:

          grid["growth_2025"] = grid[2025] / grid[2024] - 1

          The labels came from dt.year, so they’re integers. Compute growth on the unrounded totals, then round. Sorted by growth, the story is plain: the app grew 53.1% and became the biggest channel, the marketplace grew 13.3%, web fell 7.2%.

          Months in calendar order

          dt.month_name() turns a date into “January”, “February” and so on, which is what a reader wants to see. The catch: pandas sorts text alphabetically, so a pivot on month names comes out April, August, December, February… Two fixes:

          months = ["January", "February", "March", "April", "May", "June", "July",
                    "August", "September", "October", "November", "December"]
          grid.reindex(months)                     # rows in the order of the list

          or keep the month number (dt.month) next to the name, sort by the number, and then set the name as the index. reindex is shorter when you already have the list; it also adds a row of NaN for any month that’s missing, which makes gaps visible instead of silently dropping them.

          Reading the grid: idxmax and sum(axis=1)

          Two short methods answer most “which one?” questions:

          • .idxmax() on a Series returns the label of its largest value (.max() returns the value). grid.loc["January"].idxmax() is the channel that sold most in January.
          • grid.sum(axis=1) adds across each row (one total per month); grid.sum() adds down each column (one total per channel). Chain .idxmax() to name the biggest.
          grid.idxmax(axis=1)        # the leading channel in every month

          In 2025 the app led nine months, but web led January, June and August. A headline like “the app leads every month” would be wrong, and these two lines are how you check it.

          Common mistakes

          • Pricing lines with products["unit_price"] instead of the price on the order line.
          • Writing grid["2025"] for an integer column label.
          • Leaving months in alphabetical order.
          • Using .max() when the question asks which month or channel: that’s .idxmax().
          • Summing with axis=0 when you want one total per row.

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