La solicitud

De Catherine Hale, CEO
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_yearcustomersrepeat_customersrepeat_rate_pctpendiente - 3 filas, ordenadas como se pidependiente
Paso 2 de 3. Solo son elegibles los clientes cuyo primer pedido completado es anterior a 2025-10-01. Asigna a result un DataFrame con las columnas cohort_year, eligible_customers, repeat_90d (clientes elegibles cuyo segundo pedido completado ocurre como máximo 90 días después del primero: second_order <= first_order + pd.Timedelta(days=90)) y repeat_90d_pct (repeat_90d * 100 / eligible_customers, redondeado a 2), ordenado por cohort_year y con índice por defecto. Ordena los pedidos de cada cliente por order_date y luego por order_id.
- Usa el DataFrame
orderspendiente - Muestra exactamente
cohort_yeareligible_customersrepeat_90drepeat_90d_pctpendiente - 3 filas, ordenadas como se pidependiente
Paso 3 de 3. Asigna a result un DataFrame con las columnas segment, customers (clientes con al menos un pedido completado), repeat_customers (2 o más pedidos completados), repeat_rate_pct (repeat_customers * 100 / customers, redondeado a 2) y avg_orders (pedidos completados por cliente, redondeado a 2), ordenado por segment y con índice por defecto.
- Usa el DataFrame
orderspendiente - Muestra exactamente
segmentcustomersrepeat_customersrepeat_rate_pctavg_orderspendiente - 2 filas, ordenadas como se pidependiente
Este caso se desbloquea cuando terminas:
La solicitud y la lección ya se pueden leer. Resuelve todos los niveles de esos capítulos, en cualquier orden, y este caso se abre.
Paso 1 · tu código
Cargando el editor…
Solución de referencia
| order_id | customer_id | order_date | status | channel |
|---|---|---|---|---|
| 1001 | 125 | 2023-01-01 20:51:19 | completed | web |
| 1002 | 156 | 2023-01-02 07:58:46 | completed | web |
| 1003 | 22 | 2023-01-02 10:25:27 | completed | marketplace |
| 1004 | 48 | 2023-01-02 11:21:51 | completed | marketplace |
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
Cargando el editor…
Solución de referencia
| order_id | customer_id | order_date | status | channel |
|---|---|---|---|---|
| 1001 | 125 | 2023-01-01 20:51:19 | completed | web |
| 1002 | 156 | 2023-01-02 07:58:46 | completed | web |
| 1003 | 22 | 2023-01-02 10:25:27 | completed | marketplace |
| 1004 | 48 | 2023-01-02 11:21:51 | completed | marketplace |
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
Cargando el editor…
Solución de referencia
| order_id | customer_id | order_date | status | channel |
|---|---|---|---|---|
| 1001 | 125 | 2023-01-01 20:51:19 | completed | web |
| 1002 | 156 | 2023-01-02 07:58:46 | completed | web |
| 1003 | 22 | 2023-01-02 10:25:27 | completed | marketplace |
| 1004 | 48 | 2023-01-02 11:21:51 | completed | marketplace |
Mote revisa tu resultado
Presiona Ejecutar. Cada fila que cumple el objetivo se vuelve una luciérnaga en tu linterna.
Lo que recibe Catherine, y por qué
DataFrames
Cargados como DataFrames de pandas, junto con pd y np.
orders12,000 filas · 7 columnas
order_idintegercustomer_idintegerorder_datetimestampstatusvarcharchannelvarcharshipping_feedecimalcoupon_codevarchar
| order_id | customer_id | order_date | status | channel | shipping_fee | coupon_code |
|---|---|---|---|---|---|---|
| 1001 | 125 | 2023-01-01 20:51:19 | completed | web | 0.00 | NaN |
| 1002 | 156 | 2023-01-02 07:58:46 | completed | web | 0.00 | NaN |
| 1003 | 22 | 2023-01-02 10:25:27 | completed | marketplace | 4.99 | WELCOME10 |
| 1004 | 48 | 2023-01-02 11:21:51 | completed | marketplace | 4.99 | WELCOME10 |
| 1005 | 82 | 2023-01-02 11:43:36 | completed | web | 0.00 | WELCOME10 |
customers2,018 filas · 9 columnas
customer_idintegerfirst_namevarcharlast_namevarcharemailvarcharcityvarcharstatevarcharsignup_datedatesegmentvarcharreferral_sourcevarchar
| customer_id | first_name | last_name | city | state | signup_date | segment | referral_source | |
|---|---|---|---|---|---|---|---|---|
| 1 | Owen | Hudson | owen.hudson@icloud.com | San Francisco | CA | 2022-06-02 | consumer | search |
| 2 | Douglas | Mendoza | dmendoza69@gmail.com | Atlanta | GA | 2022-06-02 | consumer | NaN |
| 3 | Henry | Brooks | hbrooks@lakesidecafe.com | Los Angeles | CA | 2022-06-04 | business | NaN |
| 4 | Brian | Graham | bgraham@bluecedarhomes.com | Seattle | WA | 2022-06-06 | business | |
| 5 | Kyle | Jimenez | kjimenez27@yahoo.com | Indianapolis | IN | 2022-06-07 | consumer | NaN |
products120 filas · 8 columnas
product_idintegernamevarcharcategoryvarcharsubcategoryvarcharunit_pricedecimalunit_costdecimallaunched_ondateis_activeboolean
| product_id | name | category | subcategory | unit_price | unit_cost | launched_on | is_active |
|---|---|---|---|---|---|---|---|
| 101 | Vale Velvet Loveseat | Furniture | Sofas | 1659.00 | NaN | 2023-09-11 | true |
| 102 | Alder Oak Coffee Table | Furniture | Tables | 919.00 | 503.50 | 2024-05-16 | true |
| 103 | Orchard Rattan Dining Chair | Furniture | Chairs | 789.00 | 429.87 | 2023-05-27 | true |
| 104 | Upland Oak Bookcase | Furniture | Storage | 429.00 | 214.70 | 2022-12-20 | true |
| 105 | Avery Leather Bed Frame | Furniture | Beds | 449.00 | 232.70 | 2021-08-31 | true |
order_items30,338 filas · 6 columnas
order_item_idintegerorder_idintegerproduct_idintegerquantityintegerunit_pricedecimaldiscount_pctdecimal
| order_item_id | order_id | product_id | quantity | unit_price | discount_pct |
|---|---|---|---|---|---|
| 1 | 1001 | 121 | 9 | 31.99 | 0.00 |
| 2 | 1001 | 154 | 3 | 11.99 | 0.00 |
| 3 | 1002 | 154 | 4 | 11.99 | 0.00 |
| 4 | 1002 | 194 | 1 | 149.00 | 0.00 |
| 5 | 1002 | 193 | 2 | 19.99 | 0.00 |
returns1,673 filas · 5 columnas
return_idintegerorder_item_idintegerreturned_ondatereasonvarcharrefund_amountdecimal
| return_id | order_item_id | returned_on | reason | refund_amount |
|---|---|---|---|---|
| 1 | 14 | 2023-01-10 | damaged | 83.69 |
| 2 | 42 | 2023-01-10 | wrong_item | 19.99 |
| 3 | 105 | 2023-01-14 | wrong_item | 10.99 |
| 4 | 24 | 2023-01-16 | changed_mind | 26.99 |
| 5 | 126 | 2023-01-16 | late | 20.99 |
employees60 filas · 8 columnas
employee_idintegerfirst_namevarcharlast_namevarchardepartmentvarchartitlevarcharmanager_idintegerhire_datedatesalaryinteger
| employee_id | first_name | last_name | department | title | manager_id | hire_date | salary |
|---|---|---|---|---|---|---|---|
| 1 | Catherine | Hale | Executive | Chief Executive Officer | NaN | 2016-03-01 | 285000 |
| 2 | Robert | Kim | Executive | Chief Operating Officer | 1 | 2017-01-09 | 235000 |
| 3 | Megan | Collins | Sales | Head of Sales | 1 | 2018-04-16 | 172000 |
| 4 | Chloe | Gutierrez | Sales | Account Executive | 3 | 2020-01-21 | 72000 |
| 5 | Frank | Bell | Sales | Account Executive | 3 | 2023-05-21 | 73000 |
web_sessions40,000 filas · 7 columnas
session_idintegercustomer_idintegerstarted_attimestampdevicevarcharsourcevarcharpages_viewedintegerconvertedboolean
| session_id | customer_id | started_at | device | source | pages_viewed | converted |
|---|---|---|---|---|---|---|
| 1 | NaN | 2023-01-01 23:02:24 | mobile | paid | 1 | false |
| 2 | 18 | 2023-01-01 15:07:49 | desktop | paid | 1 | false |
| 3 | NaN | 2023-01-01 16:55:46 | desktop | direct | 7 | false |
| 4 | NaN | 2023-01-01 12:43:28 | mobile | organic | 3 | false |
| 5 | NaN | 2023-01-01 13:06:21 | mobile | social | 2 | false |
marketing_spend3,276 filas · 3 columnas
spend_datedatechannelvarcharamountdecimal
| spend_date | channel | amount |
|---|---|---|
| 2023-01-01 | paid | 492.99 |
| 2023-01-01 | 37.38 | |
| 2023-01-01 | social | 200.53 |
| 2023-01-02 | paid | 709.13 |
| 2023-01-02 | 64.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. countfrente asize."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.
¡Linterna encendida!
Practicaste: cohortrepeat ratecumcounttimedeltamergeright censoring
- Resuelto
- Sin pistas
- Al primer intento
