Indice / Practicas / EJ-5.8
EJ-5.8 · Pipeline con asserts sobre los hechos
El ejercicio que cierra la unidad. 500 hechos de venta con ocho
clases de defecto, que hay que resolver como un pipeline: etapas
chicas encadenadas, invariantes escritas como assert y una decisión que
no es descartar.
Qué hay que hacer
A partir de dw_hecho_ventas.csv, producir:
hechos_validos.csv- las ventas utilizables, con el margen calculado y una bandera de atípico.hechos_rechazados.csv- una fila por venta descartada, con la columna, el valor original y el motivo.
Y hay un requisito de forma, no solo de resultado: escribilo como un
pipeline de funciones puras encadenadas con pipe, con los
assert del contrato al final. Podés llegar al mismo archivo de otra
manera, pero el ejercicio es este.
ELI5Explicado facil
Es una línea de control de calidad en una fábrica. Cada estación revisa una cosa y le pega una etiqueta a lo que está mal - nadie tira nada al piso en el medio.
Al final de la cinta hay dos cajas: lo que llegó sin etiqueta y lo que llegó con una. Como nadie tiró nada por el camino, las dos cajas juntas tienen exactamente lo que entró, y eso se puede verificar contando.
Archivos
- Entrada ·
dw_hecho_ventas.csv - Objetivo ·
hechos_validos.csv - Objetivo ·
hechos_rechazados.csv - Herramienta ·
comparar.py
Las cuatro decisiones que hay que tomar
- Qué se rechaza. Nueve reglas, y una fila puede fallar varias: el contrato fija el orden, así que cada rechazo aparece una sola vez.
- Qué se marca sin rechazar. Los ingresos atípicos se conservan
con
atipico = 1. Son raros, no imposibles. - Cuándo se mide la dispersión. El límite del IQR se calcula después de sacar lo imposible. Medido antes, un ingreso de siete dígitos estira el rango y arrastra decenas de filas normales.
- Qué promete el pipeline. Seis
assert, que son las invariantes del contrato escritas en el código.
Cómo saber si te salió
python comparar.py mi_validos.csv hechos_validos.csv
python comparar.py mi_rechazados.csv hechos_rechazados.csv
Antes de arrancar, mirá los datos
| venta_id | fecha | producto_id | cliente_id | cantidad | ingreso | costo |
|---|---|---|---|---|---|---|
| 1 | 2026-01-13 | 25 | 22 | 18 | 10115.14 | 7375.11 |
| 2 | 2026-05-26 | 28 | 28 | 4 | 2456.66 | 1778.94 |
| 3 | 2026-06-05 | 27 | 29 | 17 | 2041.28 | 1072.75 |
| 4 | 2026-01-13 | 4 | 27 | 21 | 6084.27 | 4549.38 |
| 5 | 2026-07-31 | 19 | 24 | 19 | 9774.03 | 8178.44 |
El contrato de salida
hechos_validos.csv · 490 filas
| Columna | Tipo | Que lleva |
|---|---|---|
venta_id | entero | El id original de la venta |
fecha | texto | AAAA-MM-DD, ya validada contra el calendario |
producto_id | entero | Entre 1 y 30 |
cliente_id | entero | Entre 1 y 40 |
cantidad | entero | Mayor que 0 |
ingreso | decimal | Mayor que 0, redondeado a 2 decimales |
costo | decimal | Redondeado a 2 decimales |
margen | decimal | ingreso menos costo, redondeado a 2 decimales. Nunca negativo |
atipico | entero | 1 si el ingreso supera el limite IQR, 0 si no. NO se descarta: se marca |
hechos_rechazados.csv · 10 filas
| Columna | Tipo | Que lleva |
|---|---|---|
venta_id | entero | El id de la venta descartada |
columna | texto | En que columna estaba el problema |
valor_original | texto | El valor tal cual venia en el archivo |
motivo | texto | Uno del vocabulario cerrado del curso |
Que hacer con cada defecto
| Defecto | Donde | Accion | Adonde va |
|---|---|---|---|
| fecha vacia | 1 fila, venta_id 402 | descartar descartar | hechos_rechazados.csv con motivo ausente |
| fecha que no existe en el calendario (2026-02-31) | 1 fila, venta_id 455 | descartar descartar | hechos_rechazados.csv con motivo fecha_invalida |
| cantidad ausente | 1 fila, venta_id 128 | descartar descartar | hechos_rechazados.csv con motivo ausente |
| cantidad 0 o negativa (0 y -6) | 2 filas, venta_id 44 y 91 | descartar descartar | hechos_rechazados.csv con motivo fuera_de_rango |
| costo ausente: campo vacio y token N/A | 2 filas, venta_id 213 y 266 | descartar descartar | hechos_rechazados.csv con motivo ausente |
| ingreso 0 o negativo (-320.0) | 1 fila, venta_id 177 | descartar descartar | hechos_rechazados.csv con motivo fuera_de_rango |
| producto_id 777, que no existe en el catalogo de 30 productos | 1 fila, venta_id 305 | descartar descartar | hechos_rechazados.csv con motivo huerfano |
| cliente_id 888, que no existe en el padron de 40 clientes | 1 fila, venta_id 351 | descartar descartar | hechos_rechazados.csv con motivo huerfano |
| ingreso menor que el costo (venta a perdida) | la unica fila que cumplia esto ya se rechazo antes por ingreso negativo, asi que este motivo no aparece en el informe | descartar. descartar. La regla va igual: sin ella, el assert de margen no negativo no estaria garantizado el mes que viene | hechos_rechazados.csv con motivo negativo_incoherente |
| ingreso de 9999999.99 y otros 14 por encima del limite IQR | 15 filas | MARCAR MARCAR con atipico = 1 y conservar. Es la unica clase de defecto que no descarta nada | hechos_validos.csv |
Una fila puede tener mas de un problema. Se informa solo el primero de esta lista:
- fecha ausente
- fecha invalida
- cantidad ausente
- cantidad menor o igual que 0
- costo ausente
- ingreso ausente
- ingreso menor o igual que 0
- producto_id fuera de 1 a 30
- cliente_id fuera de 1 a 40
- ingreso menor que costo
Se informa SOLO el primer motivo de la lista, para que cada fila rechazada aparezca una vez y valga validos + rechazados = 500. El orden importa de verdad en un caso: la venta 177 tiene ingreso -320, que es a la vez un ingreso no positivo y una venta por debajo del costo; se informa como fuera_de_rango porque esa regla va antes.
Conteos clave
| filas de entrada | 500 |
|---|---|
| hechos_validos.csv | 490 |
| hechos_rechazados.csv | 10 |
| atipicos marcados (no descartados) | 15 |
| limite IQR del ingreso | 12432.61 |
| por motivo | ausente: 4, fuera_de_rango: 3, huerfano: 2, fecha_invalida: 1, negativo_incoherente: 0 |
Invariantes
Comprobaciones que tu resultado tiene que cumplir. Corrilas vos mismo antes de comparar los archivos:
len(validos) + len(rechazados) == 500
validos['venta_id'].is_unique
(validos['cantidad'] > 0).all()
(validos['ingreso'] > 0).all()
(validos['margen'] >= 0).all()
validos['producto_id'].between(1, 30).all()
validos['cliente_id'].between(1, 40).all()
validos.notna().all().all()
int(validos['atipico'].sum()) == 15
rechazados['venta_id'].is_unique
set(rechazados['motivo']) <= {'ausente', 'fecha_invalida', 'fuera_de_rango', 'huerfano', 'negativo_incoherente'}
PISTASSi te trabas, abri esto
- Escribilo como pipeline: una funcion por etapa, que recibe una tabla y devuelve una tabla. Ninguna saca filas; todas anotan el motivo en una columna. Se parte en dos al final.
- El motivo se anota solo donde no habia otro. Sin esa condicion, una fila con dos defectos aparece dos veces y la conciliacion deja de cerrar.
- El IQR se calcula sobre los ingresos que quedaron. Calculado sobre los 500, el -320 y el de siete digitos deforman los cuartiles y el limite sale mal.
- Los asserts son parte del entregable, no un adorno: si el mes que viene el archivo trae un caso nuevo, son lo unico que te va a avisar antes de que el numero llegue a un informe.
Así tiene que quedar
| venta_id | fecha | producto_id | cliente_id | cantidad | ingreso | costo | margen | atipico |
|---|---|---|---|---|---|---|---|---|
| 1 | 2026-01-13 | 25 | 22 | 18 | 10115.14 | 7375.11 | 2740.03 | 0 |
| 2 | 2026-05-26 | 28 | 28 | 4 | 2456.66 | 1778.94 | 677.72 | 0 |
| 3 | 2026-06-05 | 27 | 29 | 17 | 2041.28 | 1072.75 | 968.53 | 0 |
| 4 | 2026-01-13 | 4 | 27 | 21 | 6084.27 | 4549.38 | 1534.89 | 0 |
| 5 | 2026-07-31 | 19 | 24 | 19 | 9774.03 | 8178.44 | 1595.59 | 0 |
| venta_id | columna | valor_original | motivo |
|---|---|---|---|
| 44 | cantidad | 0 | fuera_de_rango |
| 91 | cantidad | -6 | fuera_de_rango |
| 128 | cantidad | vacio | ausente |
| 177 | ingreso | -320.0 | fuera_de_rango |
| 213 | costo | vacio | ausente |
La regla «ingreso menor que costo» no aparece en el informe: la única fila que la cumplía ya se había rechazado antes por tener el ingreso negativo. Y sin embargo el contrato la pide.
El motivo es que uno de los assert promete que
margen nunca es negativo. Hoy eso se cumple de casualidad;
sin la regla, el archivo del mes que viene puede traer una venta a pérdida legítima y
el assert va a explotar sin que nadie sepa por qué. Una regla que hoy
captura cero filas no es una regla de más: es la que sostiene la promesa.
Las 15 filas marcadas como atípicas se quedan en el archivo, y entre todas explican el 85 % de los ingresos. Ese es exactamente el motivo de marcarlas en vez de descartarlas en silencio: cualquier total calculado sobre este archivo tiene que poder decir si las incluye o no.
Con la bandera, contestar esa pregunta es un groupby. Sin ella, hay
que rehacer todo el trabajo.