Módulo 1: When Green Does Not Mean Correct

Qué se le escapa a una compuerta local

Descripción

La lección 6 dejó dos filas silenciosas adentro de valid: ORD-9508 (product_id=P099, un producto que no existe) y ORD-9509 (unit_price=60.00, un precio con un error de dos órdenes de magnitud). Esta lección no corre ningún código nuevo de validate_orders() —ya sabes exactamente lo que hace—; en cambio, escribe el código mínimo, fuera de esa función, necesario para hacer visibles esas dos filas, y usa ese ejercicio para explicar con precisión por qué una compuerta local —una que solo mira una fila a la vez, sin ninguna referencia externa— nunca podría haberlas atrapado, sin importar cuántas reglas más se le agregaran.

Conexión con el módulo. Esta lección cierra el diagnóstico técnico del módulo, y deja el terreno preparado para los módulos 2 a 6 de esta guía: cada uno de ellos resuelve, con una herramienta específica, exactamente una de las brechas que esta lección nombra con precisión.

Una analogía: el corrector ortográfico que no sabe historia

Un corrector ortográfico revisa un texto palabra por palabra: ¿esta secuencia de letras es una palabra real del idioma? Es extraordinariamente bueno en esa tarea específica, y la resuelve mirando cada palabra de forma aislada, sin ninguna necesidad de conocer el resto del documento. Pero un corrector ortográfico no tiene ninguna forma de detectar que la frase "Cristóbal Colón llegó a América en 1592" tiene un error — cada palabra individual está perfectamente bien escrita. El error es de hecho, no de ortografía, y un corrector diseñado para verificar letra por letra nunca va a atraparlo, sin importar cuántas reglas de gramática más se le agreguen: revisar hechos históricos no es, ni puede convertirse en, una extensión de revisar ortografía. Son dos tipos de verificación completamente distintos, que necesitan herramientas completamente distintas.

validate_orders() es ese corrector ortográfico, aplicado a filas de orders. Es extraordinariamente bueno revisando cada fila de forma aislada: ¿tiene todos los campos? ¿los tipos son correctos? ¿los valores respetan un rango simple? Pero ORD-9508 con product_id=P099 es, para validate_orders(), una fila perfectamente "bien escrita" — cada carácter, cada tipo, cada valor individual pasa la revisión. El error no es de forma, es de hecho: P099 no es un producto que exista en el mundo real de Kiosko. Y ese tipo de error necesita algo que ninguna regla de rango simple puede dar: una referencia a otra fuente de verdad, en este caso, el catálogo real de productos.

Ejemplo trabajado: haciendo visible lo que validate_orders() no ve

Este código no reemplaza ni extiende validate_orders() — vive completamente aparte, como un diagnóstico manual, exactamente lo que le corresponde a un módulo que todavía no construye la solución real:

# what_slips_through.py
import csv
from kiosko import PRODUCTS

KNOWN_PRODUCT_IDS = {p["product_id"] for p in PRODUCTS}

with open("orders_2026-08-14.csv", newline="") as f:
    rows = list(csv.DictReader(f))

# 1. consistency: filas con un product_id que no existe en el catalogo
orphan_rows = [row for row in rows if row["product_id"] not in KNOWN_PRODUCT_IDS]
print("=== Filas con product_id huerfano (consistency) ===")
for row in orphan_rows:
    print(f"  {row['order_id']}: product_id='{row['product_id']}' no esta en {sorted(KNOWN_PRODUCT_IDS)}")

# 2. accuracy: comparar cada precio de P002 contra los demas precios de P002 en el MISMO archivo
p002_rows = [row for row in rows if row["product_id"] == "P002"]
p002_prices = [float(row["unit_price"]) for row in p002_rows]
print(f"\n=== Todos los precios de P002 en este archivo (accuracy) ===")
for row in p002_rows:
    print(f"  {row['order_id']}: unit_price={row['unit_price']}")

typical_price = sorted(p002_prices)[len(p002_prices) // 2]  # la mediana, sin usar la fila outlier a mano
print(f"\nPrecio tipico de P002 en este archivo (mediana): {typical_price}")
for row in p002_rows:
    price = float(row["unit_price"])
    ratio = price / typical_price
    flag = " <-- fuera de lo normal" if ratio > 2 or ratio < 0.5 else ""
    print(f"  {row['order_id']}: unit_price={price} ({ratio:.1f}x el tipico){flag}")

Qué esperar. Al correr python3 what_slips_through.py, la salida es exactamente esta:

=== Filas con product_id huerfano (consistency) ===
  ORD-9508: product_id='P099' no esta en ['P001', 'P002', 'P003', 'P004']

=== Todos los precios de P002 en este archivo (accuracy) ===
  ORD-9502: unit_price=1.20
  ORD-9509: unit_price=60.00
  ORD-9502: unit_price=1.20
  ORD-9511: unit_price=1.20

Precio tipico de P002 en este archivo (mediana): 1.2
  ORD-9502: unit_price=1.2 (1.0x el tipico)
  ORD-9509: unit_price=60.0 (50.0x el tipico) <-- fuera de lo normal
  ORD-9502: unit_price=1.2 (1.0x el tipico)
  ORD-9511: unit_price=1.2 (1.0x el tipico)

Fíjate en algo real que acaba de pasar: ORD-9502 aparece dos veces en esta lista, en primer y tercer lugar, porque aparece dos veces en el archivo —la retransmisión duplicada que ya conoces de la lección 6— y por lo tanto dos veces en p002_rows. Ni siquiera este diagnóstico manual, escrito para una lección introductoria, está libre de que un duplicado se cuele en un conteo si no se tiene cuidado: cuatro líneas de precio, no tres, aunque solo tres order_id distintos participen. La cifra que sí importa para el punto de esta lección queda igual de clara: ORD-9509, con unit_price=60.00, es la única fila marcada <-- fuera de lo normal, a 50 veces el precio típico de P002 dentro de este mismo archivo — ni siquiera necesitaste el histórico de la semana canónica para verlo, la anomalía es evidente comparando el archivo contra sí mismo.

Diagrama: por qué agregar "más reglas" a validate_orders() no alcanza

flowchart TB
    subgraph LOCAL["Lo que una regla local PUEDE verificar"]
        A["¿Este campo existe?"]
        B["¿Este valor tiene el tipo correcto?"]
        C["¿Este valor esta dentro\nde un rango fijo, conocido de antemano?"]
    end

    subgraph EXTERNO["Lo que necesita una referencia EXTERNA"]
        D["¿Este product_id existe\nen la tabla de productos?"]
        E["¿Este precio se parece\na lo que este producto\nnormalmente vale?"]
    end

    LOCAL -->|"validate_orders() vive aqui"| F["Compuerta local:\nsuficiente para A, B, C"]
    EXTERNO -->|"necesita otra tabla\no una linea base"| G["Compuerta local:\nestructuralmente ciega aqui"]

El diagrama distingue, con precisión, entre dos tipos de regla. Una regla local —"quantity debe ser mayor que cero"— se puede escribir y evaluar mirando solo el valor de una columna, dentro de una sola fila, sin necesitar nada más. Una regla que necesita referencia externa —"product_id debe existir en la tabla de productos", "unit_price debe parecerse al precio normal de este producto"— necesita, por definición, algo que vive fuera de la fila: otra tabla, un cálculo agregado sobre datos históricos, una línea base. validate_orders(), tal como la diseñó foundations M5, nunca recibe ninguna de esas dos cosas como argumento — solo recibe la lista de filas crudas. No es que le falte una regla más; le falta un tipo de insumo que su diseño actual no contempla.

Profundización: la diferencia entre "no lo intentó" y "no podía"

Vale la pena ser preciso sobre una distinción que ya se insinuó en la lección 4: validate_orders() no "falló" en atrapar ORD-9508 y ORD-9509 en el sentido de que intentó hacerlo y se equivocó. Nunca lo intentó, porque su firma —validate_orders(rows: list[dict])— solo recibe las filas mismas, sin ningún parámetro adicional que representara "el catálogo de productos válidos" o "los precios históricos de referencia". Podrías, en teoría, modificar la firma para que reciba esos datos extra —validate_orders(rows, known_products, reference_prices)—, pero en el momento en que haces eso, ya dejaste de estar extendiendo una compuerta de esquema/nulos/tipo/rango: estás construyendo un sistema distinto, con un tipo de dependencia distinto (una tabla externa, una línea base calculada), que merece su propia arquitectura, no un parámetro más colgado de una función que ya cumple su propósito original.

Esta es, con precisión, la razón de diseño detrás de la estructura completa de esta guía: el módulo 3 no modifica validate_orders() para que reciba dim_product como argumento — construye una función nueva y separada, validate_referential_integrity(orders_df, dim_product_df), con su propia responsabilidad. El módulo 5 no modifica check_business_rules() para que reciba una línea base de precios — construye check_price_baseline(), también separada. Cada dimensión que necesita una referencia externa se gana su propia pieza, en vez de inflar una función que ya estaba completa para lo que hacía.

Errores comunes

Intentar "arreglar" validate_orders() en este módulo, agregándole una lista de productos válidos a mano. Qué pasa: alguien, motivado por ver ORD-9508 sin marcar, edita check_business_rules() para agregar if order.product_id not in {"P001", "P002", "P003", "P004"}: reasons.append(...), con la lista de productos codificada directamente en el código. Por qué pasa: es la solución más rápida y más obvia frente al problema recién visto. Cómo detectarlo: si tu check_business_rules() ahora tiene una lista de product_id válidos escrita a mano dentro de la función, tienes un parche que funciona hoy, con cuatro productos, pero que se desincroniza en el momento en que Kiosko agregue un quinto producto en cualquier otro lado del sistema (por ejemplo, en dim_product del warehouse) sin acordarse de actualizar también esta lista. Cómo corregirlo: este módulo es deliberadamente de diagnóstico, no de arreglo — la solución real, que lee el catálogo real desde kiosko.duckdb en vez de codificarlo a mano, es el trabajo completo del módulo 3.

Pensar que la mediana usada en el ejemplo trabajado "ya resuelve" accuracy. Qué pasa: alguien ve que comparar contra la mediana del propio archivo detectó exitosamente el precio de 60.00, y concluye que ese es el método correcto y suficiente para toda esta guía. Por qué pasa: funcionó, de forma visible, en este caso concreto. Cómo detectarlo: pregúntate qué pasaría si el archivo de S04 tuviera una sola fila de P002 —la mediana de una lista de un solo elemento es ese mismo elemento, así que ningún precio se marcaría nunca como anómalo, sin importar cuán absurdo fuera—. Cómo corregirlo: la línea base real de accuracy, que construye el módulo 5 de esta guía, se calcula sobre la semana canónica completa de Kiosko —cuarenta filas ya conocidas y confiables—, no sobre el archivo nuevo y potencialmente problemático que se está revisando. Comparar un archivo sospechoso contra sí mismo es un truco de diagnóstico útil para esta lección, pero no es la arquitectura correcta de una detección de anomalías real.

Concluir que este módulo "ya resolvió" consistency y accuracy porque el diagnóstico las nombró. Qué pasa: después de ver el código de esta lección atrapar exitosamente ORD-9508 y ORD-9509 con un script de unas pocas líneas, alguien asume que el trabajo de los módulos 3 y 5 de esta guía ya está hecho. Por qué pasa: el diagnóstico se siente, superficialmente, parecido a la solución. Cómo detectarlo: el script de esta lección no se integra a ningún pipeline, no separa filas en valid/rejected de forma reutilizable, no lee el catálogo real desde el warehouse, y su método de accuracy (comparar contra la mediana del mismo archivo) ya se descartó como insuficiente en el error común anterior. Cómo corregirlo: este código es exactamente lo que dice ser — un diagnóstico manual, de una sola vez, para confirmar visualmente qué se le escapa a la compuerta vieja. La solución reutilizable, integrada, con datos reales del warehouse, es el trabajo de los siete módulos que siguen.

Ejercicios

Ejercicio 1 — Calcula el mismo ratio contra el precio de la semana canónica, no contra la mediana del archivo. Ya sabes, de data-engineering-foundations-guide, que P002 se vendió consistentemente a 1.20 durante toda la semana canónica de Kiosko (2026-08-03 al 2026-08-09). Calcula el ratio de ORD-9509 (60.00) contra ese precio histórico, en vez de contra la mediana del archivo de S04. ¿Cambia el resultado?

Ver solución
CANONICAL_WEEK_PRICE_P002 = 1.20  # confirmado en las 40 filas de la semana canonica, foundations M1-M2
ratio_vs_canonical = 60.00 / CANONICAL_WEEK_PRICE_P002
print(f"ratio contra el precio de la semana canonica: {ratio_vs_canonical}x")

Salida esperada:

ratio contra el precio de la semana canonica: 50.0x

El resultado es idéntico —50.0x— porque, en este archivo específico, la mediana de los precios de P002 (1.20) coincide exactamente con el precio de la semana canónica. Esto no es una coincidencia garantizada en general —podría no coincidir si el archivo nuevo tuviera una mezcla distinta de precios—, y es exactamente la razón por la que el módulo 5 de esta guía construye la línea base desde la semana canónica confiable, no desde el archivo bajo sospecha: en un caso donde no coincidieran, la línea base de la semana canónica sería la fuente de verdad correcta, no un promedio calculado sobre datos que todavía no se confirmaron como confiables.

Ejercicio 2 — Diseña, sin implementarlo, un tercer tipo de verificación externa. Además de consistency (contra otra tabla) y accuracy (contra una línea base), ¿se te ocurre alguna otra pregunta sobre los datos de Kiosko que necesitaría, igual que estas dos, algo externo a la fila individual? Descríbela en 2-3 frases, sin escribir código.

Ver solución

No hay una única respuesta correcta, pero un ejemplo razonable es: "¿esta orden ocurrió dentro del horario real de atención de la tienda S04?" — verificar eso necesitaría una tabla externa con los horarios de cada tienda (algo que Kiosko no ha declarado todavía en este ecosistema), comparada contra el order_ts de cada fila. Igual que consistency y accuracy, esta pregunta no se puede responder mirando solo quantity o unit_price de la fila misma — necesita una fuente de verdad adicional. Este tipo de verificación, de hecho, es exactamente el tipo de regla cruzada que el módulo 3 de esta guía ("consistency and referential checks") generaliza más allá del caso único de product_id.

Ejercicio 3 — Argumenta por qué separar la lógica de diagnóstico de validate_orders() es la decisión correcta, no un atajo. En 3-4 frases, y usando el ejemplo de validate_referential_integrity() mencionado en la Profundización de esta lección, explica por qué construir funciones nuevas y separadas —en vez de seguir agregando parámetros y if a validate_orders()— es una decisión de diseño sólida, no solo una forma de posponer el trabajo.

Ver solución

Cada verificación que necesita una fuente externa —una tabla del warehouse, una línea base calculada, un archivo de configuración— tiene su propio ciclo de vida: dim_product puede cambiar sin que cambie nada sobre cómo se valida el esquema de una fila, y una línea base de precios puede recalcularse semanalmente sin tocar la lógica de duplicados. Si todas esas dependencias vivieran dentro de una sola función gigante como validate_orders(), cualquier cambio en cualquiera de ellas obligaría a tocar y volver a probar toda la función completa, incluidas las partes que no cambiaron. Mantener cada verificación en su propia función —como hace esta guía en los módulos 2 a 6— permite que cada una evolucione, se pruebe y se reemplace de forma independiente, exactamente el mismo principio de responsabilidad única que ya viste al separar check_schema(), check_nulls_and_types() y check_business_rules() en foundations M5, ahora aplicado a un nivel más amplio.

Resumen y siguiente paso

En esta lección hiciste visible, con un script de diagnóstico separado —nunca integrado a validate_orders()—, exactamente lo que la lección 6 dejó silencioso: ORD-9508 no tiene un product_id real (P099 no está en el catálogo de cuatro productos), y ORD-9509 tiene un precio cincuenta veces mayor al normal de P002 dentro del mismo archivo. Y, más importante que el hallazgo puntual, entendiste por qué —con precisión estructural, no solo como observación— una compuerta que solo mira una fila a la vez nunca podría atrapar estos dos problemas sin acceso a una referencia externa.

Antes de avanzar deberías poder: explicar la diferencia entre "una regla local" y "una regla que necesita referencia externa"; nombrar por qué agregar parámetros a validate_orders() no es la solución correcta; y describir, sin mirar el código de nuevo, qué necesitaría existir —una tabla, una línea base— para que cada una de las dos filas silenciosas se atrapara de forma reutilizable.

Tienes el diagnóstico completo de este módulo: qué atrapa la compuerta vieja, qué se le escapa, y por qué. La lección 8 —el proyecto del módulo— junta todo en un solo script de diagnóstico, con un reporte escrito, cerrando este módulo antes de que el módulo 2 empiece a construir la primera pieza real de la solución: tests de calidad declarativos con Pandera.

Recursos

  • Joe Reis & Matt Housley, Fundamentals of Data Engineering (O'Reilly, 2022) — el capítulo de calidad de datos distingue explícitamente entre verificaciones de esquema/rango y verificaciones de integridad referencial. oreilly.com/library/view/fundamentals-of-data/9781098108298. En inglés.
  • Python — documentación oficial de comprensiones de listas y conjuntos (set), la base del filtrado de product_id huérfanos en esta lección. docs.python.org/3/tutorial/datastructures.html#list-comprehensions. En inglés.
  • Python — documentación oficial de sorted() y el cálculo de mediana usado en el ejemplo trabajado. docs.python.org/3/library/functions.html#sorted. En inglés.
  • DISEÑO de esta guía — la especificación exacta de validate_referential_integrity() (módulo 3) y check_price_baseline() (módulo 5), las soluciones reales que reemplazan el diagnóstico manual de esta lección. src/guides/data-reliability-and-governance-guide/DISENO.md. En español.