Módulo 6: Freshness Volume And Lineage
Checks de volumen: muy pocas o demasiadas filas
Descripción
Esta lección construye la segunda pieza de nivel de archivo de este módulo: check_volume(), una función que compara el número de filas de un DataFrame contra un rango esperado, y falla tanto si hay muy pocas como si hay demasiadas. A diferencia de la lección 4, el resultado sobre S04 esta vez va a ser PASS — un contraste deliberado, para que quede claro que el aparato de calidad de esta guía no está diseñado para encontrar problemas donde no los hay.
Conexión con el módulo. Freshness (lección 4) preguntó "¿cuándo?". Esta lección pregunta algo distinto, pero de la misma naturaleza estructural —una propiedad del archivo completo, nunca de una fila—: "¿cuántas?". Las dos funciones comparten forma (reciben un DataFrame, devuelven un dict con un veredicto), pero verifican dimensiones completamente independientes del mismo incidente.
Una analogía: el manifiesto de un camión de reparto
Un camión que reparte mercadería a las tiendas de Kiosko lleva, cada mañana, un manifiesto: cuántas cajas carga, destinadas a cuál tienda. Si el manifiesto de hoy dice "2 cajas para S04", y normalmente ese camión lleva entre 8 y 15, algo salió mal antes de que el camión saliera del depósito —un error de carga, un sistema que se detuvo a la mitad, un pedido que nunca se completó—. Si el manifiesto dice "40 cajas para S04", eso es igual de sospechoso, aunque en la dirección opuesta —una retransmisión completa del pedido de ayer, sumada por error al de hoy, o dos camiones distintos cargando el mismo pedido sin que nadie lo coordinara—.
Ningún guardia que revise caja por caja —¿está bien sellada? ¿tiene la etiqueta correcta?— puede detectar ninguno de los dos problemas, porque ambos son fallas del conteo total, no de ninguna caja individual. check_volume() es, con precisión, ese chequeo de manifiesto: no le importa qué contiene cada fila —eso ya lo revisan las otras cinco herramientas de esta guía—, solo le importa si el número total de filas cae dentro de un rango que Kiosko ya declaró razonable.
Ejemplo trabajado: construir, probar en limpio, correr sobre S04
Paso 1 — la función
# checks.py -- continuacion del archivo de la leccion 4
def check_volume(df: pl.DataFrame, min_rows: int, max_rows: int) -> dict:
"""Confirma que df.height cae dentro de [min_rows, max_rows]."""
row_count = df.height
return {
"check": "volume",
"row_count": row_count,
"min_rows": min_rows,
"max_rows": max_rows,
"status": "PASS" if min_rows <= row_count <= max_rows else "FAIL",
}
La función más corta de toda esta guía hasta ahora, y a propósito: df.height —el número de filas de cualquier DataFrame de Polars, ya usado sin comentario desde el módulo 2— es todo lo que necesita para responder la pregunta. La condición min_rows <= row_count <= max_rows es una comparación encadenada de Python, equivalente a min_rows <= row_count and row_count <= max_rows, con los dos límites inclusivos: un archivo con exactamente min_rows filas, o exactamente max_rows, pasa sin problema — la misma convención de límites inclusivos que ya usó check_freshness() en la lección 4 (<=, no <).
Paso 2 — probarla con datos de juguete, tres escenarios
# checks.py -- continuacion
if __name__ == "__main__":
toy_ok = pl.DataFrame({"order_id": [f"T{i}" for i in range(10)]})
toy_too_few = pl.DataFrame({"order_id": [f"T{i}" for i in range(2)]})
toy_too_many = pl.DataFrame({"order_id": [f"T{i}" for i in range(30)]})
print("=== check_volume sobre datos de juguete ===")
print(f"10 filas (dentro del rango): {check_volume(toy_ok, min_rows=5, max_rows=20)}")
print(f"2 filas (muy pocas): {check_volume(toy_too_few, min_rows=5, max_rows=20)}")
print(f"30 filas (demasiadas): {check_volume(toy_too_many, min_rows=5, max_rows=20)}")
Qué esperar.
=== check_volume sobre datos de juguete ===
10 filas (dentro del rango): {'check': 'volume', 'row_count': 10, 'min_rows': 5, 'max_rows': 20, 'status': 'PASS'}
2 filas (muy pocas): {'check': 'volume', 'row_count': 2, 'min_rows': 5, 'max_rows': 20, 'status': 'FAIL'}
30 filas (demasiadas): {'check': 'volume', 'row_count': 30, 'min_rows': 5, 'max_rows': 20, 'status': 'FAIL'}
Tres escenarios, tres veredictos correctos: 10 cae cómodamente dentro de [5, 20]; 2 es muy pocas; 30 es demasiadas. check_volume() ya demostró que atrapa ambos extremos, exactamente como promete su nombre — un chequeo bidireccional, a diferencia de check_freshness(), que solo tiene un límite (nunca hay un problema por revisar un archivo "demasiado rápido").
Paso 3 — corrida sobre orders_2026-08-14.csv, de verdad
# checks.py -- continuacion
con = duckdb.connect("kiosko.duckdb")
df = con.sql("SELECT * FROM orders_s04").pl()
volume_result = check_volume(df, min_rows=5, max_rows=20)
print("\n=== check_volume(df, min_rows=5, max_rows=20) ===")
for k, v in volume_result.items():
print(f" {k}: {v}")
Qué esperar.
=== check_volume(df, min_rows=5, max_rows=20) ===
check: volume
row_count: 12
min_rows: 5
max_rows: 20
status: PASS
row_count: 12, dentro de [5, 20], status: PASS. Este es el contraste deliberado que anticipó la lección 1 de este módulo: no todo lo que le pasa a S04 es un fallo. El archivo tiene un tamaño razonable para el primer día de ventas de una tienda nueva — ni sospechosamente truncado, ni sospechosamente inflado —, y check_volume() lo confirma sin ninguna advertencia. Un sistema de calidad de datos que marcara todo como sospechoso, sin distinción, sería tan poco confiable como uno que nunca marca nada — la lección 5 del módulo 5 ya hizo un argumento parecido sobre umbrales mal calibrados.
Los dos extremos, construidos sobre el archivo real de S04
Vale la pena confirmar, con el propio archivo de S04 como base —no datos de juguete inventados—, qué habría pasado si el archivo real hubiera llegado truncado o duplicado:
# volume_edge_cases.py
import duckdb
import polars as pl
from checks import check_volume
con = duckdb.connect("kiosko.duckdb")
df = con.sql("SELECT * FROM orders_s04").pl()
truncated = df.head(3)
print(f"Si el archivo se hubiera cortado a 3 filas: {check_volume(truncated, min_rows=5, max_rows=20)}")
doubled = pl.concat([df, df])
print(f"Si el archivo se hubiera retransmitido completo (24 filas): {check_volume(doubled, min_rows=5, max_rows=20)}")
Qué esperar.
Si el archivo se hubiera cortado a 3 filas: {'check': 'volume', 'row_count': 3, 'min_rows': 5, 'max_rows': 20, 'status': 'FAIL'}
Si el archivo se hubiera retransmitido completo (24 filas): {'check': 'volume', 'row_count': 24, 'min_rows': 5, 'max_rows': 20, 'status': 'FAIL'}
df.head(3) simula el escenario del camión con "2 cajas" de la analogía: un archivo que se cortó a la mitad, quizás porque el proceso que lo generó falló antes de terminar. pl.concat([df, df]) simula el escenario opuesto: el archivo completo, retransmitido por error, ahora con 24 filas en vez de 12 — nota que esto es distinto del duplicado de ORD-9502 que ya atrapó el módulo 2: aquel era una fila repetida dentro de un archivo por lo demás normal; este es el archivo entero duplicado, un problema de volumen, no de uniqueness. Ninguno de los dos escenarios ocurrió de verdad con S04 —el archivo real tiene doce filas, ni truncado ni duplicado—, pero construirlos con evidencia, sobre los mismos datos reales, confirma que check_volume() los atraparía si ocurrieran.
Diagrama: dos preguntas de nivel de archivo, ninguna sobre el contenido de una fila
flowchart TD
A["orders_2026-08-14.csv (12 filas)"] --> B["check_freshness()\nCUANDO llego el dato mas reciente"]
A --> C["check_volume()\nCUANTAS filas trae el archivo"]
B --> D["FAIL: 47.58h > SLA de 24h"]
C --> E["PASS: 12 filas dentro de [5, 20]"]
D --> F["Un archivo puede fallar\nfreshness Y pasar volumen\nal mismo tiempo -- son\npreguntas independientes"]
E --> F
Profundización: de dónde salen min_rows=5 y max_rows=20
Estos dos números no se inventaron para esta lección. Ya aparecieron, exactos, en el módulo 4: orders_contract.yaml declaró sla.row_count.min: 5 y sla.row_count.max: 20 — el mismo rango que esta lección usa para check_volume(). Esto no es una coincidencia — es la promesa central del módulo 4 cumplida: un contrato de datos declara reglas de negocio en un solo lugar versionado, y cualquier función ejecutable que las necesite (como check_volume(), aquí) las lee de ahí, en vez de inventarlas de nuevo o copiarlas de memoria. El razonamiento de negocio detrás de esos números —menos de 5 sugiere un archivo truncado, más de 20 sugiere una retransmisión completa, para el primer día de ventas de una tienda nueva— ya lo explicó, con precisión, la lección 3 del módulo 4 al escribir el contrato. Esta lección no repite ese razonamiento: lo ejecuta.
Errores comunes
Pensar que check_volume() y check_freshness() miden lo mismo, porque ambas son "de archivo completo". Qué pasa: alguien, después de ver que las dos funciones comparten forma (reciben df, devuelven un dict con status), asume que un archivo que falla una necesariamente falla la otra, o que basta con correr una de las dos. Por qué pasa: la similitud estructural (ambas de nivel de archivo, ambas con un veredicto binario) invita a pensar que también son similares en contenido. Cómo detectarlo: revisa el resultado real de S04 en esta guía — check_freshness() dio FAIL, check_volume() dio PASS, sobre el mismo archivo, al mismo tiempo. Cómo corregirlo: trata cada check de nivel de archivo como completamente independiente de los demás — el diagrama de esta lección lo muestra con precisión: un archivo puede fallar freshness y pasar volumen simultáneamente, porque miden preguntas de negocio distintas (cuándo contra cuántas), sin ninguna relación lógica entre sí.
Usar len(df) en vez de df.height y asumir que siempre son intercambiables. Qué pasa: alguien escribe len(df) en vez de df.height, familiarizado con el patrón de Python estándar (len() sobre listas, diccionarios, cadenas de texto). Por qué pasa: Polars sí soporta len(df) como alias válido de df.height — así que este "error" en realidad no rompe nada en la práctica, pero vale la pena saber por qué esta lección prefiere df.height explícitamente. Cómo detectarlo: si tu código mezcla len(df) en algunos lugares y df.height en otros dentro del mismo proyecto, sin ningún criterio, tu estilo es inconsistente aunque funcione. Cómo corregirlo: esta guía prefiere df.height de forma consistente porque es explícito sobre qué está midiendo —la dimensión de filas de un DataFrame de dos dimensiones—, mientras que len() es una función genérica de Python cuyo significado depende del tipo de objeto que reciba. No es un error funcional, es una preferencia de claridad que esta guía mantiene en cada lección.
Elegir un max_rows sin ningún margen, pegado al número de filas del día "normal". Qué pasa: alguien, al configurar check_volume() para una tienda nueva, pone max_rows exactamente igual al número de filas del primer archivo real que vio (por ejemplo, max_rows=12 para S04, en vez de 20), sin dejar ningún margen para el crecimiento normal del negocio. Por qué pasa: parece "más preciso" ajustar el límite al dato que ya se conoce. Cómo detectarlo: si tu max_rows es idéntico al row_count del primer archivo que viste, cualquier crecimiento normal —una tienda nueva que gana clientes, un día con más tráfico de lo usual— dispararía un FAIL de volumen sin que haya ningún problema real de datos. Cómo corregirlo: el contrato del módulo 4 dejó un margen deliberado —max_rows=20 contra 12 filas reales, casi el doble— precisamente para absorber variación normal de negocio sin generar alertas falsas constantes. Un límite de volumen útil dista lo suficiente del caso típico para no dispararse con cada fluctuación normal, pero lo suficientemente cerca para seguir atrapando algo genuinamente anómalo.
Ejercicios
Ejercicio 1 — Confirma los dos límites exactos, 5 y 20, como casos frontera. Usando df.head(5) y una concatenación que produzca exactamente 20 filas, confirma que ambos casos dan PASS (los límites son inclusivos).
Ver solución
five = df.head(5)
print(f"Exactamente 5 filas: {check_volume(five, min_rows=5, max_rows=20)}")
twenty = pl.concat([df, df.head(8)])
print(f"Exactamente 20 filas: {check_volume(twenty, min_rows=5, max_rows=20)}")
Salida esperada:
Exactamente 5 filas: {'check': 'volume', 'row_count': 5, 'min_rows': 5, 'max_rows': 20, 'status': 'PASS'}
Exactamente 20 filas: {'check': 'volume', 'row_count': 20, 'min_rows': 5, 'max_rows': 20, 'status': 'PASS'}
Ambos límites son inclusivos, confirmado con evidencia — min_rows <= row_count <= max_rows acepta los dos extremos exactos como válidos, la misma convención que ya usó check_freshness() con <= en la lección 4.
Ejercicio 2 — Confirma qué pasa con un DataFrame completamente vacío. Construye un DataFrame con 0 filas y corre check_volume() sobre él con min_rows=5, max_rows=20. ¿Es un caso especial, o el mismo código ya lo maneja correctamente?
Ver solución
empty_df = pl.DataFrame({"order_id": []}, schema={"order_id": pl.Utf8})
print(check_volume(empty_df, min_rows=5, max_rows=20))
Salida esperada:
{'check': 'volume', 'row_count': 0, 'min_rows': 5, 'max_rows': 20, 'status': 'FAIL'}
FAIL, sin ningún manejo especial necesario — df.height de un DataFrame vacío es simplemente 0, y 5 <= 0 <= 20 es False de forma natural, exactamente como cualquier otro caso por debajo del mínimo. Esto confirma que check_volume() no necesita ningún caso especial (if df.height == 0: ...) para manejar un archivo completamente vacío — un archivo con cero filas ya cae, sin ningún trabajo adicional, dentro de la misma lógica que atrapa cualquier archivo "demasiado pequeño".
Ejercicio 3 — Argumenta por qué el duplicado de ORD-9502 (módulo 2) y el archivo duplicado completo de esta lección son fallas relacionadas, pero atrapadas por herramientas distintas. En 2-3 frases, explica la diferencia de granularidad entre ambos problemas, y por qué ninguna de las dos herramientas —OrdersSchema (uniqueness) o check_volume()— podría reemplazar completamente a la otra.
Ver solución
Los dos problemas comparten una causa raíz plausible —una retransmisión, algo que se reenvía por error—, pero ocurren en escalas distintas: ORD-9502 es una sola fila que se repite dentro de un archivo por lo demás normal (11 order_id distintos entre 12 filas), mientras que el escenario de esta lección duplica el archivo completo, las doce filas repetidas exactamente. OrdersSchema (con unique=True en order_id) atraparía el primer caso pero no el segundo de forma directa — un archivo con 24 filas donde cada order_id se repite exactamente una vez seguiría teniendo pares duplicados detectables por uniqueness, pero el síntoma más inmediato y fácil de detectar en ese escenario es simplemente que el archivo tiene el doble de filas de lo esperado, la pregunta que responde check_volume() de forma mucho más directa y rápida que revisar duplicados uno por uno.
Resumen y siguiente paso
En esta lección construiste check_volume(), la segunda función de nivel de archivo de este módulo. La probaste en datos de juguete con tres escenarios (dentro del rango, muy pocas, demasiadas filas), confirmaste con evidencia que los dos límites son inclusivos, y la corriste sobre S04 real: row_count: 12, status: PASS — el contraste deliberado con el FAIL de freshness de la lección anterior. También construiste, sobre el archivo real de S04 (no datos inventados), los dos escenarios de fallo que check_volume() sí atraparía: un archivo truncado a 3 filas, y uno duplicado completo a 24.
Antes de avanzar deberías poder: explicar por qué check_freshness() y check_volume() son completamente independientes entre sí, con el resultado real de S04 como evidencia; y explicar de dónde salen los números 5 y 20, sin tener que inventarlos de nuevo.
Con freshness y volumen resueltos, el diagnóstico de nivel de archivo de esta guía está completo. Las lecciones 6 y 7 cambian de tema por completo: de "¿está bien?" a "¿de dónde viene?" — la pregunta del linaje, que ninguna de las siete herramientas construidas hasta ahora puede responder.
Recursos
- Polars — documentación oficial,
DataFrame.height(la propiedad usada en esta lección para contar filas). docs.pola.rs. En inglés. - Módulo 4, lección 3, de esta misma guía ("Escribiendo
orders_contract.yaml") — fuente exacta desla.row_count.min: 5ysla.row_count.max: 20, los números que reusa esta lección.src/guides/data-reliability-and-governance-guide/workbook/module-04-data-contracts-as-versioned-artifacts/es/03-writing-orders-contract-yaml.md. En español. - Módulo 2, lección 6, de esta misma guía — fuente del duplicado de
ORD-9502contrastado en el Ejercicio 3 de esta lección.src/guides/data-reliability-and-governance-guide/workbook/module-02-declarative-data-quality-tests-with-pandera/es/06-completeness-and-uniqueness-checks.md. En español. - DISEÑO de esta guía — el mandato exacto de
check_volume(df, min_rows=5, max_rows=20).src/guides/data-reliability-and-governance-guide/DISENO.md. En español.