Módulo 5: Accuracy And Deterministic Anomaly Detection

Accuracy: la dimensión más difícil de testear

Descripción

Las cinco dimensiones anteriores de esta guía —completeness, uniqueness, validity, consistency, freshness (anticipada, no construida todavía)— comparten algo que esta lección hace explícito por primera vez: cada una tiene una respuesta objetiva, que no depende de ningún juicio de negocio para decidirse. Un campo está vacío o no lo está. Un order_id se repite o no. Accuracy es distinta, y esta lección explica con precisión por qué: necesita dos decisiones humanas —¿cuál es el valor "normal"? ¿cuánta desviación es aceptable?— antes de que una sola línea de código pueda ejecutarse.

Conexión con el módulo. Esta lección retoma directamente el ejercicio 3 de la lección 3 del módulo 1 —donde ya se predijo, sin evidencia todavía, que accuracy sería la más difícil— y la confirma con el peso completo de la evidencia acumulada en los módulos 1 a 4. La lección 3 de este módulo profundiza en el caso concreto de ORD-9509; esta lección se queda en el nivel conceptual, comparando las seis dimensiones entre sí.

Una analogía: el cheque, otra vez, pero mirando de cerca qué preguntó cada verificación

La lección 1 de este módulo presentó el cheque perfectamente lleno, por el monto equivocado. Vale la pena volver a esa misma escena, pero mirando de cerca la lista completa de verificaciones del cajero, una por una, para notar algo que la primera lectura no hizo explícito: cada ítem de esa lista —¿está firmado?, ¿la fecha es válida?, ¿el monto en números coincide con el monto en letras?— tiene una respuesta que el cajero puede confirmar sin salir de su ventanilla. No necesita llamar a nadie, no necesita consultar ningún registro externo, no necesita ejercer ningún juicio sobre si el monto "tiene sentido" para esa transacción. Son verificaciones de forma, autocontenidas.

Ahora imagina que, además, el banco quisiera que el cajero verificara si el monto del cheque "tiene sentido" para el cliente que lo emite — ¿es razonable que esta persona esté pagando esta cantidad? Esa pregunta ya no se responde mirando el cheque. Necesita el historial de transacciones de esa cuenta, algún criterio de "cuánto es razonable" que alguien del banco tuvo que decidir de antemano, y una tolerancia explícita —¿un pago 20% mayor al promedio histórico ya es sospechoso, o hace falta 200%?—. Accuracy es exactamente esa segunda pregunta, aplicada a los datos de Kiosko: no "¿el precio tiene la forma correcta?", sino "¿el precio tiene sentido, comparado con lo que ese producto normalmente vale?".

Ejemplo trabajado: las seis dimensiones, comparadas por lo que necesitan para decidirse

El módulo 1, lección 3, ya construyó una función de verificación distinta para cada una de las seis dimensiones. Vale la pena correrlas de nuevo, esta vez fijándote en un detalle nuevo: cuántos argumentos externos —fuera de la fila misma— necesita cada una.

# what_each_check_needs.py
from datetime import datetime

KNOWN_PRODUCT_IDS = {"P001", "P002", "P003", "P004"}
PIPELINE_RUN_AT = "2026-08-16T09:00:00"  # el "ahora" fijo de esta guia -- nunca datetime.now()


def check_completeness(row: dict) -> bool:
    """Solo necesita la fila misma."""
    return row["unit_price"] == ""


def check_uniqueness(order_id: str, seen: set[str]) -> bool:
    """Necesita el conjunto de IDs ya vistos EN ESTE LOTE -- pero ningun dato externo al lote."""
    return order_id in seen


def check_validity(row: dict) -> bool:
    """Solo necesita la fila misma, y una regla de rango fija de antemano (quantity > 0)."""
    return row["quantity"] <= 0


def check_consistency(row: dict, known_ids: set[str]) -> bool:
    """Necesita OTRA TABLA (el catalogo de productos), pero la pregunta es binaria: existe o no."""
    return row["product_id"] not in known_ids


def check_freshness(row: dict, run_at: str, sla_hours: int) -> bool:
    """Necesita un reloj fijo y un SLA -- pero el SLA es una regla de negocio ya decidida, no un rango."""
    hours_elapsed = (datetime.fromisoformat(run_at) - datetime.fromisoformat(row["order_ts"])).total_seconds() / 3600
    return hours_elapsed > sla_hours


def check_accuracy(row: dict, reference: float, tolerance: float) -> bool:
    """Necesita DOS decisiones de negocio: cual es el valor normal (reference),
    y cuanta desviacion es aceptable (tolerance). Ninguna de las dos viene de la fila."""
    return abs(row["unit_price"] - reference) / reference > tolerance


checks_needs = {
    "completeness": ["la fila"],
    "uniqueness": ["la fila", "IDs ya vistos (mismo lote)"],
    "validity": ["la fila", "un rango fijo de antemano"],
    "consistency": ["la fila", "otra tabla (catalogo)"],
    "freshness": ["la fila (o el archivo)", "un reloj fijo", "un SLA ya decidido"],
    "accuracy": ["la fila", "una linea base (reference)", "una tolerancia (tolerance)"],
}

print(f"{'dimension':<14}{'que necesita, ademas de la fila'}")
for dim, needs in checks_needs.items():
    print(f"{dim:<14}{needs}")

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

dimension     que necesita, ademas de la fila
completeness  ['la fila']
uniqueness    ['la fila', 'IDs ya vistos (mismo lote)']
validity      ['la fila', 'un rango fijo de antemano']
consistency   ['la fila', 'otra tabla (catalogo)']
freshness     ['la fila (o el archivo)', 'un reloj fijo', 'un SLA ya decidido']
accuracy      ['la fila', 'una linea base (reference)', 'una tolerancia (tolerance)']

Mira la lista de accuracy con cuidado, comparándola con las otras cinco. consistency necesita otra tabla, es cierto — pero la pregunta que responde con esa tabla es binaria y objetiva: product_id existe en dim_product, o no existe. No hay ningún juicio de negocio involucrado en decidir "qué cuenta como existir" — o está la fila en la tabla, o no está. freshness necesita un SLA, pero ese SLA —24 horas, en el caso de S04— es una única decisión, tomada una vez, que después se aplica de la misma forma a cualquier archivo. accuracy es la única fila de esta tabla con dos decisiones de negocio distintas, y ninguna de las dos es binaria: reference (¿1.20 es el precio correcto, o debería ser el precio de la semana pasada, o el precio promedio de todas las tiendas?) y tolerance (¿12.5% de desviación ya es sospechoso, o hace falta 50%?) son, ambas, números que alguien tiene que elegir con criterio, no verificaciones que se puedan derivar mecánicamente de la estructura de los datos.

Diagrama: el espectro de "cuánto juicio de negocio necesita cada dimensión"

flowchart LR
    subgraph OBJETIVO["Objetivo -- una sola verificacion mecanica"]
        A["Completeness\nvacio o no"]
        B["Validity\ndentro del rango o no"]
    end

    subgraph UNA_DECISION["Una decision de negocio, tomada una vez"]
        C["Uniqueness\nque campo es la llave"]
        D["Consistency\nque tabla es la fuente de verdad"]
        E["Freshness\ncual es el SLA"]
    end

    subgraph DOS_DECISIONES["Dos decisiones de negocio, entrelazadas"]
        F["Accuracy\ncual es el valor normal\nY cuanta desviacion es aceptable"]
    end

    OBJETIVO --> UNA_DECISION --> DOS_DECISIONES

El diagrama ordena las seis dimensiones en un espectro, no en una lista plana: de izquierda a derecha, cada dimensión necesita progresivamente más juicio humano antes de poder ejecutarse. Accuracy no solo está más a la derecha — está sola en su categoría, porque las otras cinco necesitan, cuando mucho, una sola decisión (qué SLA, qué tabla es la fuente de verdad), mientras que accuracy necesita dos decisiones que además interactúan entre sí: cambiar la línea base cambia qué desviaciones parecen razonables, y cambiar la tolerancia cambia cuánta imprecisión en la línea base se puede tolerar sin generar ruido.

Profundización: por qué "más difícil" no significa "imposible" ni "subjetivo sin remedio"

Vale la pena cortar de raíz una conclusión equivocada que esta lección podría sugerir sin querer: que accuracy, por necesitar juicio humano, es una dimensión "subjetiva" que no se puede automatizar de forma confiable. Eso es exactamente lo opuesto de lo que construye el resto de este módulo. La diferencia real no es "objetivo contra subjetivo" — es dónde vive la decisión. En completeness o validity, la decisión (¿qué cuenta como vacío? ¿cuál es el rango permitido?) es tan simple que casi desaparece dentro del código mismo: row["unit_price"] == "", quantity > 0. En accuracy, la decisión es más grande y más visible —una línea base calculada, un umbral de tolerancia explícito—, pero sigue siendo, una vez tomada, una regla completamente mecánica y reproducible. check_price_baseline(), que este módulo construye a partir de la lección 4, no tiene absolutamente nada de subjetivo en su ejecución — dados los mismos reference_prices y el mismo tolerance, produce siempre exactamente el mismo resultado, sobre los mismos datos. Lo que cambia, comparado con check_validity(), es que las dos decisiones que la alimentan —la línea base, la tolerancia— quedan explícitas y separadas del código, en vez de escondidas dentro de una condición trivial. Esa visibilidad es, de hecho, una ventaja: cualquiera puede leer reference_prices = {"P002": 1.20, ...} y preguntar si ese número sigue siendo correcto, algo que no se puede hacer con la misma facilidad con un if quantity <= 0 enterrado dentro de una función.

Errores comunes

Concluir que, si accuracy necesita "juicio de negocio", entonces no se puede automatizar. Qué pasa: alguien, después de leer que accuracy necesita dos decisiones humanas, asume que el check de accuracy tiene que ser una revisión manual, fila por fila, hecha por una persona. Por qué pasa: "juicio de negocio" suena, a primera escucha, a lo opuesto de "automatización". Cómo detectarlo: si tu plan para accuracy involucra que alguien revise cada fila a mano, perdiste el punto central de la profundización de esta lección. Cómo corregirlo: el juicio de negocio se ejerce una sola vez, al decidir reference_prices y tolerance — después de eso, check_price_baseline() corre de forma completamente mecánica y determinista sobre cualquier cantidad de filas, sin ninguna intervención humana adicional. Es exactamente el mismo patrón que ya usó freshness: el SLA de 24 horas se decidió una vez, y después check_freshness() (módulo 6) lo aplica sin ningún juicio adicional.

Pensar que la dificultad de accuracy es un defecto de esta guía, algo que "debería" resolverse con una herramienta más sofisticada. Qué pasa: alguien, frustrado porque accuracy necesita más trabajo de configuración que las otras cinco dimensiones, busca una librería que "simplemente detecte" precios anómalos sin que nadie tenga que declarar una línea base. Por qué pasa: después de instalar Pandera con un solo pip install en el módulo 2, esperar que accuracy sea igual de directo es una expectativa razonable, aunque equivocada. Cómo detectarlo: si tu búsqueda es "detectar automáticamente precios incorrectos sin configuración", ya estás buscando, sin saberlo, exactamente el tipo de modelo de Machine Learning que la lección 1 de este módulo declaró fuera de scope. Cómo corregirlo: acepta la naturaleza de la dimensión — accuracy siempre necesita una línea base declarada por alguien, humano o modelo. Esta guía elige que sea un humano, con una fórmula transparente, precisamente para mantener cada decisión explicable en una frase.

Confundir "accuracy es difícil de testear" con "accuracy es la dimensión más importante". Qué pasa: alguien, impresionado por la atención que recibe accuracy en este módulo, concluye que es "la dimensión que más importa" de las seis, por encima de completeness o validity. Por qué pasa: dedicarle un módulo completo se siente como una señal de importancia relativa. Cómo detectarlo: pregúntate qué pasaría si Kiosko no tuviera ningún check de completeness — un archivo con la mitad de los precios vacíos rompería el pipeline de forma mucho más inmediata y visible que un puñado de precios ligeramente anómalos. Cómo corregirlo: "difícil de testear" y "más importante" son ejes distintos. Completeness y validity siguen siendo, en la práctica, las primeras líneas de defensa de cualquier sistema de calidad de datos — accuracy es difícil precisamente porque los problemas que atrapa son más sutiles, no porque sean más graves que los de otras dimensiones. Las seis dimensiones son complementarias, no una jerarquía de importancia.

Ejercicios

Ejercicio 1 — Clasifica tres reglas nuevas según cuántas decisiones de negocio necesitan. Para cada una de estas tres reglas hipotéticas de Kiosko, indica si necesita cero, una, o dos decisiones de negocio antes de poder ejecutarse (siguiendo el mismo criterio del ejemplo trabajado de esta lección): (a) quantity no puede ser mayor a 100 unidades en una sola orden; (b) el store_id debe existir en dim_store; (c) el revenue diario de una tienda no debería desviarse más de 40% del promedio de los últimos 7 días.

Ver solución

(a) Una decisión — el límite de 100 es una decisión de negocio (¿por qué 100 y no 200?), pero una vez tomada, la verificación es tan mecánica como validity: quantity <= 100, sin ninguna referencia externa. (b) Una decisión — similar a consistency: la tabla de referencia (dim_store) ya existe, la pregunta es binaria (existe o no), aunque decidir que esa tabla es la fuente de verdad correcta sí fue, en algún momento, una decisión. (c) Dos decisiones — exactamente el patrón de accuracy: necesita una línea base (el promedio de los últimos 7 días, que además es una línea base móvil, no fija como reference_prices) y una tolerancia (40%). Esta regla, de hecho, es una variante más avanzada de accuracy a nivel de tabla en vez de a nivel de fila — el mismo principio, aplicado a un agregado en vez de a un valor individual.

Ejercicio 2 — Reproduce what_each_check_needs.py, y agrega una séptima fila hipotética. Kiosko decide agregar una nueva regla: "cada orden debe pertenecer a una tienda que esté abierta en el horario de order_ts" (la misma idea que ya se sugirió como ejercicio en el módulo 1, lección 7). Agrégala al diccionario checks_needs del ejemplo trabajado, decidiendo tú mismo cuántas y cuáles dependencias necesita.

Ver solución
checks_needs["store_hours"] = ["la fila", "tabla de horarios por tienda (nueva, no existe en Kiosko todavia)"]

Esta regla se parece más a consistency que a accuracy: la pregunta ("¿la hora está dentro del horario declarado?") es binaria una vez que existe la tabla de horarios, sin ninguna tolerancia ni línea base numérica de por medio. El ejercicio confirma que no toda regla que "necesita algo externo" es automáticamente tan compleja como accuracy — la clasificación de esta lección depende de cuántas decisiones de negocio hacen falta, no solo de si hace falta alguna.

Ejercicio 3 — Argumenta, con tus propias palabras, por qué tolerance es la pieza más delicada de check_accuracy(), más incluso que reference. En 2-3 frases, usando el ejemplo de 1.35 contra 2.00 que ya trabajó el módulo 1, lección 3 (ejercicio 2), explica por qué elegir mal la tolerancia puede ser más costoso, en la práctica, que elegir mal la línea base.

Ver solución

Una línea base ligeramente equivocada (por ejemplo, 1.15 en vez de 1.20) sigue produciendo resultados razonables mientras la tolerancia sea generosa — un pequeño error en el punto de referencia no cambia mucho el resultado final. Una tolerancia mal calibrada, en cambio, tiene un efecto binario y mucho más disruptivo: demasiado estricta (por ejemplo, 5%), y cada pequeña variación legítima de precio —una promoción, un ajuste de centavos— genera una alerta falsa, entrenando al equipo a ignorar las alertas por fatiga; demasiado laxa (por ejemplo, 200%), y errores reales como el de ORD-9509 (una desviación de 4900%) podrían, en teoría, seguir sin detectarse si la tolerancia se configura sin cuidado. La lección 7 de este módulo profundiza exactamente en este problema, con evidencia ejecutada de ambos extremos.

Resumen y siguiente paso

En esta lección confirmaste, comparando las seis dimensiones una por una, por qué accuracy es la más difícil de testear: no porque sea "subjetiva" o imposible de automatizar, sino porque necesita dos decisiones de negocio explícitas —una línea base, una tolerancia— antes de que cualquier código pueda ejecutarse, mientras que las otras cinco necesitan, cuando mucho, una sola decisión, casi siempre binaria. También dejaste claro que esa dificultad no la convierte en la dimensión "más importante" de las seis, ni en una excusa para dejarla sin automatizar.

Antes de avanzar deberías poder: nombrar las dos decisiones de negocio que necesita accuracy, y explicar por qué ninguna de las otras cinco dimensiones necesita ambas a la vez; y explicar por qué "necesita juicio de negocio" no es lo mismo que "no se puede automatizar".

La lección 3 se queda con el caso concreto de ORD-9509 y lo diseca por completo: cada verificación que pasa, exactamente por qué, y qué tendría que ser cierto para que una regla de rango pudiera, en teoría, haberla atrapado — la última pieza del diagnóstico antes de empezar a construir la solución en la lección 4.

Recursos

  • Módulo 1, lección 3, de esta misma guía ("Las seis dimensiones de calidad de datos") — fuente de check_accuracy() y de la primera predicción, sin evidencia todavía, de que accuracy sería la dimensión más difícil. src/guides/data-reliability-and-governance-guide/workbook/module-01-when-green-does-not-mean-correct/es/03-six-dimensions-of-data-quality.md. En español.
  • DAMA UK — "The Six Primary Dimensions for Data Quality Assessment" (octubre de 2013) — el marco de la industria citado en la lección 1 de este módulo. dama-uk.org/resources/the-six-primary-dimensions-for-data-quality-assessment. En inglés.
  • Python — documentación oficial de datetime y timedelta, reutilizada sin cambios en el ejemplo trabajado de esta lección. docs.python.org/3/library/datetime.html. En inglés.
  • DISEÑO de esta guía — el mapa completo del módulo 5. src/guides/data-reliability-and-governance-guide/DISENO.md. En español.