NM

NutriMetrics Data Pipeline

Reporte de Validación Pandera — pedidos_diarios_2026-06-16.csv

Validación completada — 2026-06-16 08:47:23 UTC • pandera 0.20.1
Total registros
52
pedidos procesados
Válidos
38
superan el contrato
Inválidos
14
violaciones detectadas
Checks fallidos
7
reglas violadas
Calidad datos
73%
objetivo: ≥ 98%
Flujo del pipeline
📥
Ingesta CSV
52 filas leídas
🔄
Type Coercion
fechas parseadas
🛡️
OrderSchema
14 violaciones
🔀
Cuarentena
14 filas aisladas
📊
Analytics
38 filas limpias
📈
Dashboard
datos confiables
Esquema definido — OrderSchema

Campos del modelo DataFrameModel

pedido_id
str
unique=True regex: ^PED-\d{8}$
cliente_id
str
nullable=False len: 1-50
importe
float
ge=0.01 le=5000
estado
str
isin: [pendiente, procesando, completado, cancelado, reembolsado]
moneda
str
isin: [EUR, USD] default=EUR
creado_en
timestamp
nullable=False
enviado_en
timestamp
nullable=True
num_items
int
ge=1 le=50
fuente
str
isin: [pos, app, ubereats]

Checks de nivel DataFrame @dataframe_check

enviado_despues_creado
Cuando enviado_en no es nulo, debe ser posterior a creado_en. Detecta corrupción de timestamps entre sistemas POS y backend.
completado_implica_enviado
Pedidos con estado=completado deben tener enviado_en no nulo. Garantiza integridad de los KPIs de fulfillment.
importe_vs_items
El importe por ítem (importe / num_items) no puede ser menor de 1.00 € ni mayor de 250 €. Detecta pedidos test o errores de conversión.

Config class Config

strict
True — Rechaza columnas extra no declaradas
coerce
True — Convierte tipos automáticamente antes de validar
name
"NutriMetricsOrderSchema"
Implementación Python
# schemas/pedidos.py — Contrato de datos NutriMetrics import pandera as pa from pandera.typing import Series, DataFrame import pandas as pd class OrderSchema(pa.DataFrameModel): """Contrato de datos para pedidos diarios NutriMetrics.""" pedido_id: Series[str] = pa.Field(unique=True, str_matches=r"^PED-\d{8}$") cliente_id: Series[str] = pa.Field(nullable=False) importe: Series[float] = pa.Field(ge=0.01, le=5000) estado: Series[str] = pa.Field(isin=["pendiente", "procesando", "completado", "cancelado", "reembolsado"]) moneda: Series[str] = pa.Field(isin=["EUR", "USD"], default="EUR") creado_en: Series[pd.Timestamp] = pa.Field(nullable=False) enviado_en: Series[pd.Timestamp] = pa.Field(nullable=True) num_items: Series[int] = pa.Field(ge=1, le=50) fuente: Series[str] = pa.Field(isin=["pos", "app", "ubereats"]) @pa.dataframe_check def enviado_despues_creado(cls, df) -> Series[bool]: mask = df["enviado_en"].notna() result = pd.Series(True, index=df.index) result[mask] = df.loc[mask, "enviado_en"] > df.loc[mask, "creado_en"] return result @pa.dataframe_check def completado_implica_enviado(cls, df) -> Series[bool]: completed = df["estado"] == "completado" return ~completed | df["enviado_en"].notna() class Config: strict = True coerce = True # pipelines/ingest.py — Uso en el pipeline @pa.check_types def cargar_pedidos(ruta: str) -> DataFrame[OrderSchema]: df = pd.read_csv(ruta, parse_dates=["creado_en", "enviado_en"]) return df # validado automáticamente por @check_types try: pedidos = cargar_pedidos("data/pedidos_diarios_2026-06-16.csv") except pa.errors.SchemaError as err: print(err.failure_cases) # DataFrame con filas y columnas exactas que fallaron quarantine(err.data) # Aislar registros inválidos en tabla de cuarentena
Resultado por check
pedido_id: unique + regex
Todos los IDs son únicos y siguen el formato PED-XXXXXXXX.
PASS — 52/52 filas OK
importe: ge=0.01 / le=5000
Detectados 4 pedidos con importe negativo (POS offline con reversión fallida).
FAIL — 4 violaciones, filas 7, 19, 33, 48
estado: isin
Estado "entregado" recibido desde UberEats webhook v2 (no mapeado en contrato).
FAIL — 3 violaciones, fuente: ubereats
enviado_despues_creado
Timestamps de envío anteriores a la creación. Probablemente timezone offset entre POS y backend.
FAIL — 5 violaciones, fuente: pos
completado_implica_enviado
2 pedidos marcados "completado" sin fecha de envío. Posible bug en app móvil v3.1.
FAIL — 2 violaciones, filas 12, 41
num_items: ge=1 / le=50
Todos los pedidos tienen entre 1 y 50 ítems. Rango válido.
PASS — 52/52 filas OK
moneda: isin [EUR, USD]
44 pedidos en EUR, 8 en USD (franquicias LATAM). Todo dentro del contrato.
PASS — 52/52 filas OK
fuente: isin [pos, app, ubereats]
Todas las fuentes reconocidas. El valor "entregado" es del campo estado, no fuente.
PASS — 52/52 filas OK
cliente_id: nullable=False
Todos los pedidos tienen cliente asignado. Sin guest checkouts sin ID.
PASS — 52/52 filas OK
failure_cases — Detalle de violaciones (err.failure_cases)
Fila pedido_id Check fallido Columna Valor inválido Regla violada Severidad Fuente
7 PED-00000007 FAIL importe -24.50 ge=0.01 crítico pos
12 PED-00000012 FAIL df-check completado / null completado_implica_enviado crítico app
15 PED-00000015 FAIL df-check 2026-06-15T22:10 > 2026-06-16T08:00 enviado_despues_creado advertencia pos
19 PED-00000019 FAIL importe -8.00 ge=0.01 crítico pos
23 PED-00000023 FAIL estado "entregado" isin(allowed_values) advertencia ubereats
27 PED-00000027 FAIL estado "entregado" isin(allowed_values) advertencia ubereats
33 PED-00000033 FAIL importe -112.00 ge=0.01 crítico pos
36 PED-00000036 FAIL df-check 2026-06-15T23:55 > 2026-06-16T09:12 enviado_despues_creado advertencia pos
41 PED-00000041 FAIL df-check completado / null completado_implica_enviado crítico app
44 PED-00000044 FAIL estado "entregado" isin(allowed_values) advertencia ubereats
48 PED-00000048 FAIL importe -3.25 ge=0.01 crítico pos
Calidad por dimensión
Score global de calidad
73.1%
Completitud (no nulos en campos obligatorios)
100%
Unicidad (pedido_id sin duplicados)
100%
Validez de formato (regex, isin, rangos)
86.5%
Consistencia lógica (checks dataframe)
86%
Importe sin negativos (regla de negocio)
92.3%
Integridad temporal (enviado > creado)
90.4%
Plan de acción recomendado

Causas raíz detectadas & correcciones

Importes negativos
Causa: El POS genera reversiones con importe negativo cuando hay timeout de red.
Acción: Filtrar filas con importe < 0 en pre-procesado; crear tabla separada reversiones con signo positivo y tipo "reversal".
Estado "entregado"
Causa: UberEats webhook v2 cambió "completado" por "entregado" sin preaviso.
Acción: Añadir "entregado" al isin del contrato O mapear en el adaptador UberEats antes de llegar al schema.
Timestamps invertidos
Causa: El POS registra enviado_en en hora local (CET) y el backend lo almacena como UTC sin conversión.
Acción: Normalizar a UTC en el adaptador POS. Añadir tz="UTC" al parse de fechas.
Completado sin envío
Causa: Bug en app móvil v3.1: llama al endpoint PATCH /pedidos/:id/estado pero no persiste enviado_en cuando el repartidor confirma.
Acción: Hotfix en app v3.1.1. Mientras: backfill manual para filas 12 y 41.