IA responsable: detección y mitigación de sesgos
Técnicas prácticas para detectar y mitigar el sesgo en pipelines de machine learning, desde los datos hasta el despliegue y el monitoreo del modelo.

Los modelos de machine learning absorben los sesgos presentes en sus datos de entrenamiento. Un modelo de contratación entrenado con decisiones históricas perpetuará la discriminación del pasado. Un modelo de aprobación de préstamos entrenado con patrones de crédito sesgados rechazará a solicitantes solventes de grupos subrepresentados. Estos no son riesgos hipotéticos: son fallos documentados en grandes empresas.
El desarrollo responsable de IA no consiste en añadir la equidad como una ocurrencia tardía. Requiere un diseño intencional en cada etapa del pipeline de ML: recopilación de datos, ingeniería de características, entrenamiento del modelo, evaluación y monitoreo continuo.
Medir el sesgo antes de poder corregirlo
No se puede corregir lo que no se puede medir. Las métricas de equidad cuantifican cuán distinto trata tu modelo a los diferentes grupos, dándote cifras concretas sobre las cuales optimizar.
# ❌ Only looking at overall accuracy
from sklearn.metrics import accuracy_score
overall_accuracy = accuracy_score(y_true, y_pred)
print(f"Model accuracy: {overall_accuracy:.2%}")
# Output: 92% — looks great! But hides group disparities# ✅ Computing fairness metrics across groups
import numpy as np
from typing import Dict, List
def compute_group_metrics(
y_true: np.ndarray,
y_pred: np.ndarray,
sensitive_attr: np.ndarray,
positive_label: int = 1
) -> Dict[str, Dict[str, float]]:
"""Compute accuracy, TPR, FPR, and selection rate per group."""
groups = np.unique(sensitive_attr)
metrics = {}
for group in groups:
mask = sensitive_attr == group
group_true = y_true[mask]
group_pred = y_pred[mask]
tp = np.sum((group_pred == positive_label) & (group_true == positive_label))
fp = np.sum((group_pred == positive_label) & (group_true != positive_label))
tn = np.sum((group_pred != positive_label) & (group_true != positive_label))
fn = np.sum((group_pred != positive_label) & (group_true == positive_label))
metrics[str(group)] = {
"accuracy": (tp + tn) / len(group_true) if len(group_true) > 0 else 0,
"true_positive_rate": tp / (tp + fn) if (tp + fn) > 0 else 0,
"false_positive_rate": fp / (fp + tn) if (fp + tn) > 0 else 0,
"selection_rate": np.mean(group_pred == positive_label),
"sample_size": int(len(group_true)),
}
return metrics
# Example: loan approval model
metrics = compute_group_metrics(
y_true=actual_outcomes,
y_pred=model_predictions,
sensitive_attr=demographic_groups
)
for group, m in metrics.items():
print(f"Group {group}:")
print(f" Accuracy: {m['accuracy']:.2%}")
print(f" Approval rate: {m['selection_rate']:.2%}")
print(f" True positive rate: {m['true_positive_rate']:.2%}")Un modelo con un 92% de precisión general podría tener un 96% de precisión para un grupo y un 78% para otro. La brecha de equidad es invisible en las métricas agregadas.
Detección de sesgos en los datos de entrenamiento
El sesgo entra en los pipelines de ML principalmente a través de los datos de entrenamiento. Detectar el sesgo a nivel de datos de forma temprana evita construir modelos que codifiquen las desigualdades existentes.
def detect_data_bias(
df,
target_col: str,
sensitive_cols: List[str],
threshold: float = 0.8
) -> Dict[str, List[Dict]]:
"""Detect statistical disparities in the training dataset."""
findings = {"warnings": [], "critical": []}
for col in sensitive_cols:
groups = df[col].unique()
# Check representation
group_sizes = df[col].value_counts(normalize=True)
min_representation = group_sizes.min()
if min_representation < 0.05:
findings["critical"].append({
"type": "underrepresentation",
"column": col,
"detail": f"Group '{group_sizes.idxmin()}' is only "
f"{min_representation:.1%} of data",
})
# Check outcome rates across groups
outcome_rates = df.groupby(col)[target_col].mean()
max_rate = outcome_rates.max()
min_rate = outcome_rates.min()
if max_rate > 0 and (min_rate / max_rate) < threshold:
disparate_groups = {
"highest": f"{outcome_rates.idxmax()} ({max_rate:.2%})",
"lowest": f"{outcome_rates.idxmin()} ({min_rate:.2%})",
}
findings["warnings"].append({
"type": "outcome_disparity",
"column": col,
"ratio": min_rate / max_rate,
"groups": disparate_groups,
})
# Check label noise differences
for group in groups:
group_mask = df[col] == group
group_variance = df.loc[group_mask, target_col].var()
overall_variance = df[target_col].var()
if overall_variance > 0 and group_variance / overall_variance > 1.5:
findings["warnings"].append({
"type": "label_noise",
"column": col,
"group": str(group),
"detail": "Higher label variance suggests noisier labels",
})
return findingsLa regla de los cuatro quintos (umbral de 0.8) es un estándar legal habitual: si la tasa de selección de un grupo es inferior al 80% de la tasa del grupo con mayor tasa, existe evidencia de impacto adverso. Detectarlo en la etapa de datos es mucho más sencillo que corregirlo en el modelo.
Mitigación en el preprocesamiento: reequilibrio y reponderación
Cuando el sesgo existe en los datos, las técnicas de preprocesamiento pueden reducirlo antes de que comience el entrenamiento del modelo.
def compute_sample_weights(
df,
target_col: str,
sensitive_col: str
) -> np.ndarray:
"""Compute sample weights to equalize outcome rates across groups."""
groups = df[sensitive_col].unique()
overall_positive_rate = df[target_col].mean()
weights = np.ones(len(df))
for group in groups:
mask = df[sensitive_col] == group
group_positive_rate = df.loc[mask, target_col].mean()
if group_positive_rate > 0:
# Upweight underrepresented positive outcomes
positive_mask = mask & (df[target_col] == 1)
negative_mask = mask & (df[target_col] == 0)
pos_weight = overall_positive_rate / group_positive_rate
neg_weight = (1 - overall_positive_rate) / (1 - group_positive_rate)
weights[positive_mask] = pos_weight
weights[negative_mask] = neg_weight
# Normalize weights
weights = weights / weights.mean()
return weights
# Usage with scikit-learn
sample_weights = compute_sample_weights(train_df, "approved", "demographic")
from sklearn.ensemble import GradientBoostingClassifier
model = GradientBoostingClassifier(n_estimators=100)
model.fit(X_train, y_train, sample_weight=sample_weights)# ❌ Naive oversampling: can overfit minority groups
from sklearn.utils import resample
minority = df[df["demographic"] == "group_b"]
oversampled = resample(minority, n_samples=len(df) - len(minority))
# Duplicated samples lead to overfitting# ✅ Synthetic minority oversampling (SMOTE) with caution
from imblearn.over_sampling import SMOTE
smote = SMOTE(random_state=42, k_neighbors=5)
X_resampled, y_resampled = smote.fit_resample(X_train, y_train)
# Always evaluate on original distribution, never on resampled dataLa reponderación es, en general, más segura que el remuestreo porque no crea puntos de datos sintéticos ni duplica los existentes. El modelo aprende de los mismos ejemplos, pero otorga la importancia adecuada a los grupos subrepresentados.
Posprocesamiento: calibración de umbrales
Cuando no se puede modificar el pipeline de entrenamiento (por ejemplo, al usar un modelo de terceros o un ensamble complejo), el posprocesamiento ajusta los umbrales de decisión por grupo para alcanzar los objetivos de equidad.
from scipy.optimize import minimize_scalar
def find_fair_thresholds(
y_true: np.ndarray,
y_scores: np.ndarray,
sensitive_attr: np.ndarray,
target_metric: str = "equal_opportunity"
) -> Dict[str, float]:
"""Find per-group thresholds that equalize the target metric."""
groups = np.unique(sensitive_attr)
if target_metric == "equal_opportunity":
# Equalize true positive rates
target_fn = lambda threshold, mask: (
np.sum((y_scores[mask] >= threshold) & (y_true[mask] == 1))
/ max(np.sum(y_true[mask] == 1), 1)
)
elif target_metric == "demographic_parity":
# Equalize selection rates
target_fn = lambda threshold, mask: (
np.mean(y_scores[mask] >= threshold)
)
else:
raise ValueError(f"Unknown metric: {target_metric}")
# Find the average metric value at default threshold (0.5)
default_values = []
for group in groups:
mask = sensitive_attr == group
default_values.append(target_fn(0.5, mask))
target_value = np.mean(default_values)
# Optimize per-group thresholds to match target
thresholds = {}
for group in groups:
mask = sensitive_attr == group
result = minimize_scalar(
lambda t: abs(target_fn(t, mask) - target_value),
bounds=(0.1, 0.9),
method="bounded"
)
thresholds[str(group)] = float(result.x)
return thresholds
# Apply group-specific thresholds
thresholds = find_fair_thresholds(
y_true, model_scores, demographics,
target_metric="equal_opportunity"
)
fair_predictions = np.zeros_like(model_scores)
for group, threshold in thresholds.items():
mask = demographics == group
fair_predictions[mask] = (model_scores[mask] >= threshold).astype(int)La calibración de umbrales introduce una disyuntiva: se mejora la equidad a costa de algo de precisión general. La clave está en hacer explícita y medible esta disyuntiva, en lugar de fingir que no existe.
Monitoreo continuo de la equidad
El sesgo no es un problema puntual. Las distribuciones de datos cambian, las poblaciones de usuarios evolucionan y el comportamiento del modelo puede desviarse de formas que afectan desproporcionadamente a ciertos grupos.
class FairnessMonitor:
def __init__(
self,
sensitive_columns: List[str],
alert_threshold: float = 0.8
):
self.sensitive_columns = sensitive_columns
self.alert_threshold = alert_threshold
self.history: List[Dict] = []
def evaluate(
self,
y_true: np.ndarray,
y_pred: np.ndarray,
sensitive_data: Dict[str, np.ndarray],
timestamp: str
) -> Dict:
report = {"timestamp": timestamp, "alerts": []}
for col in self.sensitive_columns:
attrs = sensitive_data[col]
metrics = compute_group_metrics(y_true, y_pred, attrs)
selection_rates = {
g: m["selection_rate"] for g, m in metrics.items()
}
max_rate = max(selection_rates.values())
min_rate = min(selection_rates.values())
if max_rate > 0:
disparity_ratio = min_rate / max_rate
else:
disparity_ratio = 1.0
if disparity_ratio < self.alert_threshold:
report["alerts"].append({
"column": col,
"disparity_ratio": round(disparity_ratio, 3),
"max_group": max(selection_rates, key=selection_rates.get),
"min_group": min(selection_rates, key=selection_rates.get),
"severity": "critical" if disparity_ratio < 0.6 else "warning",
})
report[col] = metrics
self.history.append(report)
return reportPuntos clave
Construir sistemas de ML justos no consiste en elegir entre precisión y equidad, sino en hacer las disyuntivas explícitas e intencionales. Mide las métricas a nivel de grupo desde el primer día, no solo la precisión agregada. Detecta el sesgo en los datos de entrenamiento antes de que llegue a tu modelo. Usa la reponderación para el preprocesamiento y la calibración de umbrales para el posprocesamiento. Y, lo más importante, monitorea la equidad de forma continua en producción, porque la deriva de los datos puede reintroducir un sesgo que no estaba presente en el lanzamiento.
Las organizaciones que gestionan bien el sesgo en la IA comparten un rasgo común: tratan la equidad como un requisito de ingeniería de primer nivel, con el mismo rigor que la latencia, la disponibilidad y la corrección. El sesgo que no se mide no se gestiona.


