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%
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".
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.
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.
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.
Acción: Hotfix en app v3.1.1. Mientras: backfill manual para filas 12 y 41.