UD06 · Notebook 1 — Detección y corrección de sesgos¶
Vamos a auditar la equidad de un modelo real con la biblioteca Fairlearn: medir el sesgo por grupos, mitigarlo con postprocesado y —lo más importante— ver el precio que se paga por hacerlo.
Este notebook acompaña al Taller 2 y al bloque §9 · Sesgos de género y algorítmicos de la teoría de la unidad.
Criterio de evaluación: RA6-f, con conexión a RA6-b (minimización) y RA6-e (privacidad).
1. Instalación y datos¶
Usamos el conjunto UCI Adult (48 842 filas): predice si una persona gana más de 50 000 $ al año a partir de datos demográficos y laborales. Lo cargamos con fairlearn.datasets.fetch_adult, que lo descarga de OpenML y lo deja cacheado: no hay que bajar nada a mano.
%pip install -q fairlearn scikit-learn pandas matplotlib
import pandas as pd
from fairlearn.datasets import fetch_adult
datos = fetch_adult(as_frame=True)
# Quitamos «sex» de las características: no queremos que el modelo decida con ella...
X = datos.data.drop(columns=["sex"])
y = (datos.target == ">50K").astype(int)
# ...pero la guardamos aparte, porque SIN ella no se puede auditar
sexo = datos.data["sex"]
print("filas:", len(X), "| columnas:", len(X.columns))
print("reparto por sexo:", sexo.value_counts().to_dict())
Fíjate en lo que acabamos de hacer
Hemos quitado el sexo de las características y lo hemos guardado aparte. No es un truco: es la única forma de auditar. Si borras el atributo del todo —lo que la teoría llama equidad por desconocimiento— pierdes la capacidad de comprobar si discriminas.
Y eso choca con el principio de minimización del RGPD. Es la paradoja de los sesgos, y te la encuentras en la segunda celda del notebook.
# Tasas base: qué proporción de cada grupo gana de verdad más de 50 000 $
tasas = y.groupby(sexo, observed=True).mean().round(4)
print(tasas.to_dict())
Anota estas dos cifras: las tasas base son muy distintas. Esa es exactamente la condición del resultado de imposibilidad de Kleinberg et al. (2016), y explica lo que veremos en el apartado 5.
2. Preparación y entrenamiento¶
Codificamos las columnas categóricas y entrenamos. Nada de esto es específico de equidad: es un pipeline normal de scikit-learn.
from sklearn.compose import ColumnTransformer
from sklearn.ensemble import HistGradientBoostingClassifier
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder
categoricas = X.select_dtypes(include=["category", "object"]).columns.tolist()
# sparse_output=False es OBLIGATORIO: HistGradientBoostingClassifier no acepta matrices dispersas
prep = ColumnTransformer(
[("cat", OneHotEncoder(handle_unknown="ignore", sparse_output=False), categoricas)],
remainder="passthrough")
modelo = Pipeline([("prep", prep),
("clf", HistGradientBoostingClassifier(random_state=42))])
X_tr, X_te, y_tr, y_te, s_tr, s_te = train_test_split(
X, y, sexo, test_size=0.3, random_state=42, stratify=y)
modelo.fit(X_tr, y_tr)
pred = modelo.predict(X_te)
print("modelo entrenado sobre", len(X_tr), "filas")
3. Métricas de equidad¶
Tres números. Diferencia ≈ 0 significa que la métrica se cumple entre grupos.
from sklearn.metrics import accuracy_score
from fairlearn.metrics import (MetricFrame, demographic_parity_difference,
equalized_odds_difference, selection_rate)
exactitud = accuracy_score(y_te, pred)
dp = demographic_parity_difference(y_te, pred, sensitive_features=s_te)
eo = equalized_odds_difference(y_te, pred, sensitive_features=s_te)
print(f"exactitud global .................. {exactitud:.4f}")
print(f"paridad demográfica (dif) ......... {dp:.4f}")
print(f"igualdad de oportunidades (dif) ... {eo:.4f}")
4. Comparación por grupo: por qué la métrica global engaña¶
Aquí está el corazón del notebook. La exactitud global parecía respetable; mírala por grupo.
mf = MetricFrame(
metrics={"exactitud": accuracy_score, "tasa de selección": selection_rate},
y_true=y_te, y_pred=pred, sensitive_features=s_te)
print(mf.by_group)
import matplotlib.pyplot as plt
ejes = mf.by_group.plot.bar(subplots=True, layout=(1, 2), figsize=(9, 3.5),
legend=False, rot=0, title=["Exactitud", "Tasa de selección"])
plt.tight_layout()
plt.show()
Dos cosas que hay que ver, y las dos son contraintuitivas:
- El modelo acierta más con las mujeres. No es que funcione mejor para ellas: es que hay muchas menos positivas, así que decir «no» casi siempre ya acierta. La exactitud es una métrica tramposa con clases desequilibradas.
- La tasa de selección es unas tres veces mayor en hombres. El modelo propone a un hombre para el grupo de altos ingresos mucho más a menudo.
Una sola cifra global habría ocultado las dos cosas. Es, en la práctica, la paradoja de Simpson.
5. Mitigación con ThresholdOptimizer¶
ThresholdOptimizer ajusta un umbral distinto por grupo para forzar la métrica que le pidas. Es mitigación de postprocesado: no toca los datos ni reentrena el modelo.
from fairlearn.postprocessing import ThresholdOptimizer
opt = ThresholdOptimizer(estimator=modelo, constraints="equalized_odds",
predict_method="predict_proba", prefit=True)
opt.fit(X_tr, y_tr, sensitive_features=s_tr)
pred_opt = opt.predict(X_te, sensitive_features=s_te, random_state=42)
exactitud_opt = accuracy_score(y_te, pred_opt)
dp_opt = demographic_parity_difference(y_te, pred_opt, sensitive_features=s_te)
eo_opt = equalized_odds_difference(y_te, pred_opt, sensitive_features=s_te)
comparativa = pd.DataFrame(
{"antes": [exactitud, eo, dp], "después": [exactitud_opt, eo_opt, dp_opt]},
index=["exactitud global", "igualdad de oportunidades (dif)", "paridad demográfica (dif)"])
print(comparativa.round(4))
mf2 = MetricFrame(
metrics={"exactitud": accuracy_score, "tasa de selección": selection_rate},
y_true=y_te, y_pred=pred_opt, sensitive_features=s_te)
print(mf2.by_group)
Tres lecturas de esa tabla:
- La igualdad de oportunidades se resuelve: cae casi a cero. Es lo que le pedimos con
constraints="equalized_odds". - Se paga un precio: la exactitud global baja. No es gratis, y hay que poder justificar ese coste ante quien paga el sistema.
- La paridad demográfica NO se resuelve. Mejora, pero sigue lejos de cero. Y no es un fallo de la herramienta: las tasas base del apartado 1 son muy distintas, así que por Kleinberg et al. (2016) forzar la igualdad de oportunidades no te da paridad demográfica.
6. Actividad¶
Responde en celdas nuevas debajo de cada punto.
- Rellena la comparativa antes/después con tus números y explica en dos frases a costa de qué mejoró la equidad.
- Cambia
constraintsa"demographic_parity"y vuelve a ejecutar el apartado 5. ¿Qué métrica se arregla ahora y cuál empeora? Relaciónalo con el resultado de imposibilidad. - Cambia el grupo sensible a
racey repite el análisis completo. ¿El sesgo es mayor o menor que con el sexo? ¿Cambia tu conclusión sobre el modelo? - Este modelo, aplicado a selección de personal: ¿qué nivel de riesgo tendría en el AI Act y qué obligaciones concretas le tocarían? ¿Qué dice el RGPD sobre usar el sexo en producción?
- Reto: escribe una model card de este modelo. Debe incluir, como mínimo: para qué sirve y para qué no, con qué datos se entrenó, las métricas por subgrupo del apartado 4 y las limitaciones de equidad que has encontrado.