Salta el contingut

Contractes de dades: definir què esperes ABANS que arribin

Tot el que s'ha vist fins ara (pytest, validadors Python, Pandera, Great Expectations) comparteix una limitació: valida després que les dades ja han arribat. Si l'equip que produeix les dades canvia el format d'una columna sense avisar, el pipeline consumidor descobreix el problema quan ja ha fallat una validació — normalment amb el pipeline aturat i algú despertant-se de matinada per una alerta.

Un contracte de dades (data contract) inverteix aquesta lògica: és un acord explícit i versionat entre qui produeix una dada i qui la consumeix, definit abans que la dada flueixi, que especifica quina forma tindrà i quines garanties ofereix.

flowchart LR
    P["Equip productor\n(p. ex. equip de\nProducte)"] -->|"publica segons\nel contracte"| C["Contracte de dades\n(schema + SLA + owner)"]
    C -->|"consumeix confiant\nen el contracte"| Co["Equip consumidor\n(p. ex. equip\nd'Analítica)"]
    C -.->|"canvi de versió\nnotificat abans"| P

    style P fill:#1d4ed8,color:#ffffff,stroke:#3b82f6
    style C fill:#7c3aed,color:#ffffff,stroke:#a78bfa
    style Co fill:#166534,color:#ffffff,stroke:#22c55e

Per què un contracte i no només un esquema

Un esquema (com els que hem definit amb Pandera o Great Expectations) descriu la forma de les dades: tipus, nul·labilitat, rangs. Un contracte de dades va més enllà i inclou:

  • El schema: igual que abans (camps, tipus, restriccions).
  • La semàntica: què significa realment cada camp (import és en euros amb IVA inclòs o sense?).
  • Les garanties de servei (SLA): amb quina freqüència s'actualitzen les dades, quina és la finestra d'entrega esperada.
  • El propietari (owner): qui és responsable de mantenir la dada i a qui contactar si hi ha un problema.
  • La política de versionat: com i quan es poden fer canvis, i com es comuniquen als consumidors.

Exemple de contracte de dades (YAML)

# contracte_vendes.yml
nom: vendes
versio: "2.1.0"
propietari: equip-ecommerce@empresa.cat
descripcio: >
  Un registre per cada venda completada a la botiga online.
  Publicat cada 15 minuts a la cua Kafka "vendes.completades".

sla:
  frescor_maxima: "20 minuts"
  disponibilitat: "99.5%"

schema:
  - camp: id_venda
    tipus: integer
    nullable: false
    unic: true
    descripcio: "Identificador intern de la venda, autoincremental."
  - camp: data_venda
    tipus: timestamp
    nullable: false
    descripcio: "Timestamp UTC en què s'ha completat el pagament."
  - camp: import_total
    tipus: decimal(10,2)
    nullable: false
    descripcio: "Import EN EUROS, IVA inclòs."
  - camp: pais_client
    tipus: string
    nullable: false
    valors_acceptats: ["ES", "FR", "DE", "IT", "PT"]

politica_versionat:
  canvis_no_trencadors: "afegir camps opcionals nous no incrementa la versió major"
  canvis_trencadors: "eliminar/renombrar camps, o canviar un tipus, incrementa la versió major i requereix 30 dies de preavís"

Canvis trencadors (breaking) vs no trencadors

Tipus de canvi Trencador? Exemple
Afegir un camp opcional nou No Afegir codi_promocio (nullable)
Afegir un valor nou a una llista tancada Sovint sí, si el consumidor fa un switch/CASE sobre els valors coneguts Afegir "UK" a pais_client
Canviar el tipus d'un camp existent import_total de float a string
Renombrar un camp import_totaltotal_amb_iva
Eliminar un camp Eliminar email_client
Canviar la unitat o semàntica sense canviar el nom Sí (el més perillós, perquè no el detecta cap validador de schema) import_total deixa de ser "amb IVA" i passa a ser "sense IVA"

El darrer cas és el motiu principal pel qual un contracte necessita documentar la semàntica, no només el tipus: cap eina de validació de schema (GE, Pandera) detecta que el significat d'un camp ha canviat si el tipus de dada es manté igual.


Versionat semàntic aplicat a dades

De la mateixa manera que una API REST o una llibreria de software fan servir semantic versioning (MAJOR.MINOR.PATCH), un contracte de dades pot adoptar la mateixa convenció:

  • MAJOR (1.x.x → 2.0.0): canvi trencador. Requereix coordinació i preavís als consumidors.
  • MINOR (1.1.x → 1.2.0): afegir capacitat de forma compatible (nou camp opcional).
  • PATCH (1.1.1 → 1.1.2): correccions que no afecten l'estructura (arreglar un bug de generació que produïa valors nuls inesperats).
flowchart TD
    A["v1.0.0\nschema inicial"] -->|"+ camp opcional"| B["v1.1.0"]
    B -->|"fix bug generació"| C["v1.1.1"]
    C -->|"elimina un camp\n(BREAKING)"| D["v2.0.0"]

    style A fill:#166534,color:#ffffff,stroke:#22c55e
    style B fill:#166534,color:#ffffff,stroke:#22c55e
    style C fill:#166534,color:#ffffff,stroke:#22c55e
    style D fill:#7f1d1d,color:#ffffff,stroke:#ef4444

De la teoria a la pràctica

Un contracte de dades es pot fer complir de dues maneres complementàries:

  1. Validació estàtica: un test (amb pytest, Great Expectations o Pandera) que comprova que el schema real d'un lot de dades coincideix amb el que declara el contracte, executat abans de publicar-lo.
  2. Validació en temps d'execució amb Schema Registry: en sistemes de streaming (Kafka), un Schema Registry rebutja directament qualsevol missatge que no compleixi el schema registrat, abans fins i tot que arribi a cap consumidor.
import yaml
import pandera as pa
from pandera import Column, DataFrameSchema, Check

def carrega_contracte_com_a_esquema_pandera(ruta_yaml: str) -> DataFrameSchema:
    """Tradueix un contracte YAML a un esquema Pandera per validar-lo automàticament."""
    with open(ruta_yaml, encoding="utf-8") as f:
        contracte = yaml.safe_load(f)

    tipus_map = {"integer": int, "decimal(10,2)": float, "string": str, "timestamp": "datetime64[ns]"}
    columnes = {}
    for camp in contracte["schema"]:
        checks = []
        if "valors_acceptats" in camp:
            checks.append(Check.isin(camp["valors_acceptats"]))
        columnes[camp["camp"]] = Column(
            dtype=tipus_map[camp["tipus"]],
            nullable=camp["nullable"],
            unique=camp.get("unic", False),
            checks=checks or None,
        )
    return DataFrameSchema(columns=columnes, coerce=True)


esquema = carrega_contracte_com_a_esquema_pandera("contracte_vendes.yml")
df_validat = esquema.validate(df, lazy=True)

Aquest patró —generar l'esquema de validació a partir del contracte, en lloc d'escriure'l dues vegades per separat— és clau: evita que el contracte (la documentació) i la validació real (el codi) es desincronitzin amb el temps, un problema molt similar al que aborda la skill validar_sincronitzacio d'aquest mateix repositori per a la programació docent.

Recursos de referència

L'especificació oberta datacontract.com (impulsada, entre d'altres, per Andrew Jones) defineix un format YAML estàndard per a contractes de dades, més complet que l'exemple simplificat d'aquesta pàgina, i és el punt de partida recomanat si un equip vol adoptar contractes de dades de forma seriosa.


Mòdul M5074 Sistemes de Big Data | Institut Sa Palomera (Blanes) | Curs CEIABD 2026-2027