1. Lighthouse

Level 2 · Measure the return rate in pandas

The request

From Angela Rivera, Head of Customer Support

“We agreed it: returned lines over sold lines for completed orders, by category, plus the refund value and the reasons. Before you split anything, show me how returns sit across order statuses, because I know the refunded orders get processed differently.”

Your goal

Step 1 of 3. Check where returns sit. For every order line, whatever its quantity, set result to a DataFrame with columns status, lines (the number of order lines), returned_lines (lines whose order_item_id appears in returns) and return_rate_pct (returned_lines * 100 / lines, rounded to 1), sorted by status, with a default index.

  • Use the orders DataFramenot done yet
  • Use the order_items DataFramenot done yet
  • Show exactly status lines returned_lines return_rate_pctnot 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
        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
        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
        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 compute a rate, look at what the returns belong to. Every return row points to one order line, and every line belongs to an order with a status. If returns behave very differently by status, one rate for everything would hide that.

        The quickest way to mark returned lines is isin, which gives one True or False per line:

        lines["returned"] = lines["order_item_id"].isin(returns["order_item_id"])

        Summing a boolean column counts the True values, so ("returned", "sum") in a named aggregation is the number of returned lines. The result of step 1 shows refunded orders at 96.6% and completed orders at 3.3%: two different processes, which is why the rest of the case uses completed orders only.

        The rate and the money

        Step 2 needs the refund amount too, so it merges returns with how="left". Every sold line stays; returned lines get their refund_amount, the others get NaN. Two aggregations then do the counting for you:

        • ("order_item_id", "size") counts every line, the denominator;
        • ("refund_amount", "count") counts non-missing values, the returned lines.

        From the same table come the return rate, which tells Support where the work is, and the refund share, refunds divided by net sales, which tells Finance where the money goes. Compute both percentages from the unrounded sums and round last.

        Reasons as columns

        Step 3 starts from returns, since only returned lines have a reason. pd.crosstab counts every category and reason pair, and normalize="index" turns each row into shares of that row:

        shares = pd.crosstab(r["category"], r["reason"], normalize="index") * 100

        Each row then adds up to 100%, the pandas version of dividing by the category’s own COUNT(*).

        Reading the result

        category return_rate_pct refund_share_pct top reason
        Textiles 4.7 5.1 changed_mind 57.9%
        Furniture 4.4 4.7 damaged 46.0%
        Lighting 3.8 4.3 damaged 38.3%
        Kitchen 2.0 2.5 changed_mind 40.6%

        Two different problems show up. Textiles come back because customers change their minds: color, feel or size not as expected. Furniture and Lighting come back broken. Those need different fixes.

        Pitfalls

        • merge is an inner join unless you say how="left"; with returns that silently drops every line that was not returned.
        • "count" skips NaN and "size" does not; mixing them up gives a rate of 100% or of 0%.
        • Some changed-mind refunds on Furniture and Outdoor are 15% below the line value: a restocking fee. Compare refunds with sales, not with returned line values, when you talk about money lost.

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