{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# UD06 · Notebook 3 — Auditoría de sesgos con Fairlearn\n", "\n", "**Objetivo**: medir el sesgo de un modelo real con **Fairlearn**, mitigarlo y **comprobar el precio\n", "que se paga** por hacerlo (RA6-f, con conexión a RA6-b y RA6-e).\n", "\n", "**Todo el código de este taller está ejecutado**\n", "\n", "Las cifras que verás abajo son las que salen de verdad al ejecutarlo con `fairlearn 0.14.0`,\n", "`scikit-learn 1.9.0` y `pandas 3.0.5`. Si tus números difieren un poco, revisa las versiones: no\n", "revises tu razonamiento.\n", "\n", "Es una **entrega evaluable**: se corrige con la rúbrica de su tarea en Moodle." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Fase 1 — Prepara el entorno y carga los datos\n", "\n", "Usaremos el conjunto **UCI Adult** (48.842 filas), que predice si una persona gana más de 50 000 $ al\n", "año a partir de datos demográficos y laborales. Fairlearn lo trae ya empaquetado, así que **no hay\n", "que descargar nada a mano**.\n", "\n", "\n", "\n", "\n", "**La primera lección aparece antes de entrenar**\n", "\n", "Fíjate en lo que acabas de hacer: **has quitado el sexo de las características y lo has guardado\n", "aparte**. Eso no es un truco: es la única forma de auditar. Si borras el atributo del todo\n", "—«equidad por desconocimiento», §9.2— pierdes la capacidad de comprobar si discriminas. Y eso\n", "choca de frente con la minimización del RGPD: es la **paradoja de los sesgos** del §9.8, en la\n", "primera celda del taller.\n", "\n", "Antes de seguir, mide las **tasas base** de cada grupo, porque de eso depende todo lo demás:\n", "\n", "\n", "\n", "**Anota este dato**\n", "\n", "**El 10,93 % de las mujeres del conjunto gana más de 50 000 $, frente al 30,38 % de los hombres.**\n", "Las tasas base son **muy distintas**, y esa es exactamente la condición del resultado de\n", "imposibilidad de **Kleinberg et al. (2016)** (§9.4). Recuérdalo en la Fase 5, cuando veas que no\n", "puedes arreglarlo todo a la vez." ] }, { "cell_type": "code", "metadata": {}, "execution_count": null, "outputs": [], "source": [ "%pip install fairlearn scikit-learn pandas\n", "\n", "import pandas as pd\n", "from fairlearn.datasets import fetch_adult\n", "\n", "datos = fetch_adult(as_frame=True)\n", "\n", "# TU CÓDIGO: quita «sex» de X, construye y (>50K), y conserva sexo aparte\n", "X = ...\n", "y = ...\n", "sexo = ...\n", "\n", "print(\"filas:\", len(X), \"| columnas:\", len(X.columns))\n", "print(sexo.value_counts().to_dict())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**✏️ Respuesta**: si borrases el atributo del todo —«equidad por desconocimiento»—, ¿qué\n", "perderías? Relaciónalo con la minimización de datos del RGPD.\n", "\n", "*(escribe aquí)*" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Antes de entrenar, mide las **tasas base** de cada grupo: de eso depende todo lo demás." ] }, { "cell_type": "code", "metadata": {}, "execution_count": null, "outputs": [], "source": [ "# TU CÓDIGO: la proporcion de cada grupo que gana mas de 50.000\n", "..." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**✏️ Respuesta**: anota las dos tasas base. ¿Son parecidas o muy distintas? Esa es la\n", "condición exacta del resultado de imposibilidad de **Kleinberg et al. (2016)**: recuérdalo en la\n", "Fase 5, cuando veas que no puedes arreglarlo todo a la vez.\n", "\n", "*(escribe aquí)*" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Fase 2 — Entrena un modelo\n", "\n", "**`sparse_output=False` no es opcional**\n", "\n", "`HistGradientBoostingClassifier` **no acepta matrices dispersas**. Si dejas el\n", "`OneHotEncoder` por defecto, el `fit` revienta con\n", "`TypeError: Sparse data was passed for X, but dense data is required`. Es el fallo más habitual\n", "de este taller." ] }, { "cell_type": "code", "metadata": {}, "execution_count": null, "outputs": [], "source": [ "from sklearn.compose import ColumnTransformer\n", "from sklearn.ensemble import HistGradientBoostingClassifier\n", "from sklearn.model_selection import train_test_split\n", "from sklearn.pipeline import Pipeline\n", "from sklearn.preprocessing import OneHotEncoder\n", "\n", "# TU CÓDIGO: prepara el pipeline, parte los datos (arrastrando `sexo`) y entrena\n", "...\n", "pred = ..." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Fase 3 — Mide la equidad\n", "\n", "- **Diferencia ≈ 0** significa que la métrica se cumple entre grupos.\n", "- **Paridad demográfica 0,1687**: hay **17 puntos** de diferencia en la tasa de selección. Mucho.\n", "- **Igualdad de oportunidades 0,0731**: 7 puntos. Menos, pero no cero." ] }, { "cell_type": "code", "metadata": {}, "execution_count": null, "outputs": [], "source": [ "from sklearn.metrics import accuracy_score\n", "from fairlearn.metrics import (MetricFrame, demographic_parity_difference,\n", " equalized_odds_difference, selection_rate)\n", "\n", "# TU CÓDIGO: los tres valores\n", "..." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**✏️ Respuesta**: la exactitud global es alta. ¿Te vale para decir que el modelo es justo?\n", "\n", "*(escribe aquí)*" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Fase 4 — Compara por grupo (y desconfía de la métrica global)\n", "\n", "**Aquí está el corazón del taller**\n", "\n", "La exactitud global era 0,8753, un número respetable. Pero por grupo:\n", "\n", "- El modelo acierta **más** con las mujeres (0,9358) que con los hombres (0,8453). ¿Es que\n", " funciona mejor para ellas? **No.** Acierta más porque solo el 10,93 % de las mujeres son\n", " positivas, así que **decir «no» casi siempre ya acierta**. La exactitud es una métrica\n", " tramposa con clases desequilibradas.\n", "- La **tasa de selección** es del **8,5 % para las mujeres y del 25,4 % para los hombres**: el\n", " modelo propone a un hombre para el grupo de altos ingresos **tres veces más a menudo**.\n", "\n", "Una sola cifra global habría ocultado las dos cosas. Esto es, en la práctica, lo que enseña la\n", "**paradoja de Simpson** (§9.5) y por qué el checklist del §9.7 insiste en **medir por subgrupo**." ] }, { "cell_type": "code", "metadata": {}, "execution_count": null, "outputs": [], "source": [ "# TU CÓDIGO\n", "mf = ...\n", "print(mf.by_group)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**✏️ Respuesta**: ¿en qué grupo acierta más el modelo? ¿Y a qué grupo **selecciona** más? Si\n", "las dos respuestas no coinciden, explica por qué la métrica global no lo veía.\n", "\n", "*(escribe aquí)*" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Fase 5 — Mitiga el sesgo y mide el precio\n", "\n", "`ThresholdOptimizer` ajusta **un umbral distinto por grupo** para forzar la métrica que le pidas. Es\n", "mitigación de **postprocesado**: no toca los datos ni reentrena el modelo.\n", "\n", "\n", "\n", "Rellena esta tabla en tu informe con **tus** números:\n", "\n", "| Métrica | Antes | Después | ¿Mejoró? |\n", "|---|---|---|---|\n", "| Exactitud global | 0,8753 | 0,8611 | |\n", "| Igualdad de oportunidades (dif) | 0,0731 | 0,0014 | |\n", "| Paridad demográfica (dif) | 0,1687 | 0,0950 | |\n", "| Tasa de selección · mujeres | 0,0850 | 0,1017 | |\n", "| Tasa de selección · hombres | 0,2538 | 0,1967 | |\n", "\n", "**Las tres cosas que hay que ver en esta tabla**\n", "\n", "1. **La igualdad de oportunidades se ha resuelto**: de 0,0731 a **0,0014**, prácticamente cero.\n", " Es lo que le pedimos con `constraints=\"equalized_odds\"`, y lo cumple.\n", "2. **Se paga un precio**: la exactitud global baja de 0,8753 a **0,8611**, 1,4 puntos. No es\n", " gratis, y hay que poder justificar ese coste ante quien paga el sistema.\n", "3. **La paradoja demográfica NO se ha resuelto**: mejora de 0,1687 a 0,0950, pero sigue lejos de\n", " cero. **Y no es un fallo de la herramienta.** Las tasas base son 10,93 % y 30,38 % (Fase 1),\n", " así que por **Kleinberg et al. (2016)** no puedes tener a la vez calibración e igualdad de\n", " oportunidades, y forzar la igualdad de oportunidades **no** te da paridad demográfica.\n", "\n", "Prueba a cambiar `constraints=\"demographic_parity\"` y observa qué se rompe entonces. Eso es el\n", "ejercicio 79 hecho con datos." ] }, { "cell_type": "code", "metadata": {}, "execution_count": null, "outputs": [], "source": [ "from fairlearn.postprocessing import ThresholdOptimizer\n", "\n", "# TU CÓDIGO: ajusta con constraints=\"equalized_odds\" y vuelve a medir las tres metricas\n", "..." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**✏️ Respuesta**: rellena la tabla con **tus** números.\n", "\n", "| Métrica | Antes | Después | ¿Mejoró? |\n", "|---|---|---|---|\n", "| Exactitud global | | | |\n", "| Igualdad de oportunidades (dif) | | | |\n", "| Paridad demográfica (dif) | | | |\n", "| Tasa de selección · mujeres | | | |\n", "| Tasa de selección · hombres | | | |\n", "\n", "Y responde: **¿qué has pagado** por reducir la desigualdad de oportunidades? ¿Se arreglaron **las\n", "dos** métricas de equidad a la vez?\n", "\n", "*(escribe aquí)*" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Fase 6 — Reflexión ética y normativa\n", "\n", "Responde en el informe:\n", "\n", "1. Has quitado `sex` de las características pero lo has usado para auditar. ¿Cómo justificarías ese\n", " tratamiento ante el principio de **minimización** del RGPD (§5.2)? ¿Qué base legal y qué plazo\n", " pondrías?\n", "2. Si no pudieras tratar el sexo de ninguna manera, ¿cómo auditarías el modelo con **variables\n", " proxy** (§9.8)? Nombra dos columnas del conjunto que podrían servir y explica el riesgo de usarlas.\n", "3. Este sistema, aplicado a selección de personal, ¿qué **nivel de riesgo** tendría en el AI Act y\n", " qué obligaciones concretas le tocarían (§6.1)?\n", "4. Bajo la **Ley 15/2022**, si una candidata denuncia discriminación, ¿quién tiene que demostrar qué?\n", " ¿Qué documento de este taller usarías como prueba?\n", "5. La exactitud bajó 1,4 puntos al mitigar. Redacta en **tres frases** cómo se lo explicarías a un\n", " responsable que solo mira la exactitud global.\n", "6. Aplica una técnica del §8.2 a este conjunto de datos: si tuvieras que **publicarlo** para que otro\n", " centro replicara el estudio, ¿qué harías y por qué no basta con borrar el nombre?" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Entrega\n", "\n", "Sube a Moodle un informe con la tabla de la Fase 5 rellena, la salida por grupo de la Fase 4 y las\n", "seis respuestas de la Fase 6. Extensión orientativa: **dos o tres páginas**.\n", "\n", "**Corrección**\n", "\n", "Las soluciones no se publican: se corrigen y comentan en clase." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python", "version": "3.12" } }, "nbformat": 4, "nbformat_minor": 5 }