Indice / Practicas / EJ-7.5
EJ-7.5 · El mismo resultado por SQL y por pandas
El mismo resumen, resuelto dos veces: una con una
consulta SQL y otra con pandas. El ejercicio no está terminado hasta que
assert_frame_equal dice que los dos dan exactamente lo mismo.
Qué hay que hacer
A partir de tienda_pedidos.csv y tienda_clientes.csv,
cargarlos en una base sqlite3 en memoria y producir
pedidos_por_cliente_sql.csv: una fila por cliente con sus pedidos, su
total, su ticket promedio y las fechas de su primer y último pedido.
Y hacerlo por los dos caminos, comparándolos.
ELI5Explicado facil
Es hacer la misma cuenta con la calculadora y a mano, y comparar. Si dan lo mismo, las dos están bien. Si no, una está mal - y lo interesante es que tenés que averiguar cuál.
Lo que no vale es hacerla una sola vez y confiar.
Archivos
- Entrada ·
tienda_pedidos.csv - Entrada ·
tienda_clientes.csv - Objetivo ·
pedidos_por_cliente_sql.csv - Herramienta ·
comparar.py
Los dos caminos
| SQL | pandas |
|---|---|
INNER JOIN ... ON | merge(..., how="inner") |
GROUP BY p.cliente_id | groupby("cliente_id") |
COUNT(*) | ("id", "size") |
SUM(p.total) | ("total", "sum") |
MIN / MAX | "min" / "max" |
ORDER BY p.cliente_id | sort_values("cliente_id") |
La trampa: los dos redondean distinto
Hay cinco clientes cuyo ticket promedio cae exactamente en la
mitad: 1403.085, 198.515, 1602.015,
842.105, 962.425.
SQLite redondea la mitad hacia afuera y Python hacia el par. Sobre
1403.085, SQLite da 1403.09 y Python 1403.08.
Los dos resultados son correctos según su propia regla, y no hay forma de decidir cuál
es «el bueno» mirando los archivos.
La solución es redondear en un solo lugar. La consulta devuelve la suma cruda y todo el redondeo pasa del lado de pandas, para los dos caminos por igual. Ahí sí coinciden.
Cómo saber si te salió
python comparar.py mi_salida.csv pedidos_por_cliente_sql.csv
Pero la comprobación que importa la hacés vos, adentro del programa:
assert_frame_equal(en_sql, en_pandas).
Antes de arrancar, mirá los datos
| id | cliente_id | total | creado | estado |
|---|---|---|---|---|
| 101 | 32 | 374.94 | 2026-07-08 22:39:00 | preparando |
| 102 | 37 | 223.65 | 2026-04-18 02:08:00 | entregado |
| 103 | 26 | 605.95 | 2026-02-22 13:19:00 | nuevo |
| 104 | 25 | 1743.36 | 2026-04-04 12:18:00 | pagado |
| 105 | 8 | 699.82 | 2026-05-20 01:42:00 | pagado |
El contrato de salida
pedidos_por_cliente_sql.csv · 38 filas
| Columna | Tipo | Que lleva |
|---|---|---|
cliente_id | entero | La clave del cliente |
nombre | texto | Del padron de clientes |
pedidos | entero | Cuantos pedidos hizo |
total | decimal | Suma de total, redondeada a 2 decimales |
ticket_promedio | decimal | La suma SIN redondear dividida por pedidos, y recien ahi redondeada a 2 |
primer_pedido | texto | Fecha y hora del pedido mas viejo, AAAA-MM-DD HH:MM:SS |
ultimo_pedido | texto | Fecha y hora del mas reciente |
Que hacer con cada defecto
| Defecto | Donde | Accion | Adonde va |
|---|---|---|---|
| 1 pedido cuyo cliente_id no esta en el padron | 1 fila de 120 | queda queda afuera. El cruce es INNER en los dos caminos, y el contrato lo declara: este informe habla de clientes conocidos | fuera de pedidos_por_cliente_sql.csv |
| 2 pedidos sin total | 2 filas | cuentan cuentan en pedidos y no suman al total. SUM en SQL y sum en pandas ignoran el ausente igual | pedidos_por_cliente_sql.csv |
| SQLite y Python redondean distinto los valores que caen en la mitad exacta | 5 clientes: 1, 10, 20, 27 y 29 | no no redondear en SQL. La consulta devuelve SUM(total) crudo y el redondeo pasa una sola vez, del lado de pandas | pedidos_por_cliente_sql.csv |
| un cliente se llama Ana O'Connor | 1 fila del padron | no no afecta a esta consulta, que no filtra por nombre. Si filtraras por el, la unica forma correcta es params=(nombre,) | pedidos_por_cliente_sql.csv |
Una fila puede tener mas de un problema. Se informa solo el primero de esta lista:
- cargar las dos tablas en sqlite con to_sql
- resolverlo con una consulta: JOIN, GROUP BY, COUNT, SUM, MIN y MAX
- resolverlo con pandas: merge, groupby y agg
- aplicar el MISMO redondeo a los dos resultados
- assert_frame_equal entre los dos
El redondeo se aplica al final y en un solo lugar, a los dos caminos por igual. Si cada motor redondea por su cuenta, los dos resultados son correctos y distintos, y no hay forma de decidir cual es el bueno.
Conteos clave
| pedidos de entrada | 120 |
|---|---|
| clientes en el padron | 40 |
| pedidos_por_cliente_sql.csv | 38 |
| pedidos que entran al resumen | 119 |
| clientes del padron sin ningun pedido | 2 |
| clientes donde SQLite y Python redondean distinto | 5 |
| por motivo |
Invariantes
Comprobaciones que tu resultado tiene que cumplir. Corrilas vos mismo antes de comparar los archivos:
len(resumen) == 38
resumen['cliente_id'].is_unique
int(resumen['pedidos'].sum()) == 119
resumen['cliente_id'].is_monotonic_increasing
(resumen['primer_pedido'] <= resumen['ultimo_pedido']).all()
resumen.notna().all().all()
(abs(resumen['ticket_promedio'] * resumen['pedidos'] - resumen['total']) < 0.05).all()
PISTASSi te trabas, abri esto
- El objetivo del ejercicio no es el archivo: es que los dos caminos coincidan. Si el assert falla, uno de los dos esta mal, y el error dice cual columna.
- No redondees en SQL. SQLite redondea la mitad hacia afuera y Python hacia el par: 1403.085 da 1403.09 en SQLite y 1403.08 en Python.
- SUM en SQL y sum en pandas ignoran los ausentes igual. COUNT(*) cuenta filas, como size().
- 38 clientes y no 40: dos del padron nunca hicieron un pedido, y el INNER JOIN no los trae.
Así tiene que quedar
| cliente_id | nombre | pedidos | total | ticket_promedio | primer_pedido | ultimo_pedido |
|---|---|---|---|---|---|---|
| 1 | Ximena Herrera | 2 | 2806.17 | 1403.08 | 2026-06-13 05:55:00 | 2026-06-17 17:16:00 |
| 2 | Micaela Martínez | 5 | 3193.5 | 638.7 | 2026-04-25 09:34:00 | 2026-07-01 12:46:00 |
| 3 | Ana O'Connor | 3 | 4190.86 | 1396.95 | 2026-02-11 04:41:00 | 2026-06-08 05:10:00 |
| 5 | Ana Muñoz | 2 | 2304.34 | 1152.17 | 2026-02-15 22:27:00 | 2026-05-11 04:01:00 |
| 6 | Sofía Díaz | 4 | 251847.43 | 62961.86 | 2026-05-08 23:34:00 | 2026-06-22 07:57:00 |
Dos clientes del padrón nunca hicieron un pedido, así que el INNER JOIN
no los trae. Y hay un pedido cuyo cliente_id no está en el padrón, que
tampoco entra: 119 de 120 pedidos.
Las dos cosas están declaradas en el contrato. La diferencia con
EJ-6.9 - donde el pedido del cliente desconocido sí se
conservaba - es que ahí el informe era del negocio y acá es de los clientes
conocidos. La misma decisión, dos respuestas, según qué se esté informando.
Ana O'Connor
Esta consulta no filtra por nombre, así que no la toca. Pero si alguna vez escribís
f"... WHERE nombre = '{nombre}'" con ese valor, el apóstrofe cierra la
cadena de SQL y lo que sigue se interpreta como código.
La única forma correcta es params=(nombre,). Siempre, incluso cuando el
valor lo escribiste vos: hoy es tuyo, mañana viene de un archivo.