Módulo 5: Accuracy And Deterministic Anomaly Detection

Construyendo una línea base de precios desde la semana limpia de Kiosko

Descripción

Esta lección construye, por primera vez en esta guía, reference_prices — el diccionario que responde la primera de las dos preguntas de negocio que necesita accuracy: ¿cuál es el precio normal de cada producto de Kiosko? La respuesta no sale de ningún archivo nuevo ni de ninguna suposición — sale de la semana canónica, las cuarenta filas de ventas que ocho guías anteriores de este ecosistema ya usaron, revisaron, y confirmaron como confiables, del 2026-08-03 al 2026-08-09.

Conexión con el módulo. Esta lección resuelve, con código real, el problema que dejó abierto la lección 3: una línea base necesita vivir fuera del archivo bajo sospecha. Aquí construyes exactamente eso — y la lección 5 recién ahí escribe la función que la usa para comparar.

Una analogía: el precio normal de la leche en tu tienda de siempre

Vas a tu tienda de siempre a comprar leche, y sin pensarlo dos veces sabes, aproximadamente, cuánto debería costar — no porque lo hayas memorizado de un catálogo oficial, sino porque la compraste ahí docenas de veces antes, siempre a un precio parecido. Ese conocimiento acumulado es tu línea base personal: no viene de la compra de hoy —la que estás a punto de hacer, la que todavía no sabes si va a estar bien o mal—, viene de un historial ya confirmado, construido con compras anteriores en las que confías. Si hoy la misma leche cuesta cincuenta veces más, no necesitas ningún catálogo oficial para notarlo — el contraste contra lo que ya sabes que es normal es inmediato y evidente.

reference_prices es exactamente ese conocimiento acumulado, aplicado a Kiosko. No se construye mirando el archivo de S04 que se sospecha problemático —eso sería como decidir el precio normal de la leche mirando el precio de hoy, que es justamente lo que quieres verificar—. Se construye mirando la semana canónica: cuarenta compras ya confirmadas, ya revisadas por ocho guías completas de este ecosistema, el equivalente a "todas las veces anteriores que ya compraste esta leche y sabes que el precio estuvo bien".

Ejemplo trabajado: reference_prices, calculado sobre las 40 filas de la semana canónica

Paso 1 — la semana canónica, cargada en kiosko.duckdb

La semana canónica de Kiosko —lunes 2026-08-03 a domingo 2026-08-09, cuarenta líneas de orden, ya construida en data-engineering-foundations-guide— vive en la tabla orders de kiosko.duckdb, junto a orders_s04 (módulo 2) y dim_product (módulo 3):

# load_canonical_week.py
import duckdb

con = duckdb.connect("kiosko.duckdb")

con.execute("""
    CREATE OR REPLACE TABLE orders AS
    SELECT * FROM read_csv('canonical_week.csv', header=True,
        columns={
            'order_id': 'VARCHAR', 'store_id': 'VARCHAR', 'product_id': 'VARCHAR',
            'quantity': 'BIGINT', 'unit_price': 'DOUBLE', 'order_ts': 'TIMESTAMP'
        })
""")

row_count = con.sql("SELECT COUNT(*) FROM orders").fetchone()[0]
print(f"Filas cargadas en orders (semana canonica S01-S03): {row_count}")

canonical_week.csv es, exactamente, la concatenación de los siete archivos diarios que data-engineering-foundations-guide ya construyó y validó —lunes a domingo, ocho tiendas por día, cuarenta filas en total—, sin ninguna fila de S04 mezclada. CREATE OR REPLACE TABLE, la misma decisión ya explicada en el módulo 2: segura de correr las veces que haga falta.

Qué esperar.

Filas cargadas en orders (semana canonica S01-S03): 40

Paso 2 — cruzar a Polars, y calcular el precio de referencia con group_by().agg()

# build_reference_prices.py
import duckdb
import polars as pl

con = duckdb.connect("kiosko.duckdb")
week_df = con.sql("SELECT * FROM orders").pl()

print(f"week_df.shape: {week_df.shape}")

baseline = (
    week_df.group_by("product_id")
    .agg(pl.col("unit_price").mean().alias("reference_price"))
    .sort("product_id")
)

print("\nbaseline (group_by('product_id').agg(mean)):")
print(baseline)

reference_prices = dict(zip(baseline["product_id"].to_list(), baseline["reference_price"].to_list()))
print(f"\nreference_prices: {reference_prices}")

week_df.group_by("product_id") agrupa las cuarenta filas por producto — la documentación oficial de Polars describe este patrón como group_by + agg en su guía de agregaciones: agrupar, y aplicar una expresión de agregación (pl.col("unit_price").mean()) sobre cada grupo por separado. El resultado, baseline, es un DataFrame de cuatro filas —una por producto—, que después se convierte en el diccionario reference_prices con dict(zip(...)), el mismo patrón que ya usó contract_to_pandera_schema() en el módulo 4 para construir un diccionario a partir de dos columnas paralelas.

Qué esperar.

week_df.shape: (40, 6)

baseline (group_by('product_id').agg(mean)):
shape: (4, 2)
┌────────────┬─────────────────┐
│ product_id ┆ reference_price │
│ ---        ┆ ---             │
│ str        ┆ f64             │
╞════════════╪═════════════════╡
│ P001       ┆ 0.55            │
│ P002       ┆ 1.2             │
│ P003       ┆ 0.75            │
│ P004       ┆ 4.5             │
└────────────┴─────────────────┘

reference_prices: {'P001': 0.55, 'P002': 1.2, 'P003': 0.75, 'P004': 4.5}

Cuatro precios de referencia, uno por producto — y fíjate en un hecho que vale la pena confirmar antes de seguir: cada uno de esos cuatro números es exactamente el precio que ya conocías de las ocho guías anteriores de este ecosistema (P001=0.55, P002=1.20, P003=0.75, P004=4.50). Eso no es una coincidencia ni un error de redondeo — es la confirmación de un hecho real sobre la semana canónica: cada producto se vendió, en las cuarenta filas de esa semana, siempre al mismo precio exacto, sin ninguna variación. Puedes confirmarlo tú mismo con el siguiente paso.

Paso 3 — confirmar, con evidencia, que la semana canónica no tiene ninguna variación de precio

# confirm_zero_variance.py -- continuacion de build_reference_prices.py
variance_check = (
    week_df.group_by("product_id")
    .agg(
        pl.col("unit_price").std().alias("stdev"),
        pl.col("unit_price").n_unique().alias("distinct_prices"),
    )
    .sort("product_id")
)
print(variance_check)

Qué esperar.

shape: (4, 3)
┌────────────┬───────┬─────────────────┐
│ product_id ┆ stdev ┆ distinct_prices │
│ ---        ┆ ---   ┆ ---             │
│ str        ┆ f64   ┆ u32             │
╞════════════╪═══════╪═════════════════╡
│ P001       ┆ 0.0   ┆ 1               │
│ P002       ┆ 0.0   ┆ 1               │
│ P003       ┆ 0.0   ┆ 1               │
│ P004       ┆ 0.0   ┆ 1               │
└────────────┴───────┴─────────────────┘

Desviación estándar 0.0, un solo precio distinto por producto, para las cuatro filas. Esto no es un accidente del diseño de esta guía —es, de hecho, exactamente lo que uno esperaría de una semana canónica ya validada: si esas cuarenta filas tuvieran alguna variación real de precio, reference_prices seguiría siendo válido como un promedio razonable, pero esta confirmación de varianza cero te deja saber, con evidencia, que en este caso puntual AVG(unit_price) y "el precio que aparece en cada fila" son, literalmente, el mismo número.

Diagrama: de dónde sale cada pieza de reference_prices

flowchart LR
    A["canonical_week.csv\n7 archivos, lunes-domingo\n40 filas, S01-S03"] -->|"read_csv() con\ncolumns= explicito"| B["kiosko.duckdb\ntabla orders"]
    B -->|"con.sql('SELECT * FROM orders').pl()"| C["week_df\npolars.DataFrame (40, 6)"]
    C -->|"group_by('product_id')\n.agg(mean())"| D["baseline\n4 filas, un precio por producto"]
    D -->|"dict(zip(...))"| E["reference_prices\n{P001: 0.55, P002: 1.2,\nP003: 0.75, P004: 4.5}"]

Profundización: por qué la fuente es la semana canónica, y no orders_s04 promediada consigo misma

Vale la pena ser explícito sobre una alternativa que alguien podría considerar razonable a primera vista: ¿por qué no calcular reference_prices promediando los propios doce precios de orders_s04, en vez de traer una tabla completamente distinta? La respuesta tiene dos partes. La primera ya la adelantó la lección 3: promediar el archivo bajo sospecha contra sí mismo es circular — si ORD-9509 se incluyera en ese promedio, el precio de referencia de P002 saldría distorsionado ((1.20 + 60.00 + 1.20 + 1.20) / 4 = 15.90, casi trece veces el precio real), y la propia anomalía terminaría contaminando la vara con la que se la mide. La segunda parte es más general: la semana canónica no es solo "otro conjunto de datos" — es, específicamente, el conjunto de datos que ocho guías completas de este ecosistema ya usaron, revisaron y confirmaron como correcto, empezando por data-engineering-foundations-guide. Usarla como fuente de la línea base no es una elección arbitraria de esta lección: es la decisión de construir sobre la única porción de la historia de Kiosko que ya tiene, literalmente, la mayor cantidad de ojos encima confirmando que está bien.

Errores comunes

Calcular AVG(unit_price) directamente en SQL, y sorprenderse con el resultado. Qué pasa: alguien, en vez de traer los datos a Polars con group_by().agg(.mean()), corre SELECT product_id, AVG(unit_price) FROM orders GROUP BY product_id directamente en DuckDB, y al imprimir el resultado ve números con ruido de punto flotante en vez de los valores limpios de esta lección.

print(con.sql("SELECT product_id, AVG(unit_price) AS reference_price FROM orders GROUP BY product_id ORDER BY product_id"))
┌────────────┬────────────────────┐
│ product_id │  reference_price   │
│  varchar   │       double       │
├────────────┼────────────────────┤
│ P001       │ 0.5499999999999999 │
│ P002       │ 1.1999999999999997 │
│ P003       │               0.75 │
│ P004       │                4.5 │
└────────────┴────────────────────┘

Por qué pasa: AVG() en SQL y .mean() en Polars son, en teoría, la misma operación matemática, pero cada motor suma los valores internamente en un orden distinto, y la aritmética de punto flotante (DOUBLE/Float64, el mismo estándar IEEE 754 en ambos casos) no es perfectamente asociativa — sumar dieciséis números de P001 en un orden puede dar un resultado con menos ruido que sumarlos en otro orden, aunque el resultado matemáticamente correcto sea idéntico. Cómo detectarlo: si tus precios de referencia tienen dígitos decimales largos y feos (0.5499999999999999 en vez de 0.55) inmediatamente después de un AVG() de SQL, no es un error en tus datos — es ruido de punto flotante acumulado durante la suma. Cómo corregirlo: esta guía construye reference_prices siempre con week_df.group_by("product_id").agg(pl.col("unit_price").mean()) en Polars —el mismo resultado matemático, con menos ruido visible en este caso concreto—, nunca con AVG() directo en SQL. Y, de forma más general: nunca compares dos valores de punto flotante con == exacto en ningún lenguaje — la lección 5 de este módulo usa siempre una comparación de desviación relativa contra una tolerancia, nunca una igualdad exacta, precisamente para no depender de que el ruido de punto flotante desaparezca por casualidad.

Construir reference_prices sobre orders_s04 en vez de sobre la semana canónica. Qué pasa: alguien, apurado, reutiliza la tabla orders_s04 que ya tiene cargada desde el módulo 2, en vez de cargar canonical_week.csv como una tabla nueva. Por qué pasa: orders_s04 ya está en kiosko.duckdb, así que parece el camino de menor resistencia. Cómo detectarlo: si tu reference_prices para P002 sale distinto de 1.20 —por ejemplo, algo cercano a 15.90, como ya calculó la profundización de esta lección—, ya construiste la línea base sobre el archivo bajo sospecha. Cómo corregirlo: recuerda la analogía de la leche — la línea base viene siempre de compras ya confirmadas, nunca de la compra que se está evaluando en este momento. reference_prices se calcula, siempre, sobre orders (la semana canónica), nunca sobre orders_s04.

Olvidar que reference_prices es un diccionario fijo, no una función que se recalcula cada vez. Qué pasa: alguien escribe código que recalcula reference_prices desde cero cada vez que se necesita comparar un precio, en vez de calcularlo una sola vez y reutilizar el resultado. Por qué pasa: parece más "seguro" recalcular siempre, en vez de confiar en un valor guardado. Cómo detectarlo: si tu código llama a week_df.group_by(...).agg(...) dentro de un bucle que procesa cada fila de S04, estás repitiendo un cálculo costoso e innecesario. Cómo corregirlo: reference_prices se calcula una vez —como hizo esta lección—, y el resultado (un diccionario simple de cuatro pares clave-valor) se reutiliza para comparar cualquier cantidad de filas nuevas, exactamente como va a hacer check_price_baseline() en la lección 5.

Ejercicios

Ejercicio 1 — Reproduce el cálculo completo tú mismo, desde cero. Con canonical_week.csv (las cuarenta filas de la semana canónica, ya conocidas de data-engineering-foundations-guide) en tu carpeta de trabajo, corre load_canonical_week.py y build_reference_prices.py en orden. Confirma que obtienes exactamente {'P001': 0.55, 'P002': 1.2, 'P003': 0.75, 'P004': 4.5}.

Ver solución

Si canonical_week.csv contiene las cuarenta filas exactas de la semana canónica (los siete archivos diarios de data-engineering-foundations-guide, concatenados, sin ninguna fila de S04 mezclada), el resultado debería reproducir exactamente el de esta lección. Si tu resultado difiere, la causa más probable es una fila faltante o duplicada — recuerda que la semana canónica tiene, siempre, exactamente cuarenta filas: revisa primero que week_df.shape reporte (40, 6) antes de sospechar de ningún otro paso.

Ejercicio 2 — Calcula reference_prices filtrando solo los primeros tres días de la semana, y compáralo. Modifica la consulta SQL de load_canonical_week.py para incluir solo los archivos de lunes a miércoles (2026-08-03 a 2026-08-05, dieciséis filas en total según el módulo 2 de data-engineering-foundations-guide), y recalcula reference_prices solo sobre esas filas. ¿Cambia el resultado?

Ver solución
partial_week_df = con.sql("SELECT * FROM orders WHERE order_ts < '2026-08-06'").pl()
print(f"partial_week_df.shape: {partial_week_df.shape}")

partial_baseline = (
    partial_week_df.group_by("product_id")
    .agg(pl.col("unit_price").mean().alias("reference_price"))
    .sort("product_id")
)
print(partial_baseline)

Salida esperada:

partial_week_df.shape: (16, 6)
shape: (4, 2)
┌────────────┬─────────────────┐
│ product_id ┆ reference_price │
│ ---        ┆ ---             │
│ str        ┆ f64             │
╞════════════╪═════════════════╡
│ P001       ┆ 0.55            │
│ P002       ┆ 1.2             │
│ P003       ┆ 0.75            │
│ P004       ┆ 4.5             │
└────────────┴═════════════════┘

El resultado no cambia, ni con dieciséis filas ni con las cuarenta completas — porque, como ya confirmó esta lección, cada producto se vendió siempre al mismo precio exacto durante toda la semana canónica, sin ninguna excepción. Este ejercicio confirma algo importante sobre la robustez de reference_prices en este caso concreto: no depende de cuántos días de la semana canónica se incluyan, porque la varianza real es cero. En un caso de datos reales, con precios que sí varían día a día, este mismo experimento sí mostraría diferencias entre una línea base de tres días y una de siete.

Ejercicio 3 — Argumenta si reference_prices debería actualizarse automáticamente cada semana, o mantenerse fijo. En 2-3 frases, considerando que Kiosko cambia el precio de P002 el 2026-08-15 (un hecho ya resuelto por data-modeling-for-analytics-guide, dbt-analytics-engineering-guide y lakehouse-and-iceberg-guide), argumenta si reference_prices, tal como lo construye esta lección, seguiría siendo útil después de ese cambio de precio, o si necesitaría recalcularse.

Ver solución

reference_prices, calculado sobre la semana canónica del 2026-08-03 al 2026-08-09, refleja el precio de P002 antes del cambio del 2026-08-15 — sigue siendo correcto para evaluar el archivo de S04 del 2026-08-14 (un día antes del cambio), pero dejaría de ser preciso para cualquier archivo posterior al 15 de agosto, donde 1.20 ya no sería el precio vigente de P002. Esto no es un defecto de diseño de esta lección — es una consecuencia honesta de que toda línea base tiene una fecha de vigencia implícita: en un sistema de producción real, reference_prices necesitaría recalcularse periódicamente (por ejemplo, sobre la semana anterior más reciente, en vez de una semana fija para siempre), un problema de mantenimiento que esta guía nombra pero no resuelve en detalle, porque el foco de este módulo es la mecánica de detección, no la operación continua de la línea base.

Resumen y siguiente paso

En esta lección construiste reference_prices, con evidencia ejecutada: un diccionario de cuatro precios, uno por producto, calculado con group_by("product_id").agg(pl.col("unit_price").mean()) sobre las cuarenta filas de la semana canónica de Kiosko — nunca sobre el archivo bajo sospecha. Confirmaste, con un segundo cálculo independiente (std(), n_unique()), que la semana canónica no tiene ninguna variación de precio, y entendiste por qué construir la línea base sobre orders_s04 en vez de sobre orders sería un error circular, con un ejemplo numérico concreto (15.90 en vez de 1.20).

Antes de avanzar deberías poder: reproducir reference_prices desde cero, con los valores exactos de las cuatro claves; explicar por qué AVG() de SQL y .mean() de Polars pueden dar resultados con distinto ruido de punto flotante sobre los mismos datos; y explicar, con un ejemplo numérico propio, por qué construir la línea base sobre el archivo bajo sospecha sería circular.

Tienes la línea base lista. La lección 5 escribe, por fin, la función que la usa para comparar —check_price_baseline()—, probada primero sobre datos de juguete antes de tocar el archivo real de S04.

Recursos

  • Polars — "Aggregation" (guía de usuario oficial, el patrón group_by().agg() con expresiones como .mean(), incluido el ejemplo de agrupación anidada). docs.pola.rs/user-guide/expressions/aggregation. En inglés.
  • Polars — referencia de API, DataFrame.group_by() y GroupBy.agg() (firma completa de los métodos usados en esta lección). docs.pola.rs/api/python/stable/reference/dataframe/group_by.html. En inglés.
  • DuckDB — "Integration with Polars" (el mismo puente .pl() ya usado desde el módulo 2 de esta guía). duckdb.org/docs/lts/guides/python/polars. En inglés.
  • data-engineering-foundations-guide, módulo 2, lección 7 ("Parseando una semana completa de órdenes") — fuente exacta de las cuarenta filas de la semana canónica que esta lección carga como canonical_week.csv. src/guides/data-engineering-foundations-guide/workbook/module-02-batch-vs-streaming-and-the-sla/es/07-parsing-a-week-of-orders.md. En español.
  • DISEÑO de esta guía — la firma exacta de reference_prices y su fuente (la semana canónica S01-S03). src/guides/data-reliability-and-governance-guide/DISENO.md. En español.