La solicitud

De Jordan Reyes, Director de Marketing
Tu objetivo
Paso 1 de 3. Asigna a result un DataFrame con las columnas source, sessions, conversions y conversion_rate_pct (conversions * 100 / sessions, redondeado a 2), ordenado por conversion_rate_pct de forma descendente y con índice por defecto.
- Usa el DataFrame
web_sessionspendiente - Muestra exactamente
sourcesessionsconversionsconversion_rate_pctpendiente - 5 filas, ordenadas como se pidependiente
Paso 2 de 3. Asigna a result un DataFrame con una fila por fuente de sesión y las columnas source, conversions, spend (gasto total de 2025 de ese canal, redondeado a 2; NaN cuando la fuente no tiene gasto) y cost_per_conversion (spend / conversions, redondeado a 2; NaN cuando no hay gasto), ordenado por source de forma ascendente y con índice por defecto.
- Usa el DataFrame
web_sessionspendiente - Usa el DataFrame
marketing_spendpendiente - Muestra exactamente
sourceconversionsspendcost_per_conversionpendiente
Paso 3 de 3. Solo para el canal de pago, asigna a result un DataFrame con las columnas month (texto 'YYYY-MM'), spend (redondeado a 2), sessions, conversions y cost_per_conversion (spend / conversions, redondeado a 2; NaN en un mes sin conversiones), una fila por cada mes de 2025, ordenado por month y con índice por defecto.
- Usa el DataFrame
web_sessionspendiente - Usa el DataFrame
marketing_spendpendiente - Muestra exactamente
monthspendsessionsconversionscost_per_conversionpendiente
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
| session_id | customer_id | started_at | device | source |
|---|---|---|---|---|
| 1 | NaN | 2023-01-01 23:02:24 | mobile | paid |
| 2 | 18 | 2023-01-01 15:07:49 | desktop | paid |
| 3 | NaN | 2023-01-01 16:55:46 | desktop | direct |
| 4 | NaN | 2023-01-01 12:43:28 | mobile | organic |
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
| session_id | customer_id | started_at | device | source |
|---|---|---|---|---|
| 1 | NaN | 2023-01-01 23:02:24 | mobile | paid |
| 2 | 18 | 2023-01-01 15:07:49 | desktop | paid |
| 3 | NaN | 2023-01-01 16:55:46 | desktop | direct |
| 4 | NaN | 2023-01-01 12:43:28 | mobile | organic |
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
| session_id | customer_id | started_at | device | source |
|---|---|---|---|---|
| 1 | NaN | 2023-01-01 23:02:24 | mobile | paid |
| 2 | 18 | 2023-01-01 15:07:49 | desktop | paid |
| 3 | NaN | 2023-01-01 16:55:46 | desktop | direct |
| 4 | NaN | 2023-01-01 12:43:28 | mobile | organic |
Mote revisa tu resultado
Presiona Ejecutar. Cada fila que cumple el objetivo se vuelve una luciérnaga en tu linterna.
Lo que recibe Jordan, y por qué
DataFrames
Cargados como DataFrames de pandas, junto con pd y np.
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 |
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 |
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 |
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 |
La lección
La misma pregunta, las mismas definiciones
Este es el análisis de presupuesto de Jordan en pandas, con las mismas definiciones que el nivel de
SQL de este caso: solo 2025 en ambas tablas, una conversión es una sesión con converted == True,
y el gasto se asocia a las sesiones por el nombre del canal. Organic y direct tienen sesiones pero
no tienen gasto.
Paso 1: contar valores True
converted es una columna booleana. En pandas True se comporta como 1 y False como 0, así que
("converted", "sum") cuenta las conversiones y ("converted", "mean") daría la tasa directamente.
El ejercicio pide un porcentaje redondeado a 2, así que calcúlalo a partir de los dos conteos:
by_source = ws.groupby("source", as_index=False).agg(
sessions=("session_id", "count"), conversions=("converted", "sum")
)
by_source["conversion_rate_pct"] = (by_source["conversions"] * 100 / by_source["sessions"]).round(2)
"count" cuenta los valores no faltantes de la columna indicada. session_id nunca falta, así que
cuenta todas las sesiones. Contar customer_id sería un error silencioso: las sesiones anónimas
tienen NaN ahí y no se contarían.
Paso 2: preagrega y luego une
En SQL esto lleva dos CTE. En pandas cada una es simplemente un DataFrame:
conv = ws.groupby("source", as_index=False).agg(conversions=("converted", "sum"))
spend = ms.groupby("channel", as_index=False).agg(spend=("amount", "sum")).rename(columns={"channel": "source"})
table = conv.merge(spend, on="source", how="left", validate="one_to_one")
marketing_spend llama channel a su clave mientras que web_sessions la llama source.
.rename(columns={"channel": "source"}) renombra una columna (el nombre viejo a la izquierda, el
nuevo a la derecha) para que ambos DataFrames compartan la clave y on="source" funcione.
left_on="source", right_on="channel" hace lo mismo sin renombrar.
validate="one_to_one" es la forma en pandas de afirmar lo que razonaste en SQL: después del
groupby, cada lado tiene una fila por clave, así que el merge no puede duplicar filas. Si más
adelante alguien une las tablas en bruto por error, pandas lanza un MergeError en lugar de
devolver un gasto inflado.
how="left" desde el lado de las sesiones conserva organic y direct, con gasto NaN (la marca
de pandas para un número faltante). Cualquier operación aritmética con NaN da NaN, así que su
costo por conversión falta, y eso es correcto: no es cero, no aplica.
División entre cero
pandas devuelve inf (infinito) sin avisar cuando divides un número entre cero. La solución es
convertir primero los ceros del denominador en valores faltantes. .replace(0, np.nan) cambia cada 0
de una Serie por np.nan, el número faltante de numpy, y cualquier cosa dividida entre NaN es
NaN:
pd.Series([5000, 5000]) / pd.Series([25, 0]) # 200.0, inf
pd.Series([5000, 5000]) / pd.Series([25, 0]).replace(0, np.nan) # 200.0, NaN
Ningún canal ni mes de 2025 tiene cero conversiones en estos datos, pero un informe mensual que se
ejecuta cada mes tarde o temprano se topará con uno, y un inf en un gráfico es difícil de explicar.
Paso 3: añade el mes a ambas claves
Construye una etiqueta month en cada tabla con .dt.strftime("%Y-%m"), que convierte un datetime
en texto como "2025-03" (%Y es el año y %m el mes de dos dígitos). Agrupa cada tabla por mes y
haz merge por month. El grano ahora es una fila por mes en ambos lados.
SQL y pandas lado a lado
| Paso | SQL | pandas |
|---|---|---|
| conversiones | COUNT(*) FILTER (WHERE converted) |
("converted", "sum") |
| CTE | WITH conv AS (...) |
conv = ws.groupby(...) |
| los nombres de la clave difieren | ON s.channel = c.source |
rename o left_on/right_on |
| afirmar que no hay duplicación | razonarlo | validate="one_to_one" |
NULLIF(x, 0) |
incluido | x.replace(0, np.nan) |
Errores comunes
- Unir sesiones en bruto con gasto en bruto. Incluso por
["source", "date"], el gasto de cada día se repite una vez por sesión de ese día. - Redondear el gasto antes de dividir. Calcula el costo por conversión con la suma sin redondear y redondea después.
- Días de gasto faltantes. Faltan doce filas por caídas en
marketing_spend; los totales quedan ligeramente por debajo, igual en SQL y en pandas. - Fechas como texto. Los filtros comparan una columna
datetime64con"2025-01-01"; pandas interpreta el texto, así que no hace falta ninguna conversión.
¡Linterna encendida!
Practicaste: groupbynamed aggregationpre aggregateleft mergeconversion ratecost per conversion
- Resuelto
- Sin pistas
- Al primer intento
