1. Faro
  2. Faro

Nivel 2 · Retención de clientes en pandas

La solicitud

De Catherine Hale, CEO

“Priya dice que los números de retención deberían vivir en un cuaderno que podamos volver a ejecutar cada trimestre. Por favor, construye las tablas de cohortes, de 90 días y de segmentos en pandas, iguales a la versión en SQL de este caso.”

Tu objetivo

Paso 1 de 3. Asigna a result un DataFrame con las columnas cohort_year (entero, año del primer pedido completado), customers, repeat_customers (2 o más pedidos completados) y repeat_rate_pct (repeat_customers * 100 / customers, redondeado a 2), ordenado por cohort_year y con índice por defecto.

  • Usa el DataFrame orderspendiente
  • Muestra exactamente cohort_year customers repeat_customers repeat_rate_pctpendiente
  • 3 filas, ordenadas como se pidependiente

Paso 1 · 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.

    Paso 2 · 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.

      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.

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

        La misma pregunta, las mismas definiciones

        La revisión de retención de Catherine en pandas, con las mismas definiciones que el nivel de SQL de este caso: solo pedidos completados, cohortes por el año del primer pedido completado, «recurrente» significa dos o más pedidos completados, y la tasa de 90 días solo cuenta a los clientes que tuvieron 90 días para volver.

        Paso 1: una fila por cliente

        Una fila por cliente es un groupby sin as_index=False, así que customer_id queda como índice:

        completed = orders[orders["status"] == "completed"]
        per_customer = completed.groupby("customer_id").agg(
            first_order=("order_date", "min"), n_orders=("order_id", "count")
        )

        A partir de aquí, cada pregunta es un segundo groupby sobre esta tabla pequeña. Una columna booleana is_repeat hace legible el conteo: "size" cuenta a todos los clientes de la cohorte, "sum" cuenta los valores True. .dt.year da un año entero, igual que year() en SQL.

        Mira la cohorte de 2025 antes de seguir: su tasa de recompra de toda la historia es mucho más baja. Eso es censura por la derecha, no necesariamente un desplome. Alguien que compró por primera vez en noviembre de 2025 ha tenido unas semanas para volver, un cliente de 2023 tres años, así que cualquier tasa de toda la historia es injusta con las cohortes recientes. El paso 2 lo arregla con una ventana fija de 90 días.

        Paso 2: el segundo pedido con cumcount

        Para encontrar el segundo pedido de cada cliente, numera sus pedidos. groupby(...).cumcount() numera las filas dentro de cada grupo como 0, 1, 2, … en el orden en que están en ese momento, así que primero ordena y luego numera:

        completed = orders[orders["status"] == "completed"].sort_values(["customer_id", "order_date", "order_id"])
        completed["n"] = completed.groupby("customer_id").cumcount()   # 0, 1, 2, ...

        cumcount empieza en 0, así que el primer pedido es n == 0 y el segundo es n == 1. Ordenar también por order_id además de la fecha resuelve los empates exactamente como lo hace la versión en SQL.

        pivot(index="customer_id", columns="n", values="order_date") convierte las dos filas por cliente en dos columnas. Es pivot_table sin agregación, para cuando cada par índice/columna tiene un solo valor. Las columnas nuevas se llaman como los valores de n, 0 y 1, así que .rename(columns={0: "first_order", 1: "second_order"}) les da nombres legibles (el nombre viejo a la izquierda, el nuevo a la derecha). Los compradores de una sola vez reciben NaT (marca de tiempo faltante) como segundo pedido. Las comparaciones con NaT devuelven False, así que no pasan la prueba de 90 días sin código adicional.

        pd.Timedelta(days=90) es una duración, exactamente 90 × 24 horas, y sumarla a un datetime lo adelanta: pd.Timestamp("2025-01-10 14:00") + pd.Timedelta(days=90) es 2025-04-10 14:00. Es la forma en pandas del INTERVAL 90 DAY de SQL. Usar (second - first).dt.days <= 90 también sería razonable, pero puede diferir en un día del intervalo de SQL cerca de la medianoche; usar la misma aritmética en ambos es lo que hace que los números coincidan.

        Paso 3: merge para el segmento

        customers tiene una fila por cliente y per_customer también, así que validate="one_to_one" debería pasar, y pasa. Si los registros de clientes duplicados (ids 2001–2018) tuvieran pedidos, aquí es donde aparecerían; no los tienen, pero la comprobación no cuesta nada.

        ("n_orders", "mean") da los pedidos por cliente, igual que el AVG(n_orders) de SQL. Selecciona las columnas en el orden que lista el ejercicio antes de devolver el resultado.

        SQL y pandas lado a lado

        Idea SQL pandas
        clientes de una cohorte COUNT(*) ("is_repeat", "size")
        recurrentes COUNT(*) FILTER (WHERE n_orders >= 2) ("is_repeat", "sum")
        n-ésimo pedido ROW_NUMBER() sort_values + cumcount()
        filas a columnas MAX(CASE WHEN rn = 2 ...) pivot(columns="n")
        segundo pedido ausente NULL NaT

        Errores comunes

        • Olvidar ordenar antes de cumcount. Numera las filas en el orden en que están, que después de un filtro o un merge no está garantizado que sea por fecha.
        • count frente a size. "count" omite los valores faltantes, "size" no. En una columna sin valores faltantes coinciden; sabe cuál quieres decir.
        • Elegibilidad sobre la fecha equivocada. Filtra por el primer pedido, no por la fecha de registro: los clientes se registran antes de comprar, a veces meses antes.

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