Módulo 3: Consistency And Referential Checks
Atrapando el producto huérfano de S04
Descripción
Esta es la lección central de todo el módulo. ORD-9508, con product_id="P099", pasó validate_orders() en el módulo 1 sin ninguna marca. Pasó OrdersSchema.validate() en el módulo 2, también sin ninguna marca. Dos herramientas distintas, dos módulos distintos, el mismo resultado silencioso. Esta lección corre validate_referential_integrity() —la función que construyó la lección 4, sin ningún cambio— sobre el archivo real y completo de S04, y cierra el diagnóstico que abrió el módulo 1: la primera herramienta de esta guía que atrapa esta fila con evidencia.
Conexión con el módulo. Las lecciones 2 a 4 construyeron el criterio y la herramienta. La lección 5 agregó una segunda categoría de reglas. Esta lección es la aplicación completa, sobre datos reales, del trabajo de todo el módulo hasta ahora. La lección 7 combina este resultado con el de Pandera en un solo reporte.
El material: las mismas doce líneas, la misma dim_product
orders_s04 sigue siendo exactamente el mismo archivo que abriste en el módulo 1 y validaste con Pandera en el módulo 2 — doce líneas, seis limpias, seis rotas, sin ningún cambio. dim_product, cargada en la lección 4 de este módulo, tiene los cuatro productos reales de Kiosko: P001, P002, P003, P004. Ningún dato nuevo entra en esta lección — el punto es, precisamente, que la misma evidencia que ya tenías desde el módulo 1 alcanza para atrapar el problema, una vez que tienes la herramienta correcta.
Ejemplo trabajado: validate_referential_integrity(), corrida sobre el archivo completo
# catch_orphan_product.py
import duckdb
import polars as pl
con = duckdb.connect("kiosko.duckdb")
orders_df = con.sql("SELECT * FROM orders_s04").pl()
dim_product_df = con.sql("SELECT * FROM dim_product").pl()
def validate_referential_integrity(orders_df: pl.DataFrame, dim_product_df: pl.DataFrame) -> pl.DataFrame:
"""Filas de orders_df cuyo product_id NO existe en dim_product_df (anti-join)."""
return orders_df.join(dim_product_df, on="product_id", how="anti")
orphans = validate_referential_integrity(orders_df, dim_product_df)
print(f"orders_df: {orders_df.height} filas | dim_product_df: {dim_product_df.height} filas")
print(f"orphans: {orphans.height} filas de {orders_df.height}\n")
print(orphans)
print("\n=== Reporte legible ===")
for row in orphans.iter_rows(named=True):
print(f"{row['order_id']} | store_id={row['store_id']} | product_id={row['product_id']} "
f"(no existe en dim_product) | quantity={row['quantity']} | unit_price={row['unit_price']}")
Qué esperar. Al correr python3 catch_orphan_product.py (con kiosko.duckdb conteniendo ambas tablas, orders_s04 del módulo 2 y dim_product de la lección 4), la salida es exactamente esta:
orders_df: 12 filas | dim_product_df: 4 filas
orphans: 1 filas de 12
shape: (1, 6)
┌──────────┬──────────┬────────────┬──────────┬────────────┬─────────────────────┐
│ order_id ┆ store_id ┆ product_id ┆ quantity ┆ unit_price ┆ order_ts │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ str ┆ str ┆ i64 ┆ f64 ┆ datetime[μs] │
╞══════════╪══════════╪════════════╪══════════╪════════════╪═════════════════════╡
│ ORD-9508 ┆ S04 ┆ P099 ┆ 2 ┆ 1.0 ┆ 2026-08-14 08:56:00 │
└──────────┴──────────┴────────────┴──────────┴────────────┴─────────────────────┘
=== Reporte legible ===
ORD-9508 | store_id=S04 | product_id=P099 (no existe en dim_product) | quantity=2 | unit_price=1.0
Ahí está, con evidencia ejecutada: exactamente una fila, ORD-9508, exactamente la fila que el módulo 1 marcó como "silenciosa" y que el módulo 2 confirmó, otra vez, que seguía pasando limpia. validate_referential_integrity() no necesitó ningún ajuste, ninguna regla adicional, ninguna excepción especial para este caso — la misma función de tres líneas que ya probaste con datos de juguete en la lección 4 atrapa, sin ningún cambio, el caso real. Las otras once filas —incluidas las tres que ya atrapó OrdersSchema (ORD-9502 x2, ORD-9503, ORD-9507) y la fila de accuracy que sigue sin resolverse (ORD-9509)— desaparecen del resultado del anti-join, porque todas tienen un product_id que sí existe en dim_product. El anti-join no le importa si una fila tiene otros problemas — solo pregunta por la integridad referencial, y responde exclusivamente sobre eso.
Tabla: la misma fila, tres herramientas, tres módulos
| Herramienta | Módulo | ¿Atrapa ORD-9508? |
|---|---|---|
validate_orders() (imperativo, foundations) | Módulo 1 | No — nunca preguntó por dim_product |
OrdersSchema.validate() (declarativo, Pandera) | Módulo 2 | No — un DataFrameModel nunca recibe una segunda tabla |
validate_referential_integrity() (anti-join, Polars) | Módulo 3 (esta lección) | Sí — 1 de 1 filas huérfanas, exacta |
Tres herramientas completamente distintas, tres formas de trabajar —código imperativo, esquema declarativo, comparación entre tablas—, y solo la tercera puede, por diseño, hacer la pregunta correcta. Esto no es una crítica a las dos primeras: cada una cumplió exactamente lo que prometía. Es la confirmación final del argumento que abrió este módulo en la lección 1: la herramienta correcta depende de la pregunta, no de qué tan sofisticada sea la herramienta en general.
Diagrama: la línea de tiempo completa de ORD-9508
flowchart LR
A["Modulo 1, leccion 6:\nvalidate_orders() real\nORD-9508: valid (sin marca)"] --> B["Modulo 1, leccion 7:\nse nombra el problema,\nsin herramienta para atraparlo"]
B --> C["Modulo 2, leccion 7-8:\nOrdersSchema.validate()\nORD-9508: sigue sin marca"]
C --> D["Modulo 3, leccion 6 (esta):\nvalidate_referential_integrity()\nORD-9508: ATRAPADA"]
Cuatro puntos en el tiempo, la misma fila. El diagrama no muestra ningún cambio en los datos —ORD-9508 nunca se modificó—, muestra el avance de las herramientas de esta guía, cada módulo construyendo sobre el anterior, hasta que la pregunta correcta finalmente se hizo.
Profundización: qué NO cambió después de esta lección
Vale la pena, antes de celebrar, ser preciso sobre el alcance de este resultado. orphans tiene una sola fila. Eso significa que esta lección resolvió una de las seis dimensiones de calidad de datos del módulo 1 —consistency—, ni una más. ORD-9509, con unit_price=60.00, sigue exactamente donde la dejó el módulo 2: product_id="P002" existe perfectamente en dim_product, así que validate_referential_integrity() nunca la marca —no hay ningún problema de integridad referencial en esa fila, el problema es de otro tipo completamente distinto—. Puedes confirmarlo tú mismo con una línea:
# confirm_9509_still_clean.py -- continuacion de catch_orphan_product.py
print(f"\n'ORD-9509' esta en orphans: {'ORD-9509' in orphans['order_id'].to_list()}")
Qué esperar.
'ORD-9509' esta en orphans: False
Confirmado: ORD-9509 no aparece en orphans, y no debería —el precio de 60.00 es un problema de accuracy, no de consistency, y esa dimensión tiene su propia herramienta en el módulo 5 de esta guía, con su propia lógica (una línea base de precios históricos, no un catálogo de existencia). Este módulo cierra exactamente una brecha, con precisión, sin pretender cerrar las que no le corresponden.
Errores comunes
Esperar que orphans tenga más de una fila, "para que valga la pena el módulo". Qué pasa: alguien, al ver que el resultado tiene una sola fila, se pregunta si hizo algo mal, esperando un resultado más "dramático". Por qué pasa: después de ver reportes de 3 y 4 filas en los módulos 1 y 2, una sola fila puede sentirse como poco. Cómo detectarlo: repasa la tabla original de doce líneas del módulo 1 — solo una fila (ORD-9508) tiene, específicamente, un problema de consistency. Cada dimensión de calidad de datos de esta guía corresponde a exactamente una fila del incidente de S04 (excepto uniqueness, que cuenta las dos apariciones del duplicado). Cómo corregirlo: el número correcto de filas huérfanas para este archivo es uno — un resultado de una sola fila, exacto y correcto, no es una señal de que algo falló en tu código.
Sorprenderse de que orphans no incluya ninguna de las tres filas que ya atrapó OrdersSchema. Qué pasa: alguien espera ver ORD-9502, ORD-9503 o ORD-9507 también en el resultado del anti-join, porque "ya sabe" que esas filas tienen problemas. Por qué pasa: es fácil pensar en "filas problemáticas" como una categoría única, en vez de recordar que cada dimensión de calidad de datos es una pregunta distinta, y cada fila del incidente de S04 rompe exactamente una. Cómo detectarlo: revisa el product_id de esas tres filas — P002, P003, P001, en ese orden — los tres existen perfectamente en dim_product. Cómo corregirlo: validate_referential_integrity() solo puede marcar problemas de integridad referencial; una fila con unit_price nulo o quantity negativo, pero con un product_id que sí existe, es completamente invisible para esta función, por diseño — no es un descuido, es exactamente el alcance que le corresponde.
Concluir que, con este resultado, S04 ya está "totalmente validada". Qué pasa: alguien, satisfecho con el resultado limpio y preciso de esta lección, reporta que el archivo de S04 ya pasó por un control de calidad completo. Por qué pasa: después de tres módulos consecutivos de esta guía, es fácil perder de vista cuánto queda todavía. Cómo detectarlo: cuenta cuántas de las seis dimensiones de calidad de datos tienen, a esta altura de la guía, un check real y ejecutado: completeness, uniqueness, validity (módulo 2), consistency (este módulo) — cuatro de seis. Cómo corregirlo: la lección 8 —el proyecto de este módulo— hace exactamente ese conteo con precisión, y confirma cuáles dos dimensiones siguen abiertas: accuracy (módulo 5) y freshness (módulo 6).
Ejercicios
Ejercicio 1 — Confirma que las tres filas ya atrapadas por Pandera tienen product_id válido. Sin ejecutar nada todavía, escribe de memoria el product_id de ORD-9502, ORD-9503 y ORD-9507 (revisa el CSV del módulo 1 si hace falta), y confirma que los tres están en la lista ['P001', 'P002', 'P003', 'P004'].
Ver solución
ORD-9502 tiene product_id="P002"; ORD-9503 tiene product_id="P003"; ORD-9507 tiene product_id="P001". Los tres existen en dim_product. Esto confirma, con los datos concretos del incidente, algo que ya explicó la Profundización de esta lección: los problemas de completeness, uniqueness y validity de esas tres filas son completamente independientes de si su product_id es válido — una fila puede tener un producto perfectamente real y, al mismo tiempo, romper alguna otra regla de calidad.
Ejercicio 2 — Verifica el conteo de dimensiones cubiertas hasta ahora, con código. Escribe un pequeño script que combine el resultado de OrdersSchema.validate() (módulo 2) con orphans de esta lección, y cuente cuántas de las doce filas de orders_s04 tienen al menos un problema conocido hasta este punto de la guía.
Ver solución
import pandera.polars as pa
class OrdersSchema(pa.DataFrameModel):
order_id: str = pa.Field(unique=True)
unit_price: float = pa.Field(nullable=False, ge=0)
quantity: int = pa.Field(gt=0)
try:
OrdersSchema.validate(orders_df, lazy=True)
pandera_ids = set()
except pa.errors.SchemaErrors as exc:
pandera_ids = set(
exc.failure_cases.with_columns(
pl.col("index").map_elements(lambda i: orders_df["order_id"][i], return_dtype=pl.String).alias("order_id")
)["order_id"].to_list()
)
referential_ids = set(orphans["order_id"].to_list())
all_flagged = pandera_ids | referential_ids
print(f"Pandera: {sorted(pandera_ids)}")
print(f"Referencial: {sorted(referential_ids)}")
print(f"Total de order_id distintos con algun problema conocido: {len(all_flagged)} de {orders_df.height}")
Salida esperada:
Pandera: ['ORD-9502', 'ORD-9503', 'ORD-9507']
Referencial: ['ORD-9508']
Total de order_id distintos con algun problema conocido: 4 de 12
Cuatro order_id distintos con algún problema conocido, de doce filas totales —ORD-9509 sigue sin aparecer en ningún lado—. La lección 7 formaliza exactamente este tipo de combinación en una sola función reusable.
Ejercicio 3 — Argumenta si validate_referential_integrity() necesitaría cambiar si S04 mandara un segundo archivo mañana, con un producto huérfano distinto. En 2-3 frases, explica por qué la función, tal como está escrita, no necesitaría ningún cambio si mañana S04 mandara orders_2026-08-17.csv con, por ejemplo, product_id="P200" en vez de "P099".
Ver solución
validate_referential_integrity() no menciona "P099" ni ningún valor específico en ningún lugar de su código —recibe dos DataFrames como parámetros y hace un join genérico sobre la columna product_id—, así que funcionaría exactamente igual sobre cualquier archivo nuevo de órdenes, sin necesitar ningún cambio de código: bastaría con pasarle el nuevo orders_df como primer argumento. Esta es la misma cualidad de reusabilidad que ya destacó el módulo 2 sobre OrdersSchema —ninguna de las dos herramientas de esta guía está escrita "a la medida" del incidente puntual de S04, ambas son funciones genéricas que reciben los datos como parámetro, no como valores fijos dentro del código.
Resumen y siguiente paso
En esta lección corriste validate_referential_integrity(), sin ningún cambio respecto a la lección 4, sobre las doce líneas reales de orders_2026-08-14.csv y los cuatro productos reales de dim_product. El resultado: exactamente una fila huérfana, ORD-9508, la misma fila que dos herramientas anteriores de esta guía —validate_orders() en el módulo 1, OrdersSchema en el módulo 2— dejaron pasar sin ninguna marca. Confirmaste también, con código, que el resultado no se desborda hacia las otras filas: las tres que ya atrapó Pandera siguen fuera de este resultado (tienen product_id válido), y ORD-9509 sigue completamente silenciosa (su problema no es de integridad referencial).
Antes de avanzar deberías poder: explicar por qué el resultado correcto de esta lección tiene exactamente una fila, ni más ni menos; y nombrar, sin mirar el código de nuevo, cuáles cuatro order_id distintos tienen, a esta altura de la guía, al menos un problema conocido.
Tienes la fila atrapada. La lección 7 combina este resultado con el de OrdersSchema (Pandera) y con check_retransmission_consistency() (lección 5) en un solo reporte — el primer borrador del sistema de confianza completo que esta guía construye a lo largo de sus ocho módulos.
Recursos
- Módulo 1, lección 6, de esta misma guía — la primera vez que
ORD-9508aparece en el archivo real, pasandovalidate_orders()sin ninguna marca.src/guides/data-reliability-and-governance-guide/workbook/module-01-when-green-does-not-mean-correct/es/06-running-the-old-quality-gate-on-s04.md. En español. - Módulo 2, lección 7, de esta misma guía — la confirmación, con Pandera, de que
ORD-9508sigue sin marca después deOrdersSchema.validate(lazy=True).src/guides/data-reliability-and-governance-guide/workbook/module-02-declarative-data-quality-tests-with-pandera/es/07-validity-checks-and-reading-failure-cases.md. En español. - Polars — "Joins" (guía de usuario oficial), la misma referencia de la lección 4, reutilizada aquí sobre los datos reales. docs.pola.rs/user-guide/transformations/joins. En inglés.
- DISEÑO de esta guía — la fila exacta (
ORD-9508,product_id="P099") que este módulo debía atrapar, confirmada aquí con evidencia ejecutada.src/guides/data-reliability-and-governance-guide/DISENO.md. En español.