Saltar al contenido

Indice / Practicas / EJ-7.5

EJ-7.5 · El mismo resultado por SQL y por pandas

Practica de: 7.5 El puente con SQLDificultad: M

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

Los dos caminos

La misma operación, dos veces
SQLpandas
INNER JOIN ... ONmerge(..., how="inner")
GROUP BY p.cliente_idgroupby("cliente_id")
COUNT(*)("id", "size")
SUM(p.total)("total", "sum")
MIN / MAX"min" / "max"
ORDER BY p.cliente_idsort_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

tienda_pedidos.csv (entrada) · 120 filas, se muestran 5
idcliente_idtotalcreadoestado
10132374.942026-07-08 22:39:00preparando
10237223.652026-04-18 02:08:00entregado
10326605.952026-02-22 13:19:00nuevo
104251743.362026-04-04 12:18:00pagado
1058699.822026-05-20 01:42:00pagado

El contrato de salida

pedidos_por_cliente_sql.csv · 38 filas

ColumnaTipoQue lleva
cliente_identeroLa clave del cliente
nombretextoDel padron de clientes
pedidosenteroCuantos pedidos hizo
totaldecimalSuma de total, redondeada a 2 decimales
ticket_promediodecimalLa suma SIN redondear dividida por pedidos, y recien ahi redondeada a 2
primer_pedidotextoFecha y hora del pedido mas viejo, AAAA-MM-DD HH:MM:SS
ultimo_pedidotextoFecha y hora del mas reciente

Orden de las filas: cliente_id ascendente. Es unico. Indice: no se escribe: to_csv(index=False)

Que hacer con cada defecto

DefectoDondeAccionAdonde va
1 pedido cuyo cliente_id no esta en el padron1 fila de 120queda queda afuera. El cruce es INNER en los dos caminos, y el contrato lo declara: este informe habla de clientes conocidosfuera de pedidos_por_cliente_sql.csv
2 pedidos sin total2 filascuentan cuentan en pedidos y no suman al total. SUM en SQL y sum en pandas ignoran el ausente igualpedidos_por_cliente_sql.csv
SQLite y Python redondean distinto los valores que caen en la mitad exacta5 clientes: 1, 10, 20, 27 y 29no no redondear en SQL. La consulta devuelve SUM(total) crudo y el redondeo pasa una sola vez, del lado de pandaspedidos_por_cliente_sql.csv
un cliente se llama Ana O'Connor1 fila del padronno 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
Orden de comprobacion

Una fila puede tener mas de un problema. Se informa solo el primero de esta lista:

  1. cargar las dos tablas en sqlite con to_sql
  2. resolverlo con una consulta: JOIN, GROUP BY, COUNT, SUM, MIN y MAX
  3. resolverlo con pandas: merge, groupby y agg
  4. aplicar el MISMO redondeo a los dos resultados
  5. 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

Si estos numeros no te dan, algo se desvio antes de que llegues a comparar los archivos.

pedidos de entrada120
clientes en el padron40
pedidos_por_cliente_sql.csv38
pedidos que entran al resumen119
clientes del padron sin ningun pedido2
clientes donde SQLite y Python redondean distinto5
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

pedidos_por_cliente_sql.csv (objetivo) · 38 filas, se muestran 5
cliente_idnombrepedidostotalticket_promedioprimer_pedidoultimo_pedido
1Ximena Herrera22806.171403.082026-06-13 05:55:002026-06-17 17:16:00
2Micaela Martínez53193.5638.72026-04-25 09:34:002026-07-01 12:46:00
3Ana O'Connor34190.861396.952026-02-11 04:41:002026-06-08 05:10:00
5Ana Muñoz22304.341152.172026-02-15 22:27:002026-05-11 04:01:00
6Sofía Díaz4251847.4362961.862026-05-08 23:34:002026-06-22 07:57:00
38 clientes y no 40

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.

Un cliente se llama 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.