Módulo 6: Freshness Volume And Lineage
Qué responde el linaje que un contrato no responde
Descripción
Con check_freshness() y check_volume() completos, esta guía ya tiene una función ejecutable para cada una de las seis dimensiones de fila-y-tabla, más el chequeo de volumen. Esta lección cambia de tema por completo: en vez de preguntar "¿estos datos están bien?" —la pregunta que ha organizado cada lección desde el módulo 1—, pregunta algo que ninguna herramienta anterior de esta guía intentó responder: ¿de dónde viene, exactamente, cada columna del warehouse de Kiosko?
Conexión con el módulo. Esta lección es puramente conceptual —sin escribir ningún diccionario todavía, eso es la lección 7—, pero es indispensable antes de construirlo: si no queda claro qué pregunta responde el linaje que el contrato del módulo 4 no responde, LINEAGE_MAP se sentiría como trabajo redundante en vez de una capa genuinamente nueva.
Una analogía completa: el árbol genealógico de una columna
Un certificado de nacimiento describe a una persona: nombre, fecha, lugar. Es un documento válido, completo en lo que se propone documentar. Pero no dice nada sobre de dónde vienen los rasgos de esa persona —qué abuelo aportó el apellido, de qué línea familiar viene tal característica—. Para eso hace falta un documento distinto: un árbol genealógico, que traza, generación por generación, de dónde desciende cada rama.
Un contrato de datos —como orders_contract.yaml del módulo 4— es el certificado de nacimiento de una columna: dice que unit_price debe ser un float, no nulo, >= 0. Es una descripción válida y completa de lo que esa columna debe ser. Pero no dice nada sobre de dónde viene —si unit_price se copió directo de un sistema de punto de venta, si pasó por alguna conversión de moneda, o si otra columna, en otra tabla, se calculó a partir de ella—. El linaje es el árbol genealógico: no describe cómo debe verse una columna, describe su ascendencia — de qué columna, en qué tabla, en qué momento del pipeline, desciende.
Ejemplo trabajado: lo que el contrato de S04 nunca declaró
El contrato del módulo 4, orders_contract.yaml, declaró reglas para tres columnas: order_id, unit_price, quantity. Vale la pena confirmar, con una comparación directa, cuánto de la realidad de Kiosko queda fuera de ese contrato — no porque esté mal escrito, sino porque nunca se propuso cubrir esto.
# contract_vs_warehouse.py
ORDERS_S04_COLUMNS = ["order_id", "store_id", "product_id", "quantity", "unit_price", "order_ts"]
CONTRACT_SCHEMA_COLUMNS = ["order_id", "unit_price", "quantity"]
FACT_ORDERS_COLUMNS = ["order_id", "store_id", "product_id", "quantity", "unit_price", "revenue", "order_ts"]
no_contract_rule = sorted(set(ORDERS_S04_COLUMNS) - set(CONTRACT_SCHEMA_COLUMNS))
print(f"Columnas de orders_s04: {ORDERS_S04_COLUMNS}")
print(f"Columnas con reglas en el contrato: {CONTRACT_SCHEMA_COLUMNS}")
print(f"Columnas de orders_s04 SIN ninguna regla en el contrato: {no_contract_rule}")
print()
new_in_fact = sorted(set(FACT_ORDERS_COLUMNS) - set(ORDERS_S04_COLUMNS))
print(f"Columnas de fact_orders: {FACT_ORDERS_COLUMNS}")
print(f"Columnas de fact_orders que NO existen en orders_s04 (ni en el contrato): {new_in_fact}")
Qué esperar. Al correr python3 contract_vs_warehouse.py, la salida es exactamente esta:
Columnas de orders_s04: ['order_id', 'store_id', 'product_id', 'quantity', 'unit_price', 'order_ts']
Columnas con reglas en el contrato: ['order_id', 'unit_price', 'quantity']
Columnas de orders_s04 SIN ninguna regla en el contrato: ['order_ts', 'product_id', 'store_id']
Columnas de fact_orders: ['order_id', 'store_id', 'product_id', 'quantity', 'unit_price', 'revenue', 'order_ts']
Columnas de fact_orders que NO existen en orders_s04 (ni en el contrato): ['revenue']
Dos hallazgos, cada uno con una lectura distinta. Primero: orders_s04 tiene seis columnas reales (confirmadas con DESCRIBE orders_s04 desde el módulo 2), pero el contrato solo declaró reglas para tres — store_id, product_id y order_ts nunca tuvieron un Field de Pandera ni una entrada en schema: del YAML. Eso no es un error del contrato — cubrió exactamente lo que la lección 3 del módulo 4 identificó como necesario para las reglas de negocio que ya se conocían—, pero confirma que ni siquiera dentro de las columnas que sí existen en el archivo original, el contrato pretende ser un catálogo completo.
Segundo, y más importante para esta lección: fact_orders —la tabla final del warehouse, la que construyó data-modeling-for-analytics-guide— tiene una columna, revenue, que no existe en ningún lado dentro de orders_s04. Nadie la manda en el CSV. Ningún Field del contrato la menciona, porque no tiene sentido declarar una regla sobre una columna que ni siquiera existe todavía en el punto donde vive el contrato. revenue se calcula, en algún paso entre orders_s04 y fact_orders, a partir de otras columnas — y ni el contrato, ni OrdersSchema, ni ninguna de las cinco herramientas anteriores de esta guía dice, en ningún lugar, cuáles.
Tabla comparativa: qué responde cada artefacto
| Pregunta | ¿La responde el contrato (módulo 4)? | ¿La responde el linaje (este módulo)? |
|---|---|---|
¿unit_price puede ser nulo? | Sí — nullable: false | No es su pregunta |
| ¿Cuántas filas debería traer el archivo? | Sí — sla.row_count | No es su pregunta |
| ¿Qué tan tarde puede llegar? | Sí — sla.freshness_hours | No es su pregunta |
¿De qué columna, de qué tabla, viene fact_orders.revenue? | No — nunca lo declaró | Sí — exactamente la pregunta que responde |
¿dim_store.country es un dato copiado o calculado? | No — country ni siquiera vive en orders_s04 | Sí — el linaje traza también transformaciones, no solo copias |
Si unit_price cambia de significado (dólares a centavos), ¿qué otras columnas del warehouse se ven afectadas? | No — el contrato describe una tabla a la vez | Sí — el linaje cruza tablas y muestra el impacto río abajo |
La última fila de la tabla merece una pausa. El bug de dólares-a-centavos que atrapó el módulo 5 (ORD-9509, unit_price=60.00) vivía dentro de orders_s04, una sola tabla. Pero si ese mismo tipo de error hubiera pasado desapercibido y llegado hasta fact_orders, no se quedaría contenido en una columna: revenue —calculada a partir de unit_price— también estaría mal, y cualquier reporte que sumara revenue (como el que ya construyó data-modeling-for-analytics-guide) heredaría el error sin que nadie supiera de dónde vino. Sin un mapa de linaje, rastrear ese impacto significaría leer, a mano, cada consulta SQL del warehouse buscando dónde se usa unit_price — exactamente el trabajo que un mapa de linaje ya escrito evita.
Diagrama: dos documentos, dos preguntas distintas sobre la misma columna
flowchart LR
subgraph CONTRATO["orders_contract.yaml (M4)"]
C1["unit_price:\ntype=float\nnullable=false\nminimum=0"]
end
subgraph LINAJE["LINEAGE_MAP (M6, leccion 7)"]
L1["fact_orders.revenue\n<- orders.quantity\n<- orders.unit_price"]
end
C1 -.->|"'¿que forma debe tener\neste valor?'"| Q1["Pregunta del contrato"]
L1 -.->|"'¿de que columna\nviene este valor,\ny que otras columnas\nlo heredan?'"| Q2["Pregunta del linaje"]
Profundización: por qué el linaje se vuelve más importante mientras más crece el ecosistema
Vale la pena conectar esta lección con el mapa completo del ecosistema de NIEVA Data Engineering. Kiosko ya cruzó, en las ocho guías anteriores a esta, varias fronteras de herramienta: un CSV crudo (data-engineering-foundations-guide), un warehouse modelado en DuckDB (data-modeling-for-analytics-guide), un proyecto dbt que transforma esas tablas (dbt-analytics-engineering-guide), y —nombrados, aunque no construidos con profundidad en esta guía— Spark y tablas Iceberg. Cada cruce de herramienta es un punto donde el linaje se puede perder si nadie lo documenta explícitamente: dbt docs generate sabe trazar el grafo de dependencias dentro de un proyecto dbt (qué modelo .sql depende de cuál otro), pero no sabe nada sobre qué columna del CSV original alimentó la primera tabla de ese proyecto — ese primer eslabón vive fuera del alcance de dbt, en el paso de extracción e ingesta.
Esta es, con precisión, la frontera que trazó el diseño de este módulo desde el inicio: el linaje automático de una sola herramienta (dbt docs) documenta un tramo del camino completo. El linaje de esta lección —y el que construye la lección 7— cruza herramientas: desde el CSV, pasando por DuckDB, hasta la columna calculada del warehouse. Cuantas más herramientas distintas participan en un pipeline real (y el ecosistema completo de NIEVA ya demostró que son varias), más valioso se vuelve tener un mapa que las cruce todas, en vez de confiar en que cada herramienta documenta perfectamente su propio tramo y que esos tramos, juntos, cuentan la historia completa sin ningún hueco.
Errores comunes
Pensar que el linaje reemplaza al contrato, o que uno de los dos ya no hace falta si existe el otro. Qué pasa: alguien, después de esta lección, concluye que si ya existe un mapa de linaje completo, el contrato del módulo 4 se vuelve redundante —o al revés—. Por qué pasa: ambos artefactos describen "de dónde viene la confianza en los datos de Kiosko", y es fácil pensar que cumplen el mismo rol. Cómo detectarlo: revisa la tabla comparativa de esta lección — cada fila muestra una pregunta que solo uno de los dos artefactos responde. Ningún renglón tiene un "Sí" en ambas columnas. Cómo corregirlo: trátalos como complementarios, nunca sustitutos: el contrato certifica la forma de una tabla en un momento dado; el linaje traza cómo esa tabla, y cada una de sus columnas, llegó a existir. Un sistema de confianza completo —el que ensambla el proyecto del módulo 8— necesita los dos.
Asumir que el linaje solo importa para columnas calculadas, nunca para columnas copiadas directamente. Qué pasa: alguien, viendo el ejemplo de revenue (una columna genuinamente calculada), concluye que el linaje solo tiene sentido para transformaciones complejas, y que una columna copiada 1:1 (como dim_product.product_name, tomada directo de products.product_name) no necesita ningún mapeo. Por qué pasa: una copia directa se siente "obvia", sin ningún misterio que documentar. Cómo detectarlo: pregúntate qué pasaría si, algún día, alguien renombrara product_name en la fuente original — sin un mapa de linaje que documente la relación, nadie sabría automáticamente que dim_product.product_name también necesita actualizarse. Cómo corregirlo: la lección 7 va a mapear todas las columnas del warehouse, copiadas o calculadas por igual — la distinción entre "copia directa" y "transformación" es información útil dentro del mapa (y la lección 7 la señala explícitamente para el caso de dim_store.country), pero no es una razón para omitir las columnas copiadas del mapa completo.
Creer que el linaje es exclusivo de sistemas grandes, y que no vale la pena para un caso tan chico como Kiosko. Qué pasa: alguien argumenta que, con solo tres tablas (orders_s04, dim_product, dim_store) y quince columnas en total, documentar el linaje a mano es trabajo innecesario para un caso de este tamaño. Por qué pasa: el linaje suena a una disciplina de "big data" — grafos enormes, cientos de tablas, algo que solo importa a escala. Cómo detectarlo: si tu razonamiento es "esto es demasiado pequeño para necesitar linaje", revisa cuánto tiempo tomaría, sin ningún mapa, responder "¿qué se rompe si cambio unit_price?" para el caso real de Kiosko — la respuesta ya está en la Profundización de esta lección: revisar a mano cada consulta SQL del warehouse. Cómo corregirlo: el valor del linaje no depende del tamaño del sistema, depende de si alguien necesita responder "¿de dónde viene esto?" o "¿qué se ve afectado si esto cambia?" — preguntas que ya son reales para Kiosko con solo tres tablas, y que se vuelven imposibles de responder a mano, no importa el tamaño, si nadie las documenta desde el principio.
Ejercicios
Ejercicio 1 — Reproduce la comparación de columnas con tus propias palabras. Sin ver el código de esta lección, escribe de memoria la lista de las seis columnas de orders_s04 y las tres que sí tienen reglas en el contrato. Confirma tu respuesta contra contract_vs_warehouse.py.
Ver solución
orders_s04: order_id, store_id, product_id, quantity, unit_price, order_ts (confirmado por DESCRIBE orders_s04 en el módulo 2, lección 4). Con reglas en el contrato: order_id (único, no nulo), unit_price (no nulo, >= 0), quantity (no nulo, > 0) — las mismas tres que ya declaró OrdersSchema desde el módulo 2. Las otras tres (store_id, product_id, order_ts) existen en la tabla, pero nunca tuvieron una regla de Pandera ni una entrada en el YAML del contrato — no porque no importen, sino porque ninguna herramienta de las lecciones anteriores necesitó todavía una regla explícita sobre ellas (product_id, por ejemplo, se valida por otro camino: el anti-join contra dim_product del módulo 3, no una regla de tipo o rango).
Ejercicio 2 — Argumenta si store_id necesitaría, algún día, una entrada en LINEAGE_MAP aunque nunca haya tenido una regla en el contrato. En 2-3 frases, considerando que el linaje y el contrato responden preguntas distintas (confirmado por la tabla comparativa de esta lección), argumenta si store_id debería aparecer en el mapa de linaje de la lección 7, aunque el contrato nunca haya declarado ninguna regla sobre ella.
Ver solución
Sí, debería aparecer — la ausencia de una regla en el contrato no dice nada sobre si una columna necesita trazarse en el linaje, porque son preguntas completamente distintas (confirmado por la tabla comparativa de esta lección: "¿qué forma debe tener?" contra "¿de dónde viene?"). store_id sí viaja desde orders_s04 hasta fact_orders sin ningún cambio, y también participa en el join contra dim_store que construye el star schema completo (data-modeling-for-analytics-guide) — cualquier persona que necesite entender de dónde sale el store_id de una fila de fact_orders necesita esa respuesta, sin importar si el contrato alguna vez declaró una regla de validación sobre ella.
Ejercicio 3 — Predice, antes de la lección 7, si dim_store.country va a tener un mapeo de linaje "directo" o "derivado". Usando lo que ya sabes desde el módulo 1 de esta guía (la regla determinista Bogotá→Colombia, Lima→Peru, Santiago→Chile, Ciudad de México→Mexico), predice si LINEAGE_MAP["dim_store.country"] va a apuntar a una columna que se copia igual, o a una que se transforma.
Ver solución
Se transforma — country nunca existe como columna independiente en ninguna fuente cruda de Kiosko (stores solo tiene store_id, store_name, city, según el DISEÑO de data-engineering-foundations-guide); se deriva de city mediante una función determinista, la misma que ya aplicó lakehouse-and-iceberg-guide (módulo 4) y que esta guía extendió a S04 en su primera lección. Esto significa que, a diferencia de una columna como dim_product.product_name (que se copia sin ningún cambio desde products.product_name), dim_store.country sí depende de una lógica de transformación —aunque simple y determinista—, y un mapa de linaje completo debería, idealmente, poder distinguir entre ambos tipos de relación. La lección 7 confirma esta predicción con el mapa real.
Resumen y siguiente paso
En esta lección estableciste, con una comparación ejecutada de verdad (no solo con prosa), la frontera exacta entre lo que responde un contrato de datos y lo que responde el linaje: el contrato del módulo 4 declaró reglas para tres de las seis columnas de orders_s04, y ninguna regla en absoluto sobre revenue —una columna que ni siquiera existe hasta llegar a fact_orders—. Confirmaste, con la analogía del árbol genealógico, que el linaje no describe cómo debe verse una columna, sino de dónde desciende — una pregunta de naturaleza completamente distinta, que se vuelve más urgente mientras más herramientas distintas cruza un pipeline real.
Antes de avanzar deberías poder: nombrar, sin ver la tabla comparativa de nuevo, al menos dos preguntas que solo responde el contrato y dos que solo responde el linaje; y explicar por qué revenue es el ejemplo más claro de esta guía de una columna que el contrato nunca podría haber cubierto.
Tienes el argumento conceptual completo. La lección 7 lo convierte en código: LINEAGE_MAP, un diccionario de Python trazado a mano sobre las columnas reales de fact_orders, dim_product y dim_store — y el nombre del estándar abierto, OpenLineage, que la industria usa para automatizar exactamente este mapa en producción.
Recursos
- Módulo 2, lección 4, de esta misma guía — fuente de
DESCRIBE orders_s04, las seis columnas reales de la tabla comparadas en esta lección.src/guides/data-reliability-and-governance-guide/workbook/module-02-declarative-data-quality-tests-with-pandera/es/04-installing-pandera-and-bridging-duckdb-to-polars.md. En español. - Módulo 4, lección 3, de esta misma guía — fuente de las tres columnas con reglas en
orders_contract.yaml, contrastadas contra el total de la tabla.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. data-modeling-for-analytics-guide, módulo 8, proyecto — fuente de la columnarevenuedefact_orders, calculada a partir dequantityyunit_price.src/guides/data-modeling-for-analytics-guide/workbook/module-08-project-kioskos-analytics-warehouse/es/08-project-kioskos-first-analytics-warehouse.md. En español.- OpenLineage — documentación oficial (el estándar abierto que nombra la lección 7, con el modelo de
Dataset/Job/Run). openlineage.io/docs. En inglés. - DISEÑO de esta guía — la frontera exacta entre el linaje de esta guía y el linaje automático de
dbt docs, de un solo proyecto.src/guides/data-reliability-and-governance-guide/DISENO.md. En español.