1. Lighthouse

Level 7 · Is the app catching up with the website?

The request

From Jordan Reyes, Head of Marketing

“We pushed the app hard in 2025. I'd like one chart with a line per sales channel showing monthly revenue through the year. Did the app overtake the website?”

Your goal

Using order lines with quantity > 0 from orders with status 'completed' placed in 2025, set result to a DataFrame with one row per month and columns month (the first day of the month, a date), app, marketplace and web (revenue = sum of quantity * unit_price * (1 - discount_pct / 100), rounded to 2), sorted by month. Draw one line per channel with a legend.

  • Use the orders DataFramenot done yet
  • Use the order_items DataFramenot done yet
  • Show exactly month app marketplace webnot done yet

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

    Wide tables draw several lines

    pandas draws one line per column when you call .plot() on a DataFrame, with the index on the x-axis and a legend built from the column names. So a chart with a line per channel needs a table with a column per channel. The starter gives you one row per order line with a month column (the first day of each month); pivot_table turns it wide:

    wide = lines.pivot_table(index="month", columns="channel",
                             values="revenue", aggfunc="sum").round(2)
    #                  app  marketplace        web
    # 2025-01-01  38566.02     24323.19   87681.78
    # 2025-02-01  46574.43      8919.22   44409.50
    # ...
    ax = wide.plot(marker="o")

    .plot() returns the Axes, and from there it’s the usual matplotlib: ax.set_title, ax.set_ylabel, ax.set_ylim(0, None) to keep zero in view.

    Doing it by hand

    The same chart in plain matplotlib is one ax.plot per series, each with a label=, and a legend at the end:

    fig, ax = plt.subplots()
    for channel in ["app", "web", "marketplace"]:
        ax.plot(wide.index, wide[channel], marker="o", label=channel)
    ax.legend()

    ax.legend() only knows about lines that were given a label. Call it after the plotting, not before.

    What the lines say

    January started with the website far ahead: $87,681.78 against $38,566.02 for the app. From February the app led in every month except July and August, including the peaks of November ($118,601.68) and December ($130,471.52). Over the year the app brought $996,296.62 and the website $980,935.78, so Jordan’s answer is yes, narrowly. The marketplace stays a smaller, steady third line at $324,457.72 for the year.

    Keep the number of lines small. Three lines are easy to follow; eight lines in eight colors are spaghetti. With many groups, draw small separate charts (next levels) or highlight one line and gray out the rest.

    Common mistakes

    • Plotting a long table with x="month": every channel’s points go into one zigzag line.
    • Calling ax.legend() when no line has a label, which gives an empty legend.
    • Letting the y-axis start at the lowest value, which exaggerates the gaps between lines.
    • Leaving months as text like “2025-01”; real dates keep the axis in calendar order.
    • Forgetting .round(2) or rounding before summing: round the totals, never the line values.

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