Skip to content

Latest commit

 

History

History
148 lines (96 loc) · 14.2 KB

File metadata and controls

148 lines (96 loc) · 14.2 KB

Arquitectura del Data Lake de Licitaciones PLACSP

Una implementación de la Arquitectura Medallion con capa Raw explícita

Este documento explica cómo está organizado el proyecto, qué es la Arquitectura Medallion, por qué la hemos elegido como base estructural, y por qué hemos extendido el modelo estándar de tres capas con una capa Raw independiente.

1. El problema que resuelve una arquitectura de capas

El pipeline de licitaciones PLACSP ingiere datos de cinco conjuntos de datos distintos publicados por el gobierno español en formato XML/ATOM. Estos datos pasan por fases radicalmente distintas: primero se descargan tal cual de la red, luego se extraen de la compleja estructura anidada de los XML y se convierten en tablas relacionales, y finalmente se limpian, enriquecen y organizan para el análisis.

Sin una separación clara de capas, estos tres tipos de trabajo —descarga, extracción y transformación— terminarían mezclados en un mismo lugar. Los datos originales podrían modificarse accidentalmente, no habría forma de reprocesar una fase sin repetir las demás, y la causa de un fallo sería difícil de aislar.

La Arquitectura Medallion es la respuesta estándar de la industria a este problema. Su principio fundamental es simple: cada capa representa un contrato de calidad distinto sobre los datos, y cada contrato justifica una zona de almacenamiento y un conjunto de scripts separados.

2. Qué es la Arquitectura Medallion

La Arquitectura Medallion es un patrón de diseño para Data Lakes introducido y popularizado por Databricks, los creadores de Delta Lake, como parte de su referencia de arquitectura Lakehouse. El nombre hace referencia a las tres capas del modelo clásico, ordenadas como las medallas de un podio:

  • Bronze — datos crudos tal como llegan de la fuente, sin modificar.
  • Silver — datos limpios, tipados y enriquecidos; la fuente de verdad del negocio.
  • Gold — datos agregados y organizados por dominio de negocio; listos para el análisis final.

El criterio que separa una capa de la siguiente no es el nombre ni el color, sino la confianza que se deposita en los datos de esa zona. En Bronze puedes encontrar XML malformado. En Silver no debería haber ni un tipo de dato incorrecto. En Gold no debería haber ni una fila que no tenga sentido de negocio.

Esta separación tiene tres ventajas concretas e inmediatas:

Reprocessabilidad. Si se descubre un bug en la lógica de extracción de la capa Silver, se puede corregir el código y volver a ejecutar la transformación desde los datos Bronze sin necesidad de volver a descargar nada de internet. Los datos originales nunca se tocan.

Trazabilidad. Si un valor en Gold parece incorrecto, se puede rastrear hacia atrás: ¿está mal en Silver? ¿O ya estaba mal en Bronze? ¿O el dato original en el XML era incorrecto? Cada capa responde una pregunta diferente sobre el origen del problema.

Separación de responsabilidades. Los scripts de descarga no necesitan saber nada de Spark. Los scripts de transformación no necesitan saber cómo funciona la API de PLACSP. Cada capa tiene sus propias herramientas, sus propios tests, y sus propios operadores.

3. Por qué la Arquitectura Medallion es la elección correcta para este proyecto

Las fuentes de datos de PLACSP tienen tres características que hacen que la Arquitectura Medallion no sea una preferencia estética sino una necesidad práctica:

La fuente es externa e impredecible. El pipeline no controla la calidad de los datos que publica el gobierno. Un organismo público puede publicar XML malformado, un campo que debería ser una fecha puede contener texto libre, o una licitación puede aparecer sin alguno de sus campos obligatorios. Separar la descarga de la transformación permite diseñar cada fase con el nivel de defensa adecuado sin que una afecte a la otra. El Dead Letter Pattern, documentado en detalle en docs/dead_letter_pattern.md, es posible precisamente porque la capa de extracción tiene acceso a los datos crudos originales.

El volumen histórico es grande y la re-descarga es costosa. El histórico de PLACSP comprende varios años de licitaciones, con cientos de ficheros .atom por dataset y por año. Volver a descargar todo desde cero en caso de un bug en la transformación no es una opción viable. Mantener los datos crudos intactos en la capa de origen garantiza que cualquier corrección puede implementarse reprocesando desde los datos ya descargados.

El pipeline opera de forma autónoma. El script de ingesta incremental (raw/scripts/daily_download.py) está diseñado para ejecutarse diariamente sin supervisión humana. En este contexto, la separación de capas es también una separación de puntos de fallo: un problema en la transformación Bronze no afecta a las descargas Raw del día siguiente, y viceversa.

4. La extensión de cuatro capas: por qué añadimos Raw

El modelo estándar de Medallion define Bronze como la zona de datos crudos sin modificar. Esta definición es correcta en proyectos donde los datos de origen ya llegan en formato tabular (una tabla de base de datos, un CSV, una API que devuelve JSON plano).

En este proyecto, los datos de origen son ficheros XML con estructura profundamente anidada que siguen el estándar CODICE. Transformar esos ficheros en tablas Delta con columnas planas —el proceso que llamamos extracción— es una operación no trivial que implica parsear el XML con spark-xml, resolver el anidamiento de namespaces CODICE, capturar registros malformados mediante el Dead Letter Pattern, y producir DataFrames con un esquema estable. Esta operación es conceptualmente diferente de la transformación que viene después. La extracción responde a la pregunta ¿qué está en el XML?. La transformación responde a ¿qué significa ese dato y cómo debe representarse?. Mezclar ambas en una sola capa Silver crearía una zona que hace demasiadas cosas a la vez.

La solución adoptada es extender el modelo a cuatro capas, separando Raw de Bronze:

  • Raw → Ficheros originales tal cual se descargan de sus fuentes (XML, ATOM, XLSX, .gc). Inmutables. Nunca se modifican. Son la fuente de verdad del dato original.

  • Bronze → Tablas Delta extraídas de los XML sin transformaciones semánticas. Los datos están aplanados y tipados estructuralmente, pero no enriquecidos ni limpios. Son la fuente de verdad de lo que estaba en los ficheros.

  • Silver → Tablas limpias, enriquecidas y organizadas. Tipos de datos correctos, códigos GC traducidos a descripciones legibles, SCD resuelto. Son la fuente de verdad del negocio.

  • Gold → Data Marts organizados por dominio. Datos listos para consulta directa o para alimentar modelos analíticos e IA.

Esta extensión a cuatro capas no es una invención propia. La propia Databricks documenta en sus proyectos de referencia una zona equivalente denominada Landing Zone o Raw Zone, diferenciada de Bronze. Simon Späti y Christoph Böhmwalder en Fundamentals of Data Engineering (O'Reilly, 2022) describen este patrón como la adición de stages con distintos niveles de confianza. Piethein Strengholt en Data Management at Scale (O'Reilly, 2020) lo documenta en contextos Azure con la misma separación entre zona Raw inmutable y Bronze como primera transformación. El criterio es siempre el mismo: una capa merece su propio nivel si tiene un contrato de calidad diferente al de la capa anterior.

5. Estructura actual del proyecto

El proyecto sigue esta organización de capas:

biddings/
│
├── raw/                            ← Capa Raw: datos originales inmutables
│   ├── data/
│   │   ├── {year}/{dataset}/       # Histórico particionado por año y dataset
│   │   ├── daily/{dataset}/        # Descargas incrementales diarias
│   │   ├── gc_codes/               # Archivos .gc descargados del repositorio CODICE
│   │   └── OrganosContratacion.xlsx  # Directorio oficial de órganos de contratación
│   ├── scripts/
│   │   ├── download_placsp.sh      # Descarga masiva del histórico (Bash)
│   │   ├── daily_download.py       # Descarga incremental diaria con watermarking (Python/lxml)
│   │   ├── download_parties.sh     # Descarga del directorio de órganos (Bash)
│   │   └── download_gc_codes.py    # Descarga de archivos .gc del repositorio CODICE (Python)
│   └── notebooks/
│
├── bronze/                         ← Capa Bronze: extracción de XML a Delta
│   ├── data/
│   │   ├── {year}/{dataset}/       # Tablas Delta por año y dataset
│   │   │   ├── entries/            # Licitaciones activas y eliminadas
│   │   │   ├── lots/               # Lotes
│   │   │   ├── documents/          # Documentos vinculados
│   │   │   └── results/            # Resultados de licitaciones
│   │   ├── parties/                # Dimensión de órganos de contratación
│   │   ├── gc_codes.parquet        # Catálogo de Genericodes extraído de raw/data/gc_codes/
│   │   ├── checkpoints/            # Checkpoints de Structured Streaming
│   │   └── dead_letters/           # Registros XML que no pudieron parsearse
│   ├── src/                        # Módulos PySpark: esquemas, extracción, Dead Letter
│   ├── scripts/
│   │   ├── extract_biddings.py     # Extracción histórica por año (batch)
│   │   ├── daily_extraction.py     # Extracción incremental (Structured Streaming)
│   │   ├── extract_parties.py      # Actualización de la dimensión de órganos
│   │   └── extract_gc_codes.py     # Extracción del catálogo de Genericodes a Parquet
│   ├── notebooks/
│   └── schema_def.yml              # Definición de tipos y reglas de limpieza
│
├── silver/                         ← Capa Silver: limpieza y enriquecimiento (en desarrollo)
│
├── gold/                           ← Capa Gold: Data Marts por dominio (en desarrollo)
│
└── docs/                           ← Documentación técnica

5.1 Raw: inmutabilidad como invariante

La regla más importante de la capa Raw es que ningún script de otras capas escribe en ella. Es de solo lectura para Bronze, Silver y Gold. Cualquier reprocesamiento parte de Raw sin modificarla.

Los scripts de Raw son deliberadamente simples: Bash para descargas masivas, Python con requests para el scraping del repositorio CODICE, y Python con lxml y DuckDB para la ingesta incremental diaria. No usan Spark porque no lo necesitan: su única responsabilidad es trasladar datos de internet al disco local conservando el byte original.

Esta capa gestiona cuatro tipos de datos de origen: los ficheros ATOM/XML de licitaciones, el Excel de Órganos de Contratación, y los archivos .gc del repositorio CODICE. Cada uno tiene su propio script de descarga y su propia subcarpeta dentro de raw/data/.

El único procesamiento permitido en Raw es el renombrado de ficheros para garantizar inmutabilidad: el fichero de entrada estático de PLACSP (que el servidor sobreescribe diariamente con el mismo nombre) recibe un sufijo de timestamp en el momento de la descarga, preservando todas las versiones históricas.

5.2 Bronze: extracción sin semántica

La capa Bronze aplica la extracción estructural: convierte los ficheros XML en tablas Delta planas y normalizadas. Los datos en Bronze están tipados estructuralmente (las columnas tienen tipos de Spark), pero no tienen semántica de negocio aplicada: los códigos GC siguen siendo URIs técnicas sin traducir; las fechas pueden necesitar normalización; los campos de texto pueden contener valores inconsistentes entre organismos.

La característica más importante de Bronze es que es la única capa donde se procesan los datos de origen directamente: los ficheros XML de licitaciones mediante spark-xml (esquemas bidding_schema y deleted_schema, modo PERMISSIVE, Dead Letter Pattern), y los archivos .gc del repositorio CODICE mediante lxml (script extract_gc_codes.py). Esta concentración garantiza que toda la complejidad del parseo queda contenida en un solo lugar.

Bronze también genera las claves subrogadas estructurales (sk_id) mediante MD5(id || updated). Su propósito en esta capa es estrictamente estructural: permiten vincular mediante clave foránea las tablas de entidades derivadas (lots, documents, results) con la tabla principal (entries), haciendo posible almacenarlas en tablas Delta separadas desde el momento de la extracción. Sin esta clave, la única alternativa sería mantener lotes y documentos como columnas anidadas dentro de entries, trasladando la complejidad de normalización a Silver.

El watermark para la ingesta incremental se calcula sobre Bronze: daily_download.py lee la tabla bronze/data/{year}/{dataset}/entries para determinar el punto hasta el que los datos han sido extraídos, y solo descarga lo que es más reciente que ese watermark.

5.3 Silver y Gold: en desarrollo

La capa Silver aplicará las transformaciones semánticas: casteo de tipos basado en schema_def.yml, traducción de códigos GC mediante Broadcast Joins, y enriquecimiento con la dimensión de órganos de contratación. Para la resolución de SCD Tipo 2, Silver hereda la sk_id generada en Bronze — no la recalcula — y la utiliza para implementar el seguimiento histórico de versiones de cada expediente.

La capa Gold producirá Data Marts específicos por dominio de negocio, comenzando por el sector de mobiliario de oficina.

6. Referencias

  • Databricks. Medallion Architecture. Databricks documentation.
  • Reis, J. & Housley, M. (2022). Fundamentals of Data Engineering. O'Reilly Media.
  • Strengholt, P. (2020). Data Management at Scale. O'Reilly Media.
  • docs/dead_letter_pattern.md — Implementación del Dead Letter Pattern en la capa Bronze.
  • docs/genericodes.md — Estrategia de enriquecimiento semántico con catálogos CODICE.
  • docs/surrogate_keys.md — Claves subrogadas MD5 para SCD Tipo 2.
  • docs/tables/diagram.md — Diagrama entidad-relación del modelo de datos Bronze.