lección 2
Las 5 dimensiones de la calidad: completitud, unicidad, validez, consistencia, frescura
Un framework profesional para evaluar datos. Cada dimensión con ejemplos reales, métricas y código para medirlas.
⏱ 55 min
### Un lenguaje común para hablar de calidad
En la lección anterior vimos el desastre que causan los datos malos. Ahora necesitamos un FRAMEWORK — un mapa mental — para pensar sistemáticamente sobre qué puede salir mal. No basta con decir "los datos están mal". Necesitas poder decir EXACTAMENTE qué está mal: "el campo email tiene un 12% de valores nulos" o "hay un 3% de registros duplicados por pedido_id" o "los datos de ayer no han llegado todavía".
Piensa en un médico: no dice "el paciente está mal". Dice "tiene la tensión a 18/10, el colesterol LDL a 240 y la glucosa en 180". Tiene un framework de indicadores de salud que le permite diagnosticar con precisión. Nosotros vamos a hacer lo mismo con los datos: definir 5 dimensiones que cubren TODO lo que puede fallar.
Estas 5 dimensiones no son inventadas — vienen del mundo del Data Quality Management y son usadas por organizaciones como DAMA International (la asociación profesional de gestión de datos) y adoptadas por empresas como Google, Amazon y la mayoría de consultoras de datos. Es el estándar de la industria.
### Dimensión 1: Completitud — ¿están todos los datos?
La completitud responde a una pregunta simple: ¿me falta algo? Puede faltar a nivel de registro (¿llegaron todos los pedidos de ayer?) o a nivel de campo (¿todos los pedidos tienen el campo ciudad relleno?). Es la dimensión más intuitiva y, paradójicamente, la que más se ignora.
Analogía: imagina que pides un puzzle de 1000 piezas y al abrirlo descubres que solo hay 940. Puedes montar la mayor parte del puzzle, pero habrá huecos. Si los huecos están en una esquina, quizás no importa tanto. Pero si los huecos están justo en la cara del retrato, el puzzle es inútil. Con los datos pasa igual: un 5% de NULLs en un campo decorativo es aceptable, pero un 5% de NULLs en el campo "importe" de una tabla de facturación es un desastre.
1import pandas as pd23def medir_completitud(df: pd.DataFrame) -> pd.DataFrame:4 """Mide la completitud de cada columna del DataFrame."""5 resultado = pd.DataFrame({6 'columna': df.columns,7 'total': len(df),8 'no_nulos': df.notna().sum().values,9 'nulos': df.isna().sum().values,10 })11 resultado['completitud_%'] = round(12 (resultado['no_nulos'] / resultado['total']) * 100, 213 )14 resultado['estado'] = resultado['completitud_%'].apply(15 lambda x: '✅' if x >= 99 else ('⚠️' if x >= 95 else '🔴')16 )17 return resultado.sort_values('completitud_%')1819# Ejemplo de uso20clientes = pd.DataFrame({21 'id': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],22 'nombre': ['Ana', 'Luis', 'María', None, 'Pedro', 'Laura', 'Carlos', None, 'Eva', 'Javi'],23 'email': ['ana@mail.com', None, 'maria@mail.com', None, None, 'laura@mail.com', None, None, 'eva@mail.com', 'javi@mail.com'],24 'ciudad': ['Madrid', 'Barcelona', 'Valencia', 'Madrid', 'Sevilla', None, 'Bilbao', 'Madrid', None, 'Barcelona'],25})2627print(medir_completitud(clientes).to_string(index=False))
Función reutilizable para medir la completitud de cualquier DataFrame
### Dimensión 2: Unicidad — ¿hay duplicados?
La unicidad responde a: ¿cada registro aparece una sola vez? Los duplicados son el cáncer silencioso de los datos. Si un pedido aparece dos veces, las ventas se inflan. Si un cliente aparece tres veces, el conteo de clientes activos es incorrecto. Y lo peor: los duplicados pueden ser EXACTOS (misma fila repetida) o PARCIALES (mismo pedido_id pero con campos ligeramente diferentes, ¿cuál es el correcto?).
Analogía: imagina que estás contando personas en una sala para decidir cuántas pizzas pedir. Pero algunas personas entran, salen y vuelven a entrar. Si no tienes un sistema para no contarlas dos veces, pedirás más pizzas de las necesarias. En datos: si tu pipeline de ingesta se ejecuta dos veces (por un retry automático, por ejemplo) y no tienes control de idempotencia, acabas con duplicados.
1def medir_unicidad(df: pd.DataFrame, columnas_clave: list[str]) -> dict:2 """Mide la unicidad basándose en columnas clave."""3 total = len(df)4 duplicados = df.duplicated(subset=columnas_clave, keep=False)5 n_duplicados = duplicados.sum()6 grupos_duplicados = df[duplicados].groupby(columnas_clave).size()78 return {9 'total_registros': total,10 'registros_duplicados': n_duplicados,11 'unicidad_%': round((1 - n_duplicados / total) * 100, 2),12 'grupos_con_duplicados': len(grupos_duplicados),13 'peor_caso': int(grupos_duplicados.max()) if len(grupos_duplicados) > 0 else 0,14 'estado': '✅' if n_duplicados == 0 else ('⚠️' if n_duplicados < total * 0.01 else '🔴'),15 }1617# Ejemplo: pedidos con duplicados18pedidos = pd.DataFrame({19 'pedido_id': [1, 2, 3, 3, 4, 5, 5, 5, 6, 7],20 'producto': ['A', 'B', 'C', 'C', 'A', 'B', 'B', 'B', 'C', 'A'],21 'importe': [10, 20, 30, 30, 15, 25, 25, 25, 35, 12],22})2324resultado = medir_unicidad(pedidos, ['pedido_id'])25for k, v in resultado.items():26 print(f" {k}: {v}")
Duplicados parciales (mismo ID, datos diferentes) son más peligrosos que los exactos
### Dimensión 3: Validez — ¿los valores son legales?
La validez pregunta: ¿este valor tiene sentido? ¿cumple las reglas del dominio? Un email sin @ no es válido. Una edad de -5 años no es válida. Un código postal de 3 dígitos en España no es válido. Una fecha del 30 de febrero no es válida. La validez se define contra REGLAS que tú estableces según el contexto de negocio.
Analogía: un formulario web con validación de campos. No te deja enviar si el email no tiene @, si el teléfono tiene letras, si la fecha de nacimiento es futura. Nosotros hacemos lo mismo pero en batch, para millones de registros que llegan de fuentes que NO tienen esa validación.
1import re2from datetime import datetime34def validar_registro(registro: dict, reglas: dict) -> list[str]:5 """Valida un registro contra un conjunto de reglas. Retorna lista de errores."""6 errores = []78 for campo, validaciones in reglas.items():9 valor = registro.get(campo)1011 for regla_nombre, regla_fn in validaciones.items():12 if not regla_fn(valor):13 errores.append(f"{campo}: falla regla '{regla_nombre}' (valor: {valor})")1415 return errores1617# Definir reglas de validez para un pedido18reglas_pedido = {19 'email': {20 'no_nulo': lambda x: x is not None,21 'tiene_arroba': lambda x: x is not None and '@' in str(x),22 'tiene_dominio': lambda x: x is not None and '.' in str(x).split('@')[-1] if x and '@' in str(x) else False,23 },24 'importe': {25 'no_nulo': lambda x: x is not None,26 'positivo': lambda x: x is not None and x > 0,27 'maximo_razonable': lambda x: x is not None and x < 100000,28 },29 'codigo_postal': {30 'no_nulo': lambda x: x is not None,31 'cinco_digitos': lambda x: x is not None and re.match(r'^\d{5}$', str(x)) is not None,32 },33}3435# Probar con registros buenos y malos36registros = [37 {'email': 'ana@empresa.com', 'importe': 150.0, 'codigo_postal': '28001'},38 {'email': 'sindominio@', 'importe': -20.0, 'codigo_postal': '123'},39 {'email': None, 'importe': 500000.0, 'codigo_postal': '08001'},40]4142for i, reg in enumerate(registros):43 errores = validar_registro(reg, reglas_pedido)44 estado = '✅' if not errores else '🔴'45 print(f"Registro {i+1} {estado}: {len(errores)} errores")46 for e in errores:47 print(f" → {e}")
Las reglas de validez dependen del dominio — un importe de 0.01€ puede ser válido en un contexto e inválido en otro
### Dimensión 4: Consistencia — ¿los datos concuerdan?
La consistencia verifica que los datos no se contradicen entre sí. Un cliente con ciudad "Madrid" y código postal "08001" (que es Barcelona) es inconsistente. Una factura con fecha de pago ANTERIOR a la fecha de emisión es inconsistente. Un pedido con estado "entregado" pero sin fecha de entrega es inconsistente.
Analogía: imagina un testigo en un juicio que dice "estaba en Madrid a las 10" pero luego dice "almorcé en Barcelona a las 11". No necesitas saber la verdad para saber que ALGO está mal — los datos se contradicen entre sí. La consistencia es eso: buscar contradicciones internas.
La consistencia es la dimensión más difícil de automatizar porque requiere conocer las REGLAS DE NEGOCIO. Un check de nulos o duplicados es genérico. Pero saber que "si tipo_envío=express, entonces fecha_entrega debe ser < fecha_pedido + 2 días" requiere entender el dominio. Por eso es tan importante hablar con el equipo de producto.
1def checks_consistencia(df: pd.DataFrame) -> list[dict]:2 """Ejecuta checks de consistencia de negocio sobre pedidos."""3 problemas = []45 # Check 1: fecha_entrega no puede ser anterior a fecha_pedido6 if 'fecha_pedido' in df.columns and 'fecha_entrega' in df.columns:7 mask = df['fecha_entrega'] < df['fecha_pedido']8 if mask.any():9 problemas.append({10 'check': 'fecha_entrega < fecha_pedido',11 'registros_afectados': int(mask.sum()),12 'severidad': 'ALTA',13 })1415 # Check 2: si estado es "entregado", debe tener fecha_entrega16 if 'estado' in df.columns and 'fecha_entrega' in df.columns:17 mask = (df['estado'] == 'entregado') & (df['fecha_entrega'].isna())18 if mask.any():19 problemas.append({20 'check': 'entregado sin fecha_entrega',21 'registros_afectados': int(mask.sum()),22 'severidad': 'ALTA',23 })2425 # Check 3: importe total ≈ cantidad * precio_unitario (tolerancia 1%)26 if all(c in df.columns for c in ['cantidad', 'precio_unitario', 'importe_total']):27 esperado = df['cantidad'] * df['precio_unitario']28 mask = abs(df['importe_total'] - esperado) > esperado * 0.0129 if mask.any():30 problemas.append({31 'check': 'importe_total != cantidad * precio_unitario',32 'registros_afectados': int(mask.sum()),33 'severidad': 'MEDIA',34 })3536 return problemas
Los checks de consistencia son reglas de negocio traducidas a código
### Dimensión 5: Frescura — ¿los datos están actualizados?
La frescura (o timeliness) responde a: ¿estos datos reflejan la realidad ACTUAL? Un dashboard de ventas que muestra datos de hace 3 días es como un GPS con el mapa del año pasado — técnicamente funciona, pero puede llevarte por un camino que ya no existe. La frescura depende del caso de uso: para un informe mensual, datos de ayer son frescos. Para trading algorítmico, datos de hace 1 segundo pueden ser obsoletos.
Analogía: la leche en tu nevera. Tiene fecha de caducidad. Los datos también. Si tu pipeline debería ejecutarse cada hora pero lleva 6 horas sin hacerlo, los datos están "caducados". La pregunta clave es: ¿cuál es el SLA de frescura de cada dataset?
1from datetime import datetime, timedelta23def medir_frescura(ultima_actualizacion: datetime, sla_horas: int) -> dict:4 """Mide la frescura de un dataset comparando con su SLA."""5 ahora = datetime.now()6 edad = ahora - ultima_actualizacion7 edad_horas = edad.total_seconds() / 360089 cumple_sla = edad_horas <= sla_horas1011 return {12 'ultima_actualizacion': ultima_actualizacion.isoformat(),13 'edad_horas': round(edad_horas, 1),14 'sla_horas': sla_horas,15 'cumple_sla': cumple_sla,16 'margen_horas': round(sla_horas - edad_horas, 1),17 'estado': '✅' if cumple_sla else '🔴 DATOS CADUCADOS',18 }1920# Ejemplo: tres datasets con diferentes SLAs21datasets = [22 ('ventas_diarias', datetime.now() - timedelta(hours=2), 24),23 ('stock_tiempo_real', datetime.now() - timedelta(hours=3), 1),24 ('informe_mensual', datetime.now() - timedelta(days=15), 720),25]2627for nombre, ultima_act, sla in datasets:28 frescura = medir_frescura(ultima_act, sla)29 print(f"{nombre}: {frescura['estado']} (edad: {frescura['edad_horas']}h, SLA: {sla}h)")
La frescura es relativa al SLA: datos de hace 2 horas pueden estar frescos o caducados según el contexto
Error común de junior: asumir que si el pipeline se ejecutó, los datos son frescos. NO. El pipeline puede ejecutarse y cargar datos vacíos, datos parciales, o datos del día anterior por un bug. Siempre verifica la fecha REAL de los datos, no solo que el pipeline corrió. Revisa el MAX(fecha) de los registros, no el timestamp del job.
### Poniendo todo junto: el Data Quality Score
En la práctica, querrás un "score" único que resuma la salud de un dataset. No es perfecto (un número no cuenta toda la historia) pero es útil para monitorización y alertas. La idea: mide cada dimensión de 0 a 100, pondera según la criticidad del dataset, y obtén un score global.
1def calcular_dq_score(2 completitud: float, # 0-1003 unicidad: float, # 0-1004 validez: float, # 0-1005 consistencia: float, # 0-1006 frescura: float, # 0-1007 pesos: dict = None,8) -> dict:9 """Calcula un score de calidad ponderado."""10 if pesos is None:11 pesos = {12 'completitud': 0.25,13 'unicidad': 0.25,14 'validez': 0.20,15 'consistencia': 0.20,16 'frescura': 0.10,17 }1819 dimensiones = {20 'completitud': completitud,21 'unicidad': unicidad,22 'validez': validez,23 'consistencia': consistencia,24 'frescura': frescura,25 }2627 score = sum(dimensiones[d] * pesos[d] for d in dimensiones)2829 return {30 'score_global': round(score, 1),31 'dimensiones': dimensiones,32 'peor_dimension': min(dimensiones, key=dimensiones.get),33 'estado': '✅ SANO' if score >= 95 else ('⚠️ DEGRADADO' if score >= 80 else '🔴 CRÍTICO'),34 }3536# Ejemplo37resultado = calcular_dq_score(38 completitud=92.5,39 unicidad=99.8,40 validez=88.0,41 consistencia=95.0,42 frescura=100.0,43)44print(f"Score global: {resultado['score_global']} → {resultado['estado']}")45print(f"Peor dimensión: {resultado['peor_dimension']}")
Un DQ Score es útil para alertas automáticas: si baja de 90, investigar
Consejo de senior: no te obsesiones con llegar a 100% en todas las dimensiones. El objetivo es CONOCER el estado de tus datos y tener UMBRALES que disparen alertas. Un dataset con 97% de completitud que SIEMPRE está en 97% es predecible y manejable. Un dataset que oscila entre 60% y 99% sin explicación es peligroso aunque su media sea alta.
Con estas 5 dimensiones tienes un vocabulario completo para hablar de calidad de datos. En la próxima lección vamos a convertir este framework en código Python ejecutable: funciones de validación, assertions y decoradores que puedes integrar en cualquier pipeline.
## ejercicios
Medir las 5 dimensiones de un dataset de clientes
El equipo de CRM te pasa un export de 10 clientes para que "le eches un ojo". Implementa una función que mida las 5 dimensiones de calidad y genere un informe completo.
💡 Resultado esperado
=== INFORME DE CALIDAD === completitud: 94.3% ⚠️ unicidad: 90.0% ⚠️ validez: 70.0% 🔴
Detectar degradación de frescura en múltiples tablas
Tu empresa tiene 5 tablas que se actualizan con diferentes frecuencias. El equipo de operaciones quiere un "semáforo" que muestre si cada tabla está al día según su SLA. Implementa el monitor de frescura.
💡 Resultado esperado
=== SEMÁFORO DE FRESCURA === Tabla SLA (h) Edad (h) Ratio Estado ---------------------------------------------------------------------- campanas_marketing 12 55.0 4.58 🔴
Escribir reglas de validez para un e-commerce
El equipo de producto te pide que definas las reglas de validez para la tabla de pedidos del e-commerce. Cada campo tiene restricciones específicas del dominio. Implementa un validador completo.
💡 Resultado esperado
=== VALIDACIÓN DE PEDIDOS === ✅ PED-001: válido 🔴 PED-002: 3 errores
Regístrate para guardar tu progreso.
## comentarios
Reporta erratas, ayuda a otros o comparte tu opinión. Sé constructivo.
Inicia sesión para comentar y responder.
cargando comentarios...