Módulo 1: When Green Does Not Mean Correct

Conoce a S04: la cuarta tienda de Kiosko

Descripción

Kiosko abre su cuarta tienda: S04 Kiosko Reforma, en Ciudad de México. Pero S04 no es un nombre nuevo para quien ya recorrió este ecosistema — ya tuvo un primer contacto con el pipeline de Kiosko, en data-engineering-foundations-guide (módulo 7), y ese primer contacto terminó mal, de una forma muy específica y muy bien documentada. Esta lección cuenta esa historia con precisión —citando el código exacto, el mensaje de error exacto, el resultado exacto— y después presenta el estado actual de S04: ya está en el catálogo de tiendas, ya puede vender sin que el pipeline se caiga, y está a punto de mandar su primer archivo real de ventas como tienda reconocida.

Conexión con el módulo. Esta lección conecta el vocabulario y la clasificación de las lecciones 2 a 4 con el caso real que las lecciones 6, 7 y 8 van a diagnosticar. Todo lo que sigue en este módulo —y en gran parte del resto de esta guía— gira alrededor de S04 y de su primer archivo, orders_2026-08-14.csv.

El antecedente, citado con precisión: el rechazo de S04 en foundations M7

data-engineering-foundations-guide, módulo 7 (lección 6, "Manejar el fallo de un paso sin perder la corrida"), le agregó a Kiosko un octavo día de datos, guardado como orders_2026-08-11.csv:

order_id,store_id,product_id,quantity,unit_price,order_ts
ORD-8101,S01,P001,2,0.55,2026-08-11T08:15:00
ORD-8102,S04,P002,1,1.20,2026-08-11T08:40:00

Mira con cuidado la segunda fila: store_id es S04. En ese momento del ecosistema, DIM_STORE —el catálogo de tiendas de Kiosko— solo conocía a S01, S02 y S03. S04 no existía todavía en ningún lado. Y aquí está el detalle exacto que hace este antecedente relevante para toda esta guía: validate_orders() no rechazó esa fila. S04 es una cadena de texto no vacía, quantity=1 es positivo, unit_price=1.20 no es negativo, order_ts tiene formato ISO válido — la fila pasa las cuatro verificaciones de la compuerta local sin ningún problema, exactamente como predijo la clasificación de la lección 4 de este módulo: validate_orders() nunca preguntó si S04 existía en ningún catálogo de tiendas.

El fallo ocurrió un paso después, en la transformación: transform_fact_orders(), construida en el módulo 4 de foundations, tenía una verificación explícita —if order.store_id not in store_index: raise ValueError(f"unknown store_id: {store_id}")— y esa verificación sí lanzó una excepción real: ValueError: unknown store_id: S04. run_pipeline(), diseñada en esa misma lección para no perder la corrida completa ante un fallo, capturó la excepción y devolvió:

PipelineResult(partition_date='2026-08-11', status='failed', rows_extracted=2, rows_valid=2, rows_rejected=0, rows_loaded=0, partition_path='', failed_step='transform')

Lee ese resultado con cuidado, porque cada campo cuenta la historia completa. rows_extracted=2: las dos filas se leyeron del CSV sin problema. rows_valid=2: las dos pasaron validate_orders() — ninguna se rechazó ahí. rows_loaded=0 y partition_path='': nada se escribió al almacén, porque transform_fact_orders() falló a mitad de su propio bucle, sin devolver ninguna lista parcial. failed_step='transform': el punto exacto de la falla, registrado con precisión por el manejo de errores que esa misma lección construyó. El resultado neto: foundations rechazó el archivo entero, no una fila individual — S01, la tienda que sí existía, tampoco llegó al almacén ese día, porque la garantía de atomicidad de la transformación (todo o nada) se aplicó a la corrida completa, no fila por fila.

Una analogía: el empleado nuevo sin gafete todavía

Piensa en un empleado que ya empezó a trabajar —ya tiene escritorio, ya tiene tareas asignadas, ya está produciendo— pero cuyo gafete de acceso todavía no está en el sistema de seguridad del edificio. El guardia de la entrada, cada mañana, no lo deja pasar: su nombre no aparece en la lista, sin importar cuánto trabajo real ya esté haciendo puertas adentro. El problema no es que el empleado sea ilegítimo — es que el sistema de registro (la lista del guardia) todavía no se sincronizó con la realidad (el empleado ya trabaja ahí).

Eso es, con precisión, lo que le pasó a S04 en foundations M7: la tienda ya estaba vendiendo —el order_ts de ORD-8102 es real, la venta ocurrió— pero el catálogo de tiendas de Kiosko (DIM_STORE, el equivalente a la lista del guardia) todavía no la conocía. El pipeline, correctamente, se negó a procesar esa fila como si fuera de una tienda válida, porque no tenía ninguna forma de confirmar que S04 era legítima. Esta guía completa —empezando por esta lección— es la historia de cómo Kiosko corrige esa desincronización, y de qué falta todavía después de corregirla apenas a medias.

S04, dado de alta: los hechos actuales

Después del fallo de foundations M7, alguien en el equipo de datos de Kiosko hizo lo mínimo necesario para que el pipeline dejara de caerse: agregó una fila a DIM_STORE a mano, sin ningún proceso formal detrás. Estos son los datos completos de esa tienda, tal como quedaron registrados:

CampoValor
store_idS04
store_nameKiosko Reforma
cityCiudad de México
countryMexico

El campo country sigue la misma regla determinista que ya estableció lakehouse-and-iceberg-guide (módulo 4) para las tres tiendas originales —Bogotá→Colombia, Lima→Peru, Santiago→Chile—, ahora extendida a la cuarta: Ciudad de México→Mexico. No es un dato inventado para esta lección — es la misma función determinista, aplicada a un caso nuevo.

Con esto, S04 ya existe en DIM_STORE. El ValueError de foundations M7 no volvería a ocurrir si se corriera de nuevo hoy: transform_fact_orders() encontraría S04 en store_index sin problema. Pero fíjate en lo que ese arreglo no hizo: nadie escribió un documento que diga qué se espera de los datos que S04 va a mandar —qué SLA de llegada tiene, qué rango de filas por día es razonable, qué pasa si un archivo suyo viene con problemas—. Simplemente se agregó una fila a una tabla, a mano, sin ningún contrato detrás. El módulo 4 de esta guía es, precisamente, donde ese vacío se cierra con un artefacto versionado real. Por ahora, S04 está "dada de alta" en el sentido más mínimo posible: existe, y ya no rompe nada por sí sola.

El primer archivo real: orders_2026-08-14.csv, y por qué ya llega tarde

Cinco días después de que cierra la semana canónica de Kiosko (2026-08-03 al 2026-08-09), y un día antes del cambio de precio de P002 del 2026-08-15 que ya resolvieron data-modeling-for-analytics-guide, dbt-analytics-engineering-guide y lakehouse-and-iceberg-guide, S04 manda su primer archivo real de ventas como tienda ya reconocida: orders_2026-08-14.csv. Las lecciones 6 y 7 de este módulo lo abren y lo corren de verdad — por ahora, hay un problema que se puede confirmar sin abrir una sola fila del archivo: cuándo llega, comparado con cuándo se revisa.

El acuerdo informal con S04 —todavía no un contrato formal, eso es el módulo 4— es que un archivo de ventas debe estar disponible para revisión dentro de las 24 horas siguientes al día que factura. S04 vendió el 2026-08-14; el archivo debería estar listo para revisión, a más tardar, el 2026-08-15. El aparato de calidad de esta guía —el que vas a correr de verdad en la lección 6— no se ejecuta el mismo día que llega el archivo. Se ejecuta el 2026-08-16 a las 09:00, la constante fija que vas a usar en toda esta guía:

# freshness_preview.py
from datetime import datetime

EXPECTED_ARRIVAL = "2026-08-14T00:00:00"   # el dia en que S04 debia mandar su primer archivo
SLA_DEADLINE = "2026-08-15T00:00:00"       # EXPECTED_ARRIVAL + 24 horas de SLA
PIPELINE_RUN_AT = "2026-08-16T09:00:00"    # el "ahora" fijo de esta guia -- nunca datetime.now()

deadline = datetime.fromisoformat(SLA_DEADLINE)
run_at = datetime.fromisoformat(PIPELINE_RUN_AT)
hours_past_deadline = (run_at - deadline).total_seconds() / 3600

arrival = datetime.fromisoformat(EXPECTED_ARRIVAL)
hours_since_expected = (run_at - arrival).total_seconds() / 3600

print(f"SLA_DEADLINE: {SLA_DEADLINE}")
print(f"PIPELINE_RUN_AT: {PIPELINE_RUN_AT}")
print(f"Horas transcurridas desde que vencio el SLA: {hours_past_deadline}")
print(f"Horas transcurridas desde el dia esperado de llegada: {hours_since_expected}")

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

SLA_DEADLINE: 2026-08-15T00:00:00
PIPELINE_RUN_AT: 2026-08-16T09:00:00
Horas transcurridas desde que vencio el SLA: 33.0
Horas transcurridas desde el dia esperado de llegada: 57.0

Cuando el aparato de calidad por fin mira el archivo de S04, ya pasaron 33 horas de más sobre el plazo de 24 horas pactado, y 57 horas desde que se esperaba la primera entrega. Esta es la sexta dimensión de calidad que la lección 3 definió —freshness—, y es distinta de las otras cinco en algo importante: no depende de ninguna fila específica del archivo. Aunque las doce filas de orders_2026-08-14.csv fueran perfectas —sin ningún campo vacío, sin ningún duplicado, sin ningún precio sospechoso—, el archivo completo seguiría violando el SLA de freshness, solo por el momento en que alguien finalmente lo revisó. Este módulo no construye todavía el check formal de freshness —check_freshness() es del módulo 6—, pero el problema ya es visible, con evidencia, desde antes de abrir una sola fila.

Diagrama: la línea de tiempo completa de S04

flowchart LR
    A["2026-08-11\nfoundations M7:\nORD-8102 -> ValueError\nstatus=failed"] --> B["Alguien agrega S04\na DIM_STORE a mano\n(sin contrato formal)"]
    B --> C["2026-08-14\nS04 vende:\norders_2026-08-14.csv"]
    C --> D["2026-08-15\nvence el SLA\nde 24 horas"]
    D --> E["2026-08-16 09:00\nPIPELINE_RUN_AT:\npor fin se revisa\n(57h despues de vender)"]
    E --> F["Leccion 6:\nvalidate_orders()\ncorrido de verdad"]

Errores comunes

Pensar que agregar S04 a DIM_STORE "ya resolvió" el problema de foundations M7. Qué pasa: alguien, al ver que S04 ya no causaría un ValueError si el pipeline corriera hoy, concluye que el incidente está cerrado. Por qué pasa: el síntoma más visible —la excepción, el status='failed'— efectivamente desaparece con ese arreglo mínimo. Cómo detectarlo: si tu razonamiento se detiene en "ya no crashea", te falta preguntar qué garantías tiene ese arreglo — ¿quién decidió el SLA de 24 horas? ¿está escrito en algún lado que cualquier sistema pueda leer, o solo alguien lo recuerda de memoria? Cómo corregirlo: agregar una fila a una tabla no es lo mismo que tener un contrato — el módulo 4 de esta guía construye ese contrato real, versionado, con SLA y política de violación explícitos, precisamente porque un arreglo a mano como el de esta lección no es sostenible a medida que Kiosko sigue creciendo.

Confundir el rechazo de foundations M7 con una "cuarentena". Qué pasa: alguien describe lo que le pasó a orders_2026-08-11.csv en foundations como si hubiera sido puesto "en cuarentena", separando las filas buenas de las malas. Por qué pasa: cuarentena es un término que ya vas a usar mucho en esta guía (módulo 7), y es fácil aplicarlo retroactivamente a cualquier fallo. Cómo detectarlo: revisa el PipelineResult citado en esta lección — rows_loaded=0. Ninguna fila se cargó, ni siquiera ORD-8101 de S01, que era perfectamente válida. Eso no es cuarentena —que separaría lo bueno de lo malo—, es un rechazo total del archivo: todo o nada, sin distinción entre filas. Cómo corregirlo: usa "rechazo" para lo que pasó en foundations M7 (el archivo completo, incluidas las filas buenas, no se cargó), y reserva "cuarentena" para el mecanismo más fino que el módulo 7 de esta guía construye, que sí separa filas buenas de malas dentro de un mismo archivo.

Saltarse la lección y asumir que ya sabes qué tiene de malo el archivo de S04. Qué pasa: alguien, familiarizado con el patrón de esta guía, asume que puede predecir con exactitud las doce filas de orders_2026-08-14.csv sin haberlas visto, y avanza directo a la lección 7. Por qué pasa: el patrón de "un archivo con filas rotas a propósito" ya es familiar después de cuatro lecciones de este módulo y de toda la experiencia con foundations. Cómo detectarlo: si no puedes nombrar, con precisión, cuántas filas tiene el archivo y cuántas de ellas rompen cada dimensión específica, todavía no tienes el diagnóstico — solo tienes una expectativa razonable. Cómo corregirlo: la lección 6 abre el archivo real, línea por línea, y corre validate_orders() de verdad sobre él — no te saltes esa evidencia, incluso si el patrón general ya te resulta familiar.

Ejercicios

Ejercicio 1 — Recalcula la línea de tiempo con un SLA distinto. Usando el script freshness_preview.py de esta lección, cambia SLA_DEADLINE a "2026-08-14T12:00:00" (un SLA más estricto, de solo 12 horas) y recalcula hours_past_deadline. ¿El archivo sigue violando el SLA?

Ver solución
SLA_DEADLINE_ESTRICTO = "2026-08-14T12:00:00"
deadline_estricto = datetime.fromisoformat(SLA_DEADLINE_ESTRICTO)
print((run_at - deadline_estricto).total_seconds() / 3600)

Salida esperada:

45.0

Sí, sigue violando el SLA —incluso más severamente, 45.0 horas de más en vez de 33.0—. Esto tiene sentido: PIPELINE_RUN_AT (2026-08-16T09:00:00) es una constante fija que no cambia; lo único que cambió es qué tan estricta era la expectativa original. Un SLA más laxo (por ejemplo, 48 horas en vez de 24) sí podría, en teoría, hacer que el archivo pasara la verificación de freshness — vale la pena notar que el SLA mismo es una decisión de negocio, no un hecho técnico fijo.

Ejercicio 2 — Argumenta por qué rows_loaded=0 afectó también a S01. En el PipelineResult citado de foundations M7, la fila de S01 (ORD-8101) era perfectamente válida, y aun así rows_loaded=0 para todo el día 2026-08-11. En 2-3 frases, explica por qué el diseño de run_pipeline() de foundations produce ese resultado, en vez de cargar ORD-8101 y solo rechazar ORD-8102.

Ver solución

transform_fact_orders(), tal como la construyó foundations M4, recorre las filas válidas en un solo bucle y lanza una excepción apenas encuentra un store_id desconocido — la función nunca devuelve una lista parcial de filas ya transformadas antes del punto de falla, porque el raise interrumpe la ejecución completa de la función. Como write_partition() (el paso de carga) nunca llega a ejecutarse sin una lista de fact_rows completa, no hay ninguna forma de que ORD-8101 se cargue por separado, aunque en sí misma fuera perfectamente válida — es la garantía de atomicidad "todo o nada" del patrón overwrite-partition, aplicada aquí a nivel de transformación completa, no solo de escritura final.

Ejercicio 3 — Predice qué dimensión de calidad viola primero el archivo de S04, sin haberlo abierto. Basándote solo en lo que ya sabes de esta lección —sin mirar el archivo real, que abre la lección 6—, ¿cuál de las seis dimensiones de calidad de datos ya sabes, con certeza, que orders_2026-08-14.csv viola, incluso antes de leer una sola fila? Justifica tu respuesta.

Ver solución

Freshness. A diferencia de las otras cinco dimensiones —que dependen del contenido específico de cada fila, algo que todavía no viste—, freshness es una propiedad del archivo completo relacionada con cuándo se revisa, no con qué contiene. El cálculo de freshness_preview.py en esta misma lección ya lo confirma con evidencia: 57.0 horas desde la llegada esperada, 33.0 horas por encima del SLA de 24 horas — un hecho ya establecido, sin necesidad de abrir el archivo ni una sola vez. Las otras cinco dimensiones (completeness, uniqueness, validity, consistency, accuracy) sí necesitan mirar filas concretas, y esas la lección 6 las revela recién al correr validate_orders() de verdad.

Resumen y siguiente paso

En esta lección conociste a S04 Kiosko Reforma, la cuarta tienda de Kiosko, y su historia completa dentro del ecosistema: el rechazo total de orders_2026-08-11.csv en foundations M7 (ValueError: unknown store_id: S04, status='failed', rows_loaded=0), el arreglo mínimo que la agregó a DIM_STORE a mano, y la violación de freshness que ya se puede confirmar, con evidencia, antes de abrir el primer archivo real que manda como tienda reconocida.

Antes de avanzar deberías poder: contar la historia de S04 en foundations M7 citando el mensaje de error exacto; explicar la diferencia entre "agregar una fila a una tabla" y "tener un contrato real"; y calcular, como hizo el ejemplo trabajado, cuántas horas de retraso tiene el archivo de S04 respecto al SLA informal.

Tienes el contexto completo. La lección 6 abre, por fin, orders_2026-08-14.csv — las doce líneas exactas, y corre validate_orders() de foundations, sin ningún cambio, para ver de verdad qué atrapa.

Recursos

  • data-engineering-foundations-guide, módulo 7, lección 6 ("Manejar el fallo de un paso sin perder la corrida") — fuente literal del rechazo de S04 citado en esta lección. src/guides/data-engineering-foundations-guide/workbook/module-07-partitioning-and-orchestration/es/06-handling-a-failed-step-without-losing-the-run.md. En español.
  • DISEÑO de lakehouse-and-iceberg-guide — fuente de la función determinista country derivada de city (Bogotá→Colombia, Lima→Peru, Santiago→Chile), extendida aquí a Ciudad de México→Mexico. src/guides/lakehouse-and-iceberg-guide/DISENO.md. En español.
  • Python — documentación oficial de datetime y la aritmética de timedelta, la base del cálculo de horas de esta lección. docs.python.org/3/library/datetime.html. En inglés.
  • DISEÑO de esta guía — la línea de tiempo exacta del incidente de S04, incluida la estructura de doce líneas que la lección 6 abre. src/guides/data-reliability-and-governance-guide/DISENO.md. En español.