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:

  1. La fórmula matemática y su intuición.
  2. Cómo calcularla en código (numpy + pandas, sin herramientas externas).
  3. Disparate Impact Ratio — la versión legal/regulatoria de la métrica (4/5 rule).
  4. Cuándo demographic parity es la métrica correcta y cuándo no lo es.
  5. 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).
  • A es el atributo protegido (género, raza, etc.).
  • a, b son 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:

  1. Goldman pudo haberlo detectado con esta métrica simple, antes de lanzar.
  2. Falla 4/5 rule = legal concern bajo EEOC-equivalente frameworks.
  3. 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:

  1. Calibration violation: rates iguales pero los positives del grupo B defaultan más → grupo B está siendo "approved hacia outcomes peores".

  2. Accuracy disparity: rates iguales pero composición distinta (más false positives en grupo B) → grupo B peor servido por sistema.

  3. 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:

  1. No tenés ground truth confiable de "merit" (advertising, recommendation).
  2. Policy intencionalmente requiere paridad (affirmative action, equal exposure).
  3. El outcome no es zero-sum (más ads para A no resta a B).
  4. Marco legal lo requiere específicamente (EEOC 4/5 rule).
  5. 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

  1. 4/5 Rule — EEOC Uniform Guidelines — base legal US.
  2. Fairness in ML textbook — Chapter 3 — definición formal.
  3. AIF360 — DemographicParity metric — implementación.
  4. 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