Introducción
Las pruebas de sistemas basados en inteligencia artificial y aprendizaje automático son un territorio completamente nuevo para QA. Los enfoques tradicionales de pruebas basados en entradas determinísticas y salidas predecibles no funcionan aquí. Cuando el resultado del sistema depende de probabilidades en lugar de reglas claras, ¿cómo determinar si el sistema funciona correctamente?
Según Gartner, para 2025, el 75% de las aplicaciones empresariales utilizarán componentes ML (como se discute en AI-Assisted Bug Triaging: Intelligent Defect Prioritization at Scale). Esto significa que cada ingeniero QA tarde o temprano enfrentará la tarea de probar sistemas AI/ML (como se discute en AI Code Smell Detection: Finding Problems in Test Automation with ML). En este artículo, exploraremos las diferencias fundamentales entre las pruebas ML (como se discute en AI-powered Test Generation: The Future Is Already Here) y las pruebas de software tradicional, y enfoques prácticos para el aseguramiento de calidad.
Por Qué las Pruebas ML Difieren de las Tradicionales
No Determinismo
Software tradicional:
def calculate_discount(price, code):
if code == "SAVE20":
return price * 0.8
return price
# Prueba: resultado siempre predecible
assert calculate_discount(100, "SAVE20") == 80
Sistema ML:
def predict_customer_churn(customer_data):
# Modelo ML retorna probabilidad
prediction = model.predict(customer_data)
return prediction # ¿0.73 o 0.68 o 0.81?
# Prueba: ¡el resultado varía!
# ¿Cómo probar probabilidad?
Dependencia de Datos
En sistemas ML, datos = código. Cambiar el conjunto de datos de entrenamiento puede cambiar radicalmente el comportamiento del modelo, incluso si el código mismo no ha cambiado.
Problemas:
- Data drift: datos de producción difieren de datos de entrenamiento
- Calidad de etiquetas: errores en etiquetado llevan a predicciones incorrectas
- Sesgo de datos: modelo reproduce sesgos de datos
Sin Lógica de Negocio Explícita
El código tradicional contiene reglas explícitas:
if age < 18:
return "Access denied"
Modelo ML es una caja negra:
# ¿Dónde está la lógica? ¡En los pesos de la red neuronal!
prediction = neural_network.forward(input_data)
¿Cómo probar lo que no se puede leer?
Las Métricas de Calidad No Son Binarias
- Prueba tradicional: PASS ✅ o FAIL ❌
- Prueba ML: Accuracy 94.3%, Precision 0.87, Recall 0.91, F1 0.89
¿Qué umbral es aceptable? Esta es una decisión de negocio, no una cuestión técnica.
Validación de Datos: Fundamento de la Calidad ML
Por Qué la Calidad de Datos es Crítica
Regla ML: Basura entra = Basura sale × 100
Los datos malos en software tradicional pueden llevar a un error o crash. En ML, llevan a un modelo que sistemáticamente toma malas decisiones.
Validación de Datos en Pipeline
Etapas de validación de datos:
# 1. Validación de esquema
from great_expectations import DataContext
context = DataContext()
suite = context.create_expectation_suite("ml_data_validation")
# Verificar estructura de datos
batch.expect_column_to_exist("customer_age")
batch.expect_column_values_to_be_between("customer_age", min_value=18, max_value=120)
batch.expect_column_values_to_be_in_set("country", ["US", "UK", "CA", "AU"])
# 2. Verificaciones de distribución de datos
batch.expect_column_mean_to_be_between("purchase_amount", min_value=50, max_value=500)
batch.expect_column_stdev_to_be_between("purchase_amount", min_value=10, max_value=200)
# 3. Integridad referencial
batch.expect_column_values_to_not_be_null("user_id")
batch.expect_compound_columns_to_be_unique(["user_id", "transaction_id"])
Detección de Data Drift
Problema: Los datos de entrenamiento de 2023 pueden no reflejar la realidad de 2025.
Solución: Monitoreo continuo de la distribución de datos
from evidently.dashboard import Dashboard
from evidently.tabs import DataDriftTab
# Comparar datos de producción con dataset de entrenamiento
dashboard = Dashboard(tabs=[DataDriftTab()])
dashboard.calculate(reference_data=train_df, current_data=production_df)
# Métricas de drift:
# - Distancia Wasserstein para features numéricas
# - Índice de Estabilidad de Población (PSI)
# - Divergencia Jensen-Shannon para categóricas
Ejemplo de detección de drift:
Reporte de Drift de Features:
customer_age:
drift_detected: false
drift_score: 0.03
purchase_amount:
drift_detected: true ⚠️
drift_score: 0.47
reason: "Media cambió de $150 a $89"
action: "Reentrenar modelo o investigar cambio de negocio"
device_type:
drift_detected: true ⚠️
drift_score: 0.62
reason: "Tráfico móvil aumentó del 40% al 78%"
Acciones cuando se detecta drift:
- Investigar causa (cambios de negocio vs problema de calidad de datos)
- Reentrenar modelo con nuevos datos
- Prueba A/B del nuevo modelo vs antiguo
- Despliegue gradual con prueba exitosa
Validación de Calidad de Etiquetas
Problema: Si el 10% de las etiquetas son incorrectas, el modelo aprenderá errores.
Estrategias de validación:
1. Validación cruzada de etiquetado:
# Múltiples anotadores etiquetan los mismos datos
from sklearn.metrics import cohen_kappa_score
annotator1_labels = [1, 0, 1, 1, 0, 1]
annotator2_labels = [1, 0, 1, 0, 0, 1]
# Kappa > 0.8 = buen acuerdo
kappa = cohen_kappa_score(annotator1_labels, annotator2_labels)
if kappa < 0.7:
print("⚠️ ¡Los anotadores no están de acuerdo! Se requiere aclaración de directrices")
2. Detección de outliers en etiquetas:
# Encontrar etiquetas sospechosas
from cleanlab import find_label_issues
# Modelo predice probabilidades para cada clase
predicted_probs = model.predict_proba(X_train)
# Cleanlab encuentra ejemplos probablemente mal etiquetados
label_issues = find_label_issues(
labels=y_train,
pred_probs=predicted_probs,
return_indices_ranked_by='self_confidence'
)
print(f"Se encontraron {len(label_issues)} ejemplos potencialmente mal etiquetados")
# Revisar manualmente los top-100 y corregir
Pruebas y Validación de Modelos
Unit Testing para Modelos ML
¡Sí, las pruebas unitarias son posibles incluso para ML!
Probar preprocesamiento de datos:
def test_feature_engineering():
# Given
raw_data = pd.DataFrame({
'date': ['2025-01-01', '2025-01-02'],
'amount': [100, 200]
})
# When
features = preprocess_features(raw_data)
# Then
assert 'day_of_week' in features.columns
assert 'amount_log' in features.columns
assert features['amount_log'].iloc[0] == pytest.approx(4.605, 0.01)
assert features['day_of_week'].iloc[0] == 2 # Miércoles
Probar lógica de inferencia del modelo:
def test_model_prediction_shape():
# Given
model = load_model('churn_predictor_v2.pkl')
test_input = np.random.rand(10, 20) # 10 muestras, 20 features
# When
predictions = model.predict(test_input)
# Then
assert predictions.shape == (10,) # Una predicción por muestra
assert np.all((predictions >= 0) & (predictions <= 1)) # Probabilidades válidas
Pruebas de Rendimiento del Modelo
Métricas para diferentes tipos de tareas:
Clasificación:
from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score
def test_model_classification_performance():
y_true = test_labels
y_pred = model.predict(X_test)
y_proba = model.predict_proba(X_test)[:, 1]
# Accuracy debe superar baseline
accuracy = (y_pred == y_true).mean()
assert accuracy > 0.85, f"Accuracy {accuracy} por debajo del umbral"
# AUC-ROC para evaluación de calidad de ranking
auc = roc_auc_score(y_true, y_proba)
assert auc > 0.90, f"AUC {auc} por debajo del umbral"
# Verificar precision/recall para cada clase
report = classification_report(y_true, y_pred, output_dict=True)
# Clase "fraud" es crítica - recall alto es obligatorio
assert report['fraud']['recall'] > 0.95, "¡Perdiendo demasiados casos de fraude!"
# Falsos positivos son costosos - se necesita buena precisión
assert report['fraud']['precision'] > 0.80, "¡Demasiadas alertas falsas de fraude!"
Detección de Sesgos: Ética y Equidad
Por Qué el Sesgo es un Problema Crítico
Ejemplos reales de sesgo en ML:
- ML de contratación de Amazon (2018): El modelo discriminó a las mujeres, entrenado con CVs donde el 90% eran hombres
- COMPAS (Justicia penal): Modelo de predicción de reincidencia mostró sesgo racial
- Apple Card (2019): Algoritmo dio a mujeres límites de crédito 10-20x más bajos que hombres con igual ingreso
Consecuencias:
- Riesgos legales (discriminación protegida por ley)
- Daño reputacional
- Problemas éticos
- Refuerzo de desigualdad social
Tipos de Sesgo
1. Sesgo de datos:
- Datos de entrenamiento no representativos de todos los grupos
- Sesgo histórico (modelo aprende de discriminación pasada)
- Sesgo de muestreo (algunos grupos subrepresentados)
2. Sesgo del modelo:
- Feature engineering amplifica sesgo
- Features proxy (código postal correlaciona con raza)
- Métrica de optimización no considera equidad
3. Sesgo de despliegue:
- Modelo usado para propósitos no previstos
- Bucles de retroalimentación amplifican sesgo
Detectando Sesgo
Métricas de equidad:
from aif360.metrics import BinaryLabelDatasetMetric, ClassificationMetric
from aif360.datasets import StandardDataset
# Preparar datos con atributo protegido
dataset = StandardDataset(
df=data,
label_name='approved',
protected_attribute_names=['gender'],
privileged_classes=[['male']],
unprivileged_classes=[['female']]
)
# Métrica 1: Diferencia de Paridad Estadística
# Debe estar cerca de 0 (probabilidad igual de resultado positivo)
metric = BinaryLabelDatasetMetric(dataset)
spd = metric.statistical_parity_difference()
assert abs(spd) < 0.1, f"Violación de paridad estadística: {spd}"
# Métrica 2: Diferencia de Igualdad de Oportunidades
# Tasa de verdaderos positivos debe ser igual para todos los grupos
predictions = model.predict(X_test)
metric = ClassificationMetric(
dataset_true=test_dataset,
dataset_pred=predictions,
privileged_groups=[{'gender': 'male'}],
unprivileged_groups=[{'gender': 'female'}]
)
eod = metric.equal_opportunity_difference()
assert abs(eod) < 0.1, f"Violación de igualdad de oportunidades: {eod}"
# Métrica 3: Impacto Dispar
# Ratio de resultados positivos entre grupos
di = metric.disparate_impact()
assert 0.8 <= di <= 1.25, f"Impacto dispar: {di} (umbral legal)"
# Por regla 4/5: ratio < 0.8 = probable discriminación
Mitigando el Sesgo
Enfoques para reducir sesgo:
1. Pre-procesamiento (arreglar datos):
from aif360.algorithms.preprocessing import Reweighing
# Reweighing: asignar pesos a ejemplos para balancear grupos
rw = Reweighing(
unprivileged_groups=[{'gender': 'female'}],
privileged_groups=[{'gender': 'male'}]
)
dataset_transformed = rw.fit_transform(dataset)
# Ahora entrenar modelo con datos re-ponderados
model.fit(dataset_transformed.features,
dataset_transformed.labels,
sample_weight=dataset_transformed.instance_weights)
2. In-processing (entrenamiento justo del modelo):
from aif360.algorithms.inprocessing import PrejudiceRemover
# Modelo optimiza accuracy y equidad simultáneamente
fair_model = PrejudiceRemover(
sensitive_attr='gender',
eta=1.0 # Trade-off entre accuracy y equidad
)
fair_model.fit(X_train, y_train)
A/B Testing para Modelos ML
Por Qué No Se Puede Simplemente Desplegar un Modelo Nuevo
Problemas con evaluación offline:
- Set de prueba puede no reflejar distribución de producción
- Métricas offline no siempre correlacionan con métricas de negocio
- Modelo puede tener comportamiento inesperado en casos extremos
Única manera de conocer calidad verdadera = prueba en producción con usuarios reales.
Diseñando Pruebas A/B de ML
Arquitectura básica:
class MLABTestFramework:
def __init__(self, control_model, treatment_model):
self.control = control_model
self.treatment = treatment_model
self.assignment_cache = {}
def get_prediction(self, user_id, features):
# Asignación consistente: un usuario siempre en un grupo
if user_id not in self.assignment_cache:
self.assignment_cache[user_id] = self._assign_variant(user_id)
variant = self.assignment_cache[user_id]
if variant == 'control':
prediction = self.control.predict(features)
self._log_prediction('control', user_id, prediction)
else:
prediction = self.treatment.predict(features)
self._log_prediction('treatment', user_id, prediction)
return prediction
def _assign_variant(self, user_id):
# Asignación basada en hash para consistencia
hash_val = int(hashlib.md5(str(user_id).encode()).hexdigest(), 16)
return 'treatment' if hash_val % 100 < 50 else 'control'
Métricas para Pruebas A/B de ML
Métricas multi-nivel:
1. Métricas del modelo (verificaciones de cordura):
# Verificar que modelo treatment funcione como se espera
control_accuracy = evaluate_model(control_predictions, labels)
treatment_accuracy = evaluate_model(treatment_predictions, labels)
assert treatment_accuracy >= control_accuracy * 0.95, \
"¡Modelo treatment significativamente peor - detener prueba!"
2. Métricas de engagement de usuario:
# Ejemplo de modelo de recomendación
metrics = {
'control': {
'click_through_rate': 0.12,
'time_on_site': 8.5, # minutos
'items_viewed': 4.2
},
'treatment': {
'click_through_rate': 0.14, # +16.7% 🎉
'time_on_site': 9.1, # +7.1%
'items_viewed': 4.8 # +14.3%
}
}
# Prueba de significancia estadística
from scipy.stats import ttest_ind
control_ctr = get_user_ctr_data('control')
treatment_ctr = get_user_ctr_data('treatment')
t_stat, p_value = ttest_ind(treatment_ctr, control_ctr)
if p_value < 0.05 and treatment_ctr.mean() > control_ctr.mean():
print("✅ ¡Treatment muestra mejora estadísticamente significativa!")
3. Métricas de negocio (estrella polar):
# Objetivo final - ingresos/conversiones
business_metrics = {
'control': {
'revenue_per_user': 45.30,
'conversion_rate': 0.032,
'ltv_30d': 120.50
},
'treatment': {
'revenue_per_user': 48.20, # +6.4%
'conversion_rate': 0.035, # +9.4%
'ltv_30d': 125.80 # +4.4%
}
}
# Significancia económica
users_per_month = 100000
revenue_lift = (48.20 - 45.30) * users_per_month
# = ¡$290,000/mes de ingresos adicionales!
Monitoreo Continuo de Modelos
Por Qué el Monitoreo es Crítico
Los modelos ML se “descomponen” con el tiempo:
- Data drift: el mundo cambia, el modelo se vuelve obsoleto
- Concept drift: las relaciones entre features y target cambian
- Cambios upstream: cambios de API rompen generación de features
Sin monitoreo, aprenderá sobre problemas cuando los usuarios se quejen (demasiado tarde).
Métricas Clave de Monitoreo
1. Métricas de rendimiento del modelo:
# Monitoreo diario
daily_metrics = {
'date': '2025-10-01',
'predictions_count': 1.2M,
'avg_confidence': 0.78, # Cayó de 0.85 - señal de advertencia
# Métricas ground truth (cuando hay etiquetas disponibles)
'accuracy': 0.89, # Era 0.94 hace una semana
'precision': 0.85,
'recall': 0.91,
# Alertas
'alerts': [
'Accuracy cayó 5% en los últimos 7 días',
'Tendencia decreciente de confianza promedio'
]
}
Conclusión
Las pruebas de sistemas AI/ML requieren un enfoque fundamentalmente nuevo:
Conclusiones clave:
✅ Calidad de datos es el 80% del éxito del sistema ML. La validación de datos es crítica en todas las etapas.
✅ Detección de sesgos no es una característica opcional, sino un requisito obligatorio para ML en producción.
✅ Pruebas A/B es la única forma de validar verdaderamente un modelo en producción.
✅ Monitoreo continuo — los modelos ML requieren observación constante, no configurar y olvidar.
Recomendaciones prácticas:
- Automatizar validación de datos en cada pipeline
- Probar equidad como parte de CI/CD
- No confiar en métricas offline — probar en producción
- Monitorear modelos 24/7 — se degradan con el tiempo
- Documentar todo — qué datos, qué suposiciones, qué limitaciones
Las pruebas ML no son solo una nueva habilidad, es una nueva disciplina. Los ingenieros QA que la dominen ahora estarán en demanda durante la próxima década.