Apéndice B — Calidad del dato en producción

mlops
calidad-datos
gobernanza
monitoreo
Autor/a

Wilder Ramírez Delgado

Fecha de publicación

8 de septiembre de 2026

Serie MLOps y Producción · de la ingeniería de datos al monitoreo.

Este complemento no reemplaza la sesión de clase; sirve como continuación práctica para cerrar la transición de la limpieza exploratoria a la operación real de un pipeline de datos en MLOps.

Enfoque

A diferencia del notebook base, aquí se trabajan tres temas más operativos: - sentineles ocultos - reglas de validación y contratos de dato - monitoreo de calidad y data drift

La idea es pasar de la limpieza como ejercicio aislado a la calidad de datos como capacidad del sistema.

Esto es importante porque un dato puede ser técnicamente ‘válido’ y aun así ser inútil para decisiones de negocio. Un ejemplo clásico es un campo de ingresos con comportamiento estable, pero con un sesgo sistemático por el mecanismo de ausencia. No basta con que el dato exista; debe ser representativo de la población objetivo.


0. Calidad del dato como problema de gobernanza y operación

La mala calidad del dato no es solo un problema estadístico, sino un problema de gobernanza. Si un dato llega con errores al pipeline o al modelo, el problema no se manifiesta solo en una métrica aislada: se propaga por todo el sistema. En MLOps, la calidad del dato debe entenderse como un activo operativo, no como un paso previo de laboratorio.

Desde esta perspectiva, la calidad se mide con varios criterios a la vez: - capacidad de detectar anomalías antes de que impacten el modelo - reproducibilidad del proceso de limpieza - cumplimiento de reglas definidas por el negocio - trazabilidad de la fuente - confiabilidad del dato que ingresa

1. Sentineles ocultos: cuando el dato se ve completo pero no lo está

En datasets reales, no todos los faltantes aparecen como NaN. Muchos sistemas usan valores centinela como -1, 999, N/A, desconocido, cadenas vacías o textos como sin_dato.

Esto genera un problema serio: la columna parece completa, pero en realidad contiene valores que no son confiables. La detección de estos patrones es esencial en ETL y validación de calidad.

import pandas as pd
import numpy as np

df = pd.DataFrame({
    'edad': [25, 32, -1, 41, 999, 28],
    'ingreso_mensual': [2200000, 3100000, 9999999, 2800000, -1, 'N/A'],
    'ciudad': ['Bogotá', 'Medellín', 'desconocido', '', 'Cali', 'Bogotá'],
    'satisfaccion': [5, 4, -1, 3, 99, 2],
})

print('Dataset original:')
display(df)

df_clean = df.copy()
df_clean['edad'] = pd.to_numeric(df_clean['edad'], errors='coerce')
df_clean['ingreso_mensual'] = pd.to_numeric(df_clean['ingreso_mensual'], errors='coerce')
df_clean['satisfaccion'] = pd.to_numeric(df_clean['satisfaccion'], errors='coerce')

df_clean.loc[df_clean['edad'] < 0, 'edad'] = np.nan
df_clean.loc[df_clean['edad'] > 100, 'edad'] = np.nan
df_clean.loc[df_clean['ingreso_mensual'] < 0, 'ingreso_mensual'] = np.nan
df_clean.loc[df_clean['ingreso_mensual'] > 8_000_000, 'ingreso_mensual'] = np.nan
df_clean.loc[(df_clean['satisfaccion'] < 1) | (df_clean['satisfaccion'] > 5), 'satisfaccion'] = np.nan
df_clean['ciudad'] = df_clean['ciudad'].replace(['desconocido', ''], np.nan)

print('Dataset corregido:')
display(df_clean)
Dataset original:
edad ingreso_mensual ciudad satisfaccion
0 25 2200000 Bogotá 5
1 32 3100000 Medellín 4
2 -1 9999999 desconocido -1
3 41 2800000 3
4 999 -1 Cali 99
5 28 N/A Bogotá 2
Dataset corregido:
edad ingreso_mensual ciudad satisfaccion
0 25.0 2200000.0 Bogotá 5.0
1 32.0 3100000.0 Medellín 4.0
2 NaN NaN NaN NaN
3 41.0 2800000.0 NaN 3.0
4 NaN NaN Cali NaN
5 28.0 NaN Bogotá 2.0

2. Reglas de negocio y validación de contrato

La validación no debe limitarse a tipos y nulls. En producción, cada columna debe cumplir un contrato de dato definido por la regla de negocio: - edad entre 18 y 90 - ingreso mensual no negativo - satisfacción en la escala 1 a 5 - fechas con formato válido - ciudades y métodos de pago limitados a un conjunto controlado

Esto hace que los errores se detecten antes de entrar al entrenamiento o a la inferencia.

df_reglas = pd.DataFrame({
    'id_cliente': [1, 2, 3, 4],
    'edad': [20, 120, 35, 27],
    'ingreso': [500000, -20000, 2500000, 3000000],
    'satisfaccion': [5, 4, 7, 2],
    'fecha_registro': ['2024-01-03', '03/02/2024', '2024-05-10', '2024-09-15']
})

edad_ok = df_reglas['edad'].between(18, 90)
ingreso_ok = df_reglas['ingreso'] >= 0
sat_ok = df_reglas['satisfaccion'].between(1, 5)

print('Edad válida:', edad_ok.tolist())
print('Ingreso válido:', ingreso_ok.tolist())
print('Satisfacción válida:', sat_ok.tolist())

def parse_fecha(x):
    for fmt in ('%Y-%m-%d', '%d/%m/%Y'):
        try:
            return pd.to_datetime(x, format=fmt)
        except ValueError:
            pass
    return pd.NaT

df_reglas['fecha_registro'] = df_reglas['fecha_registro'].apply(parse_fecha)
display(df_reglas)
Edad válida: [True, False, True, True]
Ingreso válido: [True, False, True, True]
Satisfacción válida: [True, True, False, True]
id_cliente edad ingreso satisfaccion fecha_registro
0 1 20 500000 5 2024-01-03
1 2 120 -20000 4 2024-02-03
2 3 35 2500000 7 2024-05-10
3 4 27 3000000 2 2024-09-15

3. MCAR, MAR y MNAR: no todas las ausencias sirven para la misma imputación

La clasificación del mecanismo de ausencia no es solo teórica: define la estrategia de imputación y la validez del análisis posterior.

  • MCAR: la ausencia es aleatoria y no depende de nadie.
  • MAR: la ausencia depende de otra variable observada.
  • MNAR: la ausencia depende del valor faltante mismo.

Eso explica por qué imputar con la media puede ser razonable en un caso y sesgado en otro.

np.random.seed(42)
n = 1000
edad = np.random.randint(18, 60, size=n)
ingresos = np.random.normal(2_500_000, 700_000, size=n)
satisfaccion = np.random.randint(1, 6, size=n)

df_missing = pd.DataFrame({'edad': edad, 'ingresos': ingresos, 'satisfaccion': satisfaccion})

prob_mar = np.where(df_missing['edad'] < 25, 0.45, 0.08)
df_missing['ingresos'] = np.where(np.random.rand(n) < prob_mar, np.nan, df_missing['ingresos'])

prob_mnar = np.where(df_missing['satisfaccion'] <= 2, 0.60, 0.06)
df_missing['satisfaccion'] = np.where(np.random.rand(n) < prob_mnar, np.nan, df_missing['satisfaccion'])

print('Tasa de nulos en ingresos por grupo de edad:')
print((df_missing['ingresos'].isna().groupby(np.where(df_missing['edad'] < 25, 'Menor 25', '25+')).mean() * 100).round(1))

print('La ausencia de ingresos depende de otra variable observada: edad.')
Tasa de nulos en ingresos por grupo de edad:
25+          8.3
Menor 25    46.7
Name: ingresos, dtype: float64
La ausencia de ingresos depende de otra variable observada: edad.

4. Imputación: la decisión correcta depende del mecanismo

Cuando la ausencia es MAR o MNAR, la media global es casi siempre una aproximación demasiado ingenua. En estos casos, la imputación debe respetar la lógica del dato y, en algunos casos, se debe modelar la ausencia misma.

Estrategias útiles: - media / mediana / moda según el tipo de variable - imputación por grupo - KNNImputer - IterativeImputer - eliminación solo cuando el porcentaje de nulos es muy bajo y la ausencia es MCAR.

from sklearn.impute import KNNImputer
from sklearn.preprocessing import StandardScaler

df_imput = pd.DataFrame({
    'edad': [22, 24, np.nan, 31, 28, 60],
    'ingresos': [2_000_000, 2_100_000, np.nan, 3_500_000, 2_700_000, 5_000_000],
    'monto_compra': [150000, 180000, 210000, np.nan, 200000, 420000],
})

# Escalamos antes de imputar: 'ingresos' esta en millones y dominaria la distancia
# euclidiana de KNNImputer si se dejara en su escala original frente a 'edad'.
scaler = StandardScaler()
df_imput_escalado = pd.DataFrame(scaler.fit_transform(df_imput), columns=df_imput.columns)

knn = KNNImputer(n_neighbors=3, weights='distance')
df_knn_escalado = pd.DataFrame(knn.fit_transform(df_imput_escalado), columns=df_imput.columns)

# Revertimos la escala para interpretar el resultado en las unidades originales
df_knn = pd.DataFrame(scaler.inverse_transform(df_knn_escalado), columns=df_imput.columns)
print(df_knn)
        edad      ingresos  monto_compra
0  22.000000  2.000000e+06  150000.00000
1  24.000000  2.100000e+06  180000.00000
2  26.444444  2.488889e+06  210000.00000
3  31.000000  3.500000e+06  182402.24587
4  28.000000  2.700000e+06  200000.00000
5  60.000000  5.000000e+06  420000.00000

5. Outliers: no siempre son un error

Un valor atípico puede ser un error de captura, un caso real extremadamente raro, una señal de fraude o una mezcla de poblaciones. Por eso la decisión no depende solo de la estadística, sino también del negocio.

En la práctica se usan varios métodos: - IQR - Z-score - Isolation Forest - reglas explícitas de negocio.

from sklearn.ensemble import IsolationForest

df_out = pd.DataFrame({
    'monto_compra': [120000, 130000, 125000, 140000, 160000, 1_200_000, 150000, 170000],
    'tipo_evento': ['normal', 'normal', 'normal', 'normal', 'normal', 'fraude', 'normal', 'normal']
})

q1 = df_out['monto_compra'].quantile(0.25)
q3 = df_out['monto_compra'].quantile(0.75)
iqr = q3 - q1
lim_inf = q1 - 1.5 * iqr
lim_sup = q3 + 1.5 * iqr
mask_iqr = (df_out['monto_compra'] < lim_inf) | (df_out['monto_compra'] > lim_sup)

print('Límite inferior:', lim_inf)
print('Límite superior:', lim_sup)
print('Registros fuera del rango IQR:')
display(df_out[mask_iqr])

iforest = IsolationForest(contamination=0.1, random_state=42)
labels = iforest.fit_predict(df_out[['monto_compra']])
df_out['outlier_iforest'] = labels == -1
print('Outliers detectados por Isolation Forest:')
display(df_out[df_out['outlier_iforest']])
Límite inferior: 78125.0
Límite superior: 213125.0
Registros fuera del rango IQR:
monto_compra tipo_evento
5 1200000 fraude
Outliers detectados por Isolation Forest:
monto_compra tipo_evento outlier_iforest
5 1200000 fraude True

6. Quality monitoring y data drift

En producción, la calidad del dato no se evalúa una sola vez; se monitoriza con el tiempo. Si la distribución cambia, la tasa de nulos aumenta o se rompe un rango esperado, el modelo puede empezar a fallar aunque el pipeline no haya cambiado.

Eso es data drift: un cambio en la distribución o en la calidad del dato que afecta el comportamiento operacional del sistema.

La calidad de dato en producción no es solo detectar un dato raro, sino detectar un cambio sostenible en el comportamiento del flujo de información. Un solo outlier puede ser un evento aislado; una tendencia persistente de nulos, rangos fuera de regla o desviaciones de distribución es una señal de que la fuente está cambiando.

Existen diferentes tipos de drift que conviene distinguir: - drift de concepto: cambia la relación entre variables y objetivo - drift de esquema: cambian nombres, tipos o estructura de columnas - drift de covariables: cambia la distribución de las entradas - drift de calidad: cambian métricas como nulos, rangos, complejidad o proporción de errores

baseline = {'edad_media': 35, 'ingreso_media': 2_500_000, 'nulos_ingreso_pct': 8.0}
nuevo_batch = {'edad_media': 46, 'ingreso_media': 3_600_000, 'nulos_ingreso_pct': 18.0}

drift_edad = abs(nuevo_batch['edad_media'] - baseline['edad_media']) / baseline['edad_media']
drift_ingreso = abs(nuevo_batch['ingreso_media'] - baseline['ingreso_media']) / baseline['ingreso_media']
drift_nulos = nuevo_batch['nulos_ingreso_pct'] - baseline['nulos_ingreso_pct']

print(f'Drift relativo de la edad: {drift_edad:.2%}')
print(f'Drift relativo del ingreso: {drift_ingreso:.2%}')
print(f'Aumento de nulos en ingreso: {drift_nulos:.2f} puntos porcentuales')

if drift_edad > 0.15 or drift_ingreso > 0.15 or drift_nulos > 5:
    print('Alerta: cambio significativo en la calidad o distribución del dato.')
else:
    print('La calidad parece estable.')
Drift relativo de la edad: 31.43%
Drift relativo del ingreso: 44.00%
Aumento de nulos en ingreso: 10.00 puntos porcentuales
Alerta: cambio significativo en la calidad o distribución del dato.

7. Cierre

La sesión de clase da la base teórica y práctica. Este complemento muestra que la calidad del dato no termina con una limpieza inicial; en producción debe ser una disciplina contínua de validación, medidas de riesgo y monitoreo.

En MLOps, la calidad del dato es una métrica del sistema: si la calidad baja, el rendimiento del modelo y la confianza operacional se deterioran también.

Un marco útil para pensar la calidad del dato es distinguir cuatro niveles: - nivel 1: detección de problemas visibles - nivel 2: validación de reglas y contratos - nivel 3: imputación y tratamiento del sesgo - nivel 4: monitoreo continuo y alertas de drift

Solo cuando el equipo opera en esos cuatro niveles se puede sostener un pipeline con datos confiables a lo largo del tiempo.

Checklist práctico: - documentar la lógica de limpieza por lote - detectar sentineles ocultos - monitorear drift y alertas - definir contratos de dato - tratar outliers con contexto empresarial - clasificar ausencia por mecanismo - decidir imputación según la causa

💬 ¿Te sirvió?

Deja en los comentarios una duda o un caso donde aplicarías esto — respondo todos. Sígueme para no perderte el próximo artículo de la serie y comparte con alguien que esté aprendiendo análisis de datos.

👉 El código completo está disponible para ejecutar directamente.