1. Faro

Nivel 10 · Desafío final: ¿de dónde vienen las devoluciones?

La solicitud

De Angela Rivera, Directora de Atención al Cliente

“Las devoluciones nos están comiendo la semana. ¿Qué categorías se devuelven más, cuáles tienen la tasa de devolución más alta una vez que tienes en cuenta cuánto venden, y qué canal de venta carga con los reembolsos?”

Tu objetivo

Paso 1 de 3. Asigna a result un DataFrame con las columnas category y n_returns (número de devoluciones cuya línea de pedido está en esa categoría), ordenado por n_returns descendente y luego por category ascendente, con el índice reiniciado.

  • Usa el DataFrame productspendiente
  • Usa el DataFrame order_itemspendiente
  • Muestra exactamente category n_returnspendiente

Paso 1 · tu código

Python

Cargando el editor…

    ResultadoVista previa de products · presiona Ejecutar para ver el tuyo
    Vista previa de products
    product_idnamecategorysubcategoryunit_price
    101Vale Velvet LoveseatFurnitureSofas1659.00
    102Alder Oak Coffee TableFurnitureTables919.00
    103Orchard Rattan Dining ChairFurnitureChairs789.00
    104Upland Oak BookcaseFurnitureStorage429.00

    Mote revisa tu resultado

    Presiona Ejecutar. Cada fila que cumple el objetivo se vuelve una luciérnaga en tu linterna.

    Paso 2 · tu código

    Python

    Cargando el editor…

      ResultadoVista previa de products · presiona Ejecutar para ver el tuyo
      Vista previa de products
      product_idnamecategorysubcategoryunit_price
      101Vale Velvet LoveseatFurnitureSofas1659.00
      102Alder Oak Coffee TableFurnitureTables919.00
      103Orchard Rattan Dining ChairFurnitureChairs789.00
      104Upland Oak BookcaseFurnitureStorage429.00

      Mote revisa tu resultado

      Presiona Ejecutar. Cada fila que cumple el objetivo se vuelve una luciérnaga en tu linterna.

      Paso 3 · tu código

      Python

      Cargando el editor…

        ResultadoVista previa de orders · presiona Ejecutar para ver el tuyo
        Vista previa de 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 revisa tu resultado

        Presiona Ejecutar. Cada fila que cumple el objetivo se vuelve una luciérnaga en tu linterna.

        DataFrames

        Cargados como DataFrames de pandas, junto con pd y np.

        products120 filas · 8 columnas
        • product_idinteger
        • namevarchar
        • categoryvarchar
        • subcategoryvarchar
        • unit_pricedecimal
        • unit_costdecimal
        • launched_ondate
        • is_activeboolean
        Primeras 5 filas de 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
        order_items30,338 filas · 6 columnas
        • order_item_idinteger
        • order_idinteger
        • product_idinteger
        • quantityinteger
        • unit_pricedecimal
        • discount_pctdecimal
        Primeras 5 filas de 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 filas · 5 columnas
        • return_idinteger
        • order_item_idinteger
        • returned_ondate
        • reasonvarchar
        • refund_amountdecimal
        Primeras 5 filas de 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
        orders12,000 filas · 7 columnas
        • order_idinteger
        • customer_idinteger
        • order_datetimestamp
        • statusvarchar
        • channelvarchar
        • shipping_feedecimal
        • coupon_codevarchar
        Primeras 5 filas de 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
        customers2,018 filas · 9 columnas
        • customer_idinteger
        • first_namevarchar
        • last_namevarchar
        • emailvarchar
        • cityvarchar
        • statevarchar
        • signup_datedate
        • segmentvarchar
        • referral_sourcevarchar
        Primeras 5 filas de 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 filas · 8 columnas
        • employee_idinteger
        • first_namevarchar
        • last_namevarchar
        • departmentvarchar
        • titlevarchar
        • manager_idinteger
        • hire_datedate
        • salaryinteger
        Primeras 5 filas de 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 filas · 7 columnas
        • session_idinteger
        • customer_idinteger
        • started_attimestamp
        • devicevarchar
        • sourcevarchar
        • pages_viewedinteger
        • convertedboolean
        Primeras 5 filas de 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 filas · 3 columnas
        • spend_datedate
        • channelvarchar
        • amountdecimal
        Primeras 5 filas de marketing_spend
        spend_datechannelamount
        2023-01-01paid492.99
        2023-01-01email37.38
        2023-01-01social200.53
        2023-01-02paid709.13
        2023-01-02email64.13

        La lección

        Primero dibuja la ruta

        returns solo conoce el order_item_id que volvió. Todo lo demás está a un join de distancia:

        returns --order_item_id--> order_items --product_id--> products   (category)
                                               --order_id----> orders     (channel)

        Esbozar esta ruta antes de escribir código es un buen hábito para cualquier pregunta con varias tablas. Fíjate en el grano de cada salto: como máximo una devolución por línea (one-to-one), muchas líneas por producto y por pedido (many-to-one). Esos son exactamente los valores de validate= que debes usar.

        Paso 1: recuentos por categoría

        returned = (
            returns
            .merge(order_items[["order_item_id", "product_id"]], on="order_item_id", validate="one_to_one")
            .merge(products[["product_id", "category"]], on="product_id", validate="many_to_one")
        )
        returned.groupby("category", as_index=False).agg(n_returns=("return_id", "count"))

        len(returned) debería ser igual a len(returns). Si no lo es, algún join perdió o duplicó devoluciones.

        Paso 2: tasas, no recuentos

        Los recuentos brutos favorecen lo que más se vende. Una comparación justa divide por lo que vendió cada categoría. Eso necesita un denominador sacado de todas las líneas de pedido, no solo las devueltas:

        lines = order_items.merge(products[["product_id", "category"]], on="product_id")
        lines = lines.assign(is_returned=lines["order_item_id"].isin(returns["order_item_id"]))

        La marca is_returned mantiene la tabla en el grano de línea, sin ningún merge que pueda multiplicar filas. Luego un solo groupby da ambos recuentos, ya que la suma de una columna booleana cuenta sus valores True:

        (
            lines.groupby("category", as_index=False)
            .agg(n_lines=("order_item_id", "count"), n_returns=("is_returned", "sum"))
            .assign(return_rate=lambda d: (d["n_returns"] / d["n_lines"]).round(3))
        )

        assign con una lambda te permite calcular una columna nueva a partir de la tabla agregada en la misma cadena. El orden por tasa puede diferir del orden por recuento, y esa diferencia es la conclusión: una categoría puede generar muchas devoluciones simplemente porque vende mucho, mientras que otra se devuelve con una frecuencia inusual para su volumen.

        Las devoluciones aquí incluyen todas las líneas de pedidos reembolsados, así que la tasa mezcla dos causas: reembolsos de pedidos completos y artículos devueltos individualmente. Un análisis cuidadoso las separaría; menciónalo si presentas esto.

        Paso 3: dinero por canal

        refunds = (
            returns
            .merge(order_items[["order_item_id", "order_id"]], on="order_item_id")
            .merge(orders[["order_id", "channel"]], on="order_id")
        )
        refunds.groupby("channel", as_index=False).agg(total_refund=("refund_amount", "sum"))

        refund_amount es un valor por devolución, así que sumarlo en una tabla con una fila por devolución es seguro. Compara el total con returns["refund_amount"].sum() para confirmarlo.

        Errores comunes

        • Dividir las devoluciones entre el número de productos o de pedidos en lugar de líneas de pedido.
        • Obtener tasas de devolución superiores a 1, señal segura de un fan-out en el numerador.
        • Usar count sobre is_returned, que cuenta todas las líneas, en lugar de sum.

        Elige qué puede ejecutarse en este sitio. Puedes cambiarlo cuando quieras desde "Configurar cookies", al pie de cada página.