Módulo 2: Bias and Fairness — Detection, Measurement, Mitigation
2. Fairness Metric: Demographic Parity
Descripción de la cápsula
Demographic parity es la fairness metric más común y la primera que vas a aprender. También llamada statistical parity o independence, mide si el output favorable de tu modelo es independiente del grupo protegido.
Concretamente: si tu modelo aprueba un loan al 60% de hombres y al 30% de mujeres, no hay demographic parity. La probabilidad de "approve" depende del género.
Esta cápsula cubre:
- La fórmula matemática y su intuición.
- Cómo calcularla en código (numpy + pandas, sin herramientas externas).
- Disparate Impact Ratio — la versión legal/regulatoria de la métrica (4/5 rule).
- Cuándo demographic parity es la métrica correcta y cuándo no lo es.
- Limitations conocidas: por qué demographic parity sola no garantiza fairness en muchos contextos.
Al terminar, vas a tener una función demographic_parity(predictions, groups) que devuelve un número, y vas a saber cuándo aplicarla y cuándo buscar otras métricas.
La definición formal
Demographic parity se cumple cuando:
P(Ŷ = 1 | A = a) = P(Ŷ = 1 | A = b) ∀ a, b ∈ A
Donde:
Ŷes la predicción del modelo (1 = outcome favorable).Aes el atributo protegido (género, raza, etc.).a, bson valores específicos de A.
En español: la probabilidad de que el modelo prediga "outcome favorable" debe ser igual para todos los grupos del atributo protegido.
Ejemplo concreto
Sistema de loan approval:
- Aplican 1000 hombres → 600 approved → P(approve | M) = 0.60.
- Aplican 1000 mujeres → 300 approved → P(approve | F) = 0.30.
- Demographic parity ratio: 0.30 / 0.60 = 0.50.
Demographic parity perfecta = 1.0. Cualquier valor < 1 indica disparidad.
Definición de "outcome favorable"
Importante notar: la métrica se define respecto a "outcome favorable". Para el aplicante:
- Loan: "approved" es favorable.
- Hiring: "interview" o "hired" es favorable.
- Insurance: "low premium" es favorable.
- Healthcare triage: "appointment scheduled quickly" es favorable.
Para algunos casos, hay que invertir la métrica. Si predicción es "high risk" (un score de qué tan probable es default), valor "high" puede ser desfavorable. Asegurate de definir consistentemente cuál es el "favorable outcome".
Implementación desde cero
Sin herramientas externas, solo numpy:
import numpy as np
import pandas as pd
def demographic_parity(predictions, groups, favorable_outcome=1):
"""
Calcula demographic parity ratio.
Args:
predictions: array-like of model predictions (0 or 1).
groups: array-like of group labels (e.g., 'M', 'F').
favorable_outcome: value considered favorable (default 1).
Returns:
dict with rates per group and parity ratio.
"""
df = pd.DataFrame({
'prediction': predictions,
'group': groups,
})
# Tasa de favorable outcome por grupo
rates = df.groupby('group')['prediction'].apply(
lambda x: (x == favorable_outcome).mean()
)
# Demographic parity ratio: min/max
# 1.0 = perfecta paridad; < 1 = disparidad
parity_ratio = rates.min() / rates.max()
return {
'rates_by_group': rates.to_dict(),
'parity_ratio': parity_ratio,
'meets_4_5_rule': parity_ratio >= 0.80,
}
Uso
predictions = [1, 1, 0, 1, 0, 0, 0, 0, 1, 0] # 10 predictions
groups = ['M', 'M', 'F', 'M', 'F', 'F', 'M', 'F', 'M', 'F']
result = demographic_parity(predictions, groups)
print(result)
# {
# 'rates_by_group': {'F': 0.0, 'M': 0.8},
# 'parity_ratio': 0.0,
# 'meets_4_5_rule': False,
# }
P(positive | M) = 4/5 = 0.80. P(positive | F) = 0/5 = 0.0. Ratio = 0.0/0.80 = 0.0. Catastrofa.
Versión más robusta con error handling
def demographic_parity_robust(predictions, groups, favorable_outcome=1):
"""Versión robusta con validation."""
if len(predictions) != len(groups):
raise ValueError("predictions and groups must have same length")
if len(predictions) < 30:
raise ValueError("Need at least 30 samples for stable metric")
df = pd.DataFrame({
'prediction': predictions,
'group': groups,
})
rates = df.groupby('group').agg(
n=('prediction', 'count'),
favorable_rate=('prediction', lambda x: (x == favorable_outcome).mean()),
)
# Verificar suficiente sample por grupo
min_n = rates['n'].min()
if min_n < 30:
raise ValueError(
f"Smallest group has {min_n} samples; need at least 30 per group"
)
parity_ratio = rates['favorable_rate'].min() / rates['favorable_rate'].max()
return {
'rates_by_group': rates.to_dict(),
'parity_ratio': parity_ratio,
'meets_4_5_rule': parity_ratio >= 0.80,
'sample_sizes': rates['n'].to_dict(),
}
Con validation contra sample size insuficiente. Sin enough data por grupo, la métrica tiene alta variance y no es confiable.
Disparate Impact Ratio: la versión legal
Disparate Impact Ratio (DIR) es prácticamente lo mismo que demographic parity ratio, pero con framework legal específico de US:
DIR = P(favorable | minority group) / P(favorable | majority group)
4/5 Rule (también "80% Rule"): si DIR < 0.80, hay evidencia de disparate impact en sentido legal (EEOC, US courts).
Origen histórico: la EEOC (Equal Employment Opportunity Commission) estableció esta regla en Uniform Guidelines on Employee Selection Procedures (1978) para hiring. Desde entonces, se ha generalizado a otras decisiones reguladas.
Implementación
def disparate_impact_ratio(predictions, groups, minority_label, majority_label,
favorable_outcome=1):
"""
DIR según 4/5 rule (EEOC).
"""
df = pd.DataFrame({
'prediction': predictions,
'group': groups,
})
rate_minority = df[df['group'] == minority_label]['prediction'].apply(
lambda x: x == favorable_outcome
).mean()
rate_majority = df[df['group'] == majority_label]['prediction'].apply(
lambda x: x == favorable_outcome
).mean()
if rate_majority == 0:
return None # Indeterminado
dir_value = rate_minority / rate_majority
return {
'rate_minority': rate_minority,
'rate_majority': rate_majority,
'disparate_impact_ratio': dir_value,
'passes_4_5_rule': dir_value >= 0.80,
'legal_concern': dir_value < 0.80,
}
Caso de uso: hiring system audit
import pandas as pd
# Hiring decisions: 1 = invited to interview
hiring_data = pd.DataFrame({
'gender': ['M']*1000 + ['F']*1000,
'invited': [1]*420 + [0]*580 + [1]*280 + [0]*720,
})
result = disparate_impact_ratio(
predictions=hiring_data['invited'],
groups=hiring_data['gender'],
minority_label='F',
majority_label='M',
)
print(result)
# {
# 'rate_minority': 0.28, # 28% mujeres invitadas
# 'rate_majority': 0.42, # 42% hombres invitadas
# 'disparate_impact_ratio': 0.667,
# 'passes_4_5_rule': False,
# 'legal_concern': True,
# }
DIR = 0.667 < 0.80. Legal concern bajo EEOC framework.
Cuándo demographic parity es la métrica correcta
Demographic parity es apropiada cuando:
1. La distribución de outcomes debe ser independiente del grupo
Por elección de policy o por requirement legal. Ejemplos:
- Affirmative action programs: explícitamente targeting demographic parity.
- Resource allocation donde la distribución debe igualar demografía.
- Marketing exposure: queremos que cada grupo demográfico vea similar % de ads relevantes.
2. No tenés ground truth confiable de "merit"
Cuando no podés medir objetivamente quién "merece" el outcome favorable, demographic parity es defaultable conservador. Ejemplo: en advertising, ¿quién "merece" ver un ad? No hay merit objetivo. Distribución igualitaria es defensible.
3. Alineación con marco legal regulatorio
US EEOC framework usa explícitamente disparate impact (variant de demographic parity) como criterio legal. Compliance requiere medir esta métrica específicamente.
4. Alta visibility / accountability requirement
Cuando cualquier disparidad pública sería problemática, demographic parity es la métrica más conservadora y más fácil de defender.
Cuándo demographic parity NO es apropiada
Tres scenarios donde demographic parity puede ser mala métrica:
Scenario 1: hay ground truth de merit y los grupos tienen base rates distintas
Ejemplo: si en realidad un grupo tiene 30% default rate y otro 10%, demographic parity en loan approval forzaría dar loans a default-prone applicants, generando harm a ellos (pierden dinero, hurts credit) y al banco.
En este caso, equalized odds (cápsula 03) es más apropiado: equal error rates, no equal approval rates.
Scenario 2: aplicar cambia el equilibrio de manera nociva
Imaginá un dataset donde un grupo está sub-representado (10%). Forzar demographic parity puede requerir aprobar a casos marginales del grupo minoritario, generando false positives que harm al individuo aprobado (loan que va a default).
Demographic parity puede ser patternalmente útil sin ser individualmente útil.
Scenario 3: el outcome no es zero-sum
Para algunos sistemas (ej., advertising), un grupo viendo más ads no significa que otro vea menos. Demographic parity en ese context puede no tener sentido — el outcome no es competitive.
Limitations: por qué demographic parity sola no es suficiente
Aún cuando demographic parity es la métrica correcta para tu context, sola no garantiza fairness.
Limitation 1: oculta calibration issues
Podés tener perfecta demographic parity (50% approval para grupo A y B) pero:
- Para grupo A: loans aprobados defaultan en 5%.
- Para grupo B: loans aprobados defaultan en 25%.
Demographic parity = 1.0 (perfecta). Pero el grupo B está siendo "approved" hacia outcomes peores. Eso es calibration violation (cápsula 03).
Limitation 2: oculta accuracy disparities
Demographic parity puede mantenerse mientras la calidad de la decisión difiere por grupo:
- Grupo A: 40% true positives + 10% false positives = 50% approval rate.
- Grupo B: 10% true positives + 40% false positives = 50% approval rate.
Demographic parity ✅. Pero grupo B está mucho peor servido (más errores).
Limitation 3: incentivo perverso
Si demographic parity es el único objective, podés satisfacerla con decisiones random:
- Approve 50% al azar de cada grupo. Demographic parity = 1.0.
- Pero la decisión no tiene relación con merit del aplicante.
Es satisfacer la métrica sin servir el propósito.
Implicación
Demographic parity es útil pero no suficiente. La cápsula 03 introduce equalized odds y calibration que cubren los gaps.
Caso real: aplicación a Apple Card
Apple Card emitía credit limits significativamente más bajos a mujeres.
Si Goldman Sachs hubiera medido demographic parity (con sample con género inferible o joinearable):
# Hipotético análisis post-hoc
apple_card_data = pd.DataFrame({
'gender': ['M']*500 + ['F']*500, # sample balanceado
'high_credit_limit': [...], # 1 si > median
})
result = demographic_parity(
predictions=apple_card_data['high_credit_limit'],
groups=apple_card_data['gender'],
)
# Hipotético resultado:
# rate_M = 0.55 (55% de hombres reciben above-median limit)
# rate_F = 0.25 (25% de mujeres reciben above-median limit)
# parity_ratio = 0.25 / 0.55 = 0.45
# meets_4_5_rule = False (0.45 < 0.80)
Tres conclusiones:
- Goldman pudo haberlo detectado con esta métrica simple, antes de lanzar.
- Falla 4/5 rule = legal concern bajo EEOC-equivalente frameworks.
- Independiente de "fault": aún si Goldman no usaba género como feature, el output mostraba disparidad. Métrica del output, no de inputs, detecta proxy discrimination.
Trampas y errores comunes
1. Comparar la wrong rate
A veces es tentador comparar conditional rates en vez de marginal:
# ❌ Wrong: P(approve | qualified, group=A) vs P(approve | qualified, group=B)
# Esto es equalized odds, no demographic parity
Demographic parity compara rates marginales: P(approve | group), sin condicionar en otra variable.
2. Sample sizes insuficientes
Si tenés 5 samples del grupo minoritario, demographic parity es muy variable. Usá at least 30 por grupo (regla básica de inferencia estadística).
3. Definir "favorable outcome" inconsistentemente
A veces "1" es favorable, a veces es desfavorable. Decidí explícitamente y documentá.
4. Reportar solo el ratio sin las rates
parity_ratio = 0.50 da menos información que rate_A = 0.30, rate_B = 0.60, ratio = 0.50. Las rates per se son crítica para entender magnitude.
5. Tratar 0.79 como "casi 0.80"
4/5 rule es threshold legal. 0.79 falla. No "casi pasa". Aplicá threshold estricto.
Auto-verificación
1. ¿Cuál es la fórmula matemática de demographic parity?
P(Ŷ = 1 | A = a) = P(Ŷ = 1 | A = b) ∀ a, b
En palabras: la probabilidad de predicción positiva debe ser igual para todos los grupos del atributo protegido.
Como ratio:
parity_ratio = P(Ŷ = 1 | A = minority) / P(Ŷ = 1 | A = majority)
1.0 = perfecta paridad. 0.80 = threshold legal típico (4/5 rule).
2. ¿Por qué demographic parity = 1.0 no garantiza fairness completa?
Porque puede ocultar issues en otras dimensiones:
-
Calibration violation: rates iguales pero los positives del grupo B defaultan más → grupo B está siendo "approved hacia outcomes peores".
-
Accuracy disparity: rates iguales pero composición distinta (más false positives en grupo B) → grupo B peor servido por sistema.
-
Random satisfaction: random approval satisface demographic parity pero no sirve el propósito.
Por eso necesitamos otras métricas además (equalized odds, calibration). Cápsula 03 las cubre.
3. ¿Cuándo demographic parity es la métrica preferida sobre equalized odds?
Cuando:
- No tenés ground truth confiable de "merit" (advertising, recommendation).
- Policy intencionalmente requiere paridad (affirmative action, equal exposure).
- El outcome no es zero-sum (más ads para A no resta a B).
- Marco legal lo requiere específicamente (EEOC 4/5 rule).
- Visibility/accountability requirement alto (cualquier disparidad pública sería problemática).
Cuando hay ground truth confiable y base rates distintas entre grupos (loans, healthcare), equalized odds es típicamente más apropiado.
4. ¿Cómo aplicarías demographic parity testing como CI gate?
# tests/test_fairness.py
def test_demographic_parity_gate():
"""Bloquea deploy si demographic parity falla 4/5 rule."""
# Tomar predictions del modelo en test set
predictions = model.predict(test_set)
# Para cada atributo protegido, verificar
for attribute in ['gender', 'race', 'age_bracket']:
result = demographic_parity(
predictions=predictions,
groups=test_set[attribute],
)
assert result['meets_4_5_rule'], (
f"Demographic parity violation for {attribute}: "
f"ratio={result['parity_ratio']:.3f} (threshold 0.80). "
f"Rates: {result['rates_by_group']}"
)
Integrá en CI:
# .github/workflows/ci.yml
- run: pytest tests/test_fairness.py
Si el modelo deteriora fairness, CI bloquea merge. Si mejora, pasa.
Importante: este test requiere test set con atributos demográficos. Si no podés/quieres tener esto en CI, alternativas:
- Counterfactual testing (cápsula 04).
- Periodic batch testing (no en CI sino weekly job).
- Shadow deployment con demographic instrumentation.
Resumen y siguiente paso
- Demographic parity mide si el output favorable es independiente del grupo protegido.
- Fórmula: P(Ŷ=1 | A=a) = P(Ŷ=1 | A=b) ∀ a, b.
- Threshold legal típico: 4/5 rule (DIR ≥ 0.80).
- Implementable en 10 líneas de Python con pandas.
- Apropiada cuando: no hay ground truth de merit, policy requiere paridad, marco legal aplica.
- No suficiente sola: oculta calibration y accuracy issues. Combinala con otras métricas.
Checkpoint: deberías poder implementar demographic parity desde cero y aplicarla a un dataset.
Puente a la siguiente cápsula: la cápsula 03 introduce equalized odds y calibration — las métricas que cubren los gaps de demographic parity. Equalized odds mide igualdad de error rates (false positives, false negatives) por grupo. Calibration mide si "score = 0.7" significa lo mismo across grupos. Ambas son cruciales para sistemas con ground truth confiable (loans, hiring con verifiable performance, healthcare diagnoses).
Recursos
- 4/5 Rule — EEOC Uniform Guidelines — base legal US.
- Fairness in ML textbook — Chapter 3 — definición formal.
- AIF360 — DemographicParity metric — implementación.
- Fairlearn — DemographicParity — alternativa.
Siguiente: 03-fairness-metrics-equalized-odds.md — Equalized odds y calibration.
Cápsula 02 de 08 — Módulo 2 — AI Ethics & Compliance Guide