O problema real
Uma empresa brasileira lança modelo de classificação de crédito. Treina em 10 anos de dados históricos de aprovações/rejeições. Modelo fica muito preciso — 92% de acurácia no teste. Em produção, descobre: modelo nega crédito 40% mais em aplicações de pessoas negras que de pessoas brancas, pra mesmo perfil de renda.
A culpa? Não do algoritmo (funciona mecanicamente igual pra todos). Culpa: dataset histórico. 40 anos atrás, banco rejeitava mais gente negra. Dados históricos codificam esse preconceito. Modelo aprendeu: "traços X indicam rejeição" porque historicamente foi assim.
Viés não é acaso. É padrão sistemático em favor/desfavor de certo grupo. E afeta negócio (risco legal, percepção de marca) e pessoas (discriminação).
Viés é propriedade da distribuição, não de um example
Um example isolado ({renda: $5k, raça: negra, aprovado: não}) não prova viés. Precisam de padrão. A pergunta certa é: "qual é a taxa de aprovação por grupo?"
Tipos comuns de viés:
Viés de amostragem
Dataset tem representação desigual de grupos.
import pandas as pd
# 10k aplicações, mas 90% são homens
df = pd.read_csv('credit_applications.csv')
print(df['genero'].value_counts(normalize=True))
# genero
# Masculino 0.90
# Feminino 0.10
# Outro 0.00
Treina modelo nessa distribuição, 90% de exemplos é homem. Modelo aprende features mais bem representadas. Pra mulher, extrapolação (pode quebrar).
Viés de rótulo
Labels errados ou inconsistentes pra certos grupos.
# Banco historicamente aprovava homens com risco alto
# Rejeitava mulheres com risco similar
# Label "aprovado" é inconsistente:
# {renda: $3k, homem} → label: aprovado (risco ~50%)
# {renda: $3k, mulher} → label: rejeitado (risco ~50%)
# Modelo treina: "ser homem importa pra aprovação"
Viés histórico
Dataset codifica discriminações passadas. Mesmo que labels sejam "corretos" pra época, herdam preconceito:
# 1985: banco rejeita negros por preconceito puro
# 2025: dados históricos refletem isso
# Modelo treina, aprende: "raça correlaciona com rejeição"
Viés de agregação
Diferentes subgrupos têm padrões diferentes, mas você treina modelo único.
# Renda é bom preditor pra homens (quanto mais, mais aprovado)
# Renda é fraco preditor pra mulheres (aprovação depende de outros fatores)
# Modelo único mistura: "renda tem peso X"
# Subestima importância pra mulheres, superestima pra homens
Como MEDIR viés: fairness metrics
Não é opinião. Mede-se com números. Principais métricas (todas de 0 a 1, sendo 1 = perfeição):
Paridade demográfica (Demographic Parity)
Taxa de aprovação deve ser igual entre grupos.
from sklearn.metrics import confusion_matrix
import numpy as np
# Dataset com 1000 aplicações
# 500 mulheres, 500 homens
y_true = [1, 0, 1, 1, 0, ...] # real (aprovado=1, rejeitado=0)
y_pred = [1, 0, 1, 0, 0, ...] # predição do modelo
genero = ['F', 'M', 'F', 'M', 'F', ...] # característica sensível
# Taxa de aprovação por gênero
approval_rate_f = y_pred[np.array(genero) == 'F'].mean()
approval_rate_m = y_pred[np.array(genero) == 'M'].mean()
print(f"Mulheres aprovadas: {approval_rate_f:.2%}")
print(f"Homens aprovados: {approval_rate_m:.2%}")
# Paridade demográfica = 1 se iguais
parity = min(approval_rate_f, approval_rate_m) / max(approval_rate_f, approval_rate_m)
print(f"Paridade demográfica: {parity:.2%}")
# 0.8 (80%) significa: grupo minoritário tem 80% da taxa do grupo majoritário
# < 0.6 (< 60%) = disparidade significativa
Exemplo:
Mulheres aprovadas: 45%
Homens aprovados: 65%
Paridade: 45% / 65% = 69%
Diagnóstico: modelo aprova homens 1.44x mais que mulheres
(disparidade clara)
Igualdade de oportunidade
Modelos devem ter mesma taxa de falso negativo (FNR) entre grupos. Falso negativo = pessoa que deveria ser aprovada, mas foi rejeitada.
# Verdadeiros positivos (aprovado, era bom) por grupo
tp_f = ((y_true == 1) & (y_pred == 1) & (np.array(genero) == 'F')).sum()
tp_m = ((y_true == 1) & (y_pred == 1) & (np.array(genero) == 'M')).sum()
# Falsos negativos (rejeitado, era bom) por grupo
fn_f = ((y_true == 1) & (y_pred == 0) & (np.array(genero) == 'F')).sum()
fn_m = ((y_true == 1) & (y_pred == 0) & (np.array(genero) == 'M')).sum()
# Taxa de recall (acerto em positivos) = TP / (TP + FN)
recall_f = tp_f / (tp_f + fn_f)
recall_m = tp_m / (tp_m + fn_m)
print(f"Recall mulheres (detecção de bom crédito): {recall_f:.2%}")
print(f"Recall homens: {recall_m:.2%}")
# Igualdade de oportunidade: recalls iguais
opportunity_gap = abs(recall_f - recall_m)
print(f"Gap de oportunidade: {opportunity_gap:.2%}")
Exemplo:
Recall mulheres: 60% (dos bons créditos femininos, 60% aprovados)
Recall homens: 80%
Gap: 20%
Diagnóstico: modelo falha mais em detectar bom crédito em mulheres
(mais falsos negativos = mais mulheres boas rejeitadas)
Paridade preditiva
Probs de sucesso devem ser iguais entre grupos entre pessoas aprovadas. Se modelo aprova mulher, chance dela dar certo deve ser mesma que homem aprovado.
# Dentre aprovados, qual % realmente teve sucesso?
true_positives_f = ((y_true == 1) & (y_pred == 1) & (np.array(genero) == 'F')).sum()
approved_f = (y_pred == 1)[np.array(genero) == 'F'].sum()
precision_f = true_positives_f / approved_f if approved_f > 0 else 0
true_positives_m = ((y_true == 1) & (y_pred == 1) & (np.array(genero) == 'M')).sum()
approved_m = (y_pred == 1)[np.array(genero) == 'M'].sum()
precision_m = true_positives_m / approved_m if approved_m > 0 else 0
print(f"Precisão em mulheres aprovadas: {precision_f:.2%}")
print(f"Precisão em homens aprovados: {precision_m:.2%}")
parity = abs(precision_f - precision_m)
print(f"Diferença de paridade preditiva: {parity:.2%}")
Exemplo:
Dentre mulheres aprovadas: 75% realmente obtém sucesso
Dentre homens aprovados: 82% realmente obtém sucesso
Gap: 7%
Diagnóstico: modelo é mais exigente (inconsistentemente) com mulheres
Problema: métricas conflitam
Você não consegue otimizar simultaneamente paridade demográfica (mesma taxa de aprovação) + igualdade de oportunidade (mesmo recall) + paridade preditiva (mesma precisão).
Exemplo:
Cenário: 60% de candidatos bons em ambos grupos.
Paridade demográfica (mesma taxa aprovação):
Aprova 70% mulheres, 70% homens
→ Mas se 60% são bons, haverá 10% falsos positivos
Igualdade de oportunidade (mesmo recall):
Aprova 60% mulheres boas, 60% homens bons (só bons)
→ Taxa de aprovação é diferente se há viés de rótulo
Paridade preditiva (mesma precisão):
Aprova mulheres com score > 0.75, homens com score > 0.72
→ Taxas de aprovação vão ser diferentes
Você escolhe qual métrica importa pro negócio.
Crédito: igualdade de oportunidade é legalmente mais defensável (LGPD Art. 9). Significa: oportunidade de aprovação é justa, mesmo que taxas finais sejam diferentes.
Contratação: paridade demográfica pode ser requerida (ação afirmativa). Significa: grupos sub-representados têm chances iguais de sendo contratados.
Diagnóstico de doença: paridade preditiva importa mais. Se você aprova teste em mulheres 90% confiável e em homens 85%, tem problema (inconsistência em cuidado).
Corrigindo viés: técnicas práticas
1. Rebalanceamento (resampling)
Aumentar exemplos do grupo sub-representado.
from sklearn.utils import resample
df = pd.read_csv('credit_applications.csv')
# Separa por grupo
df_majority = df[df['genero'] == 'Masculino']
df_minority = df[df['genero'] == 'Feminino']
print(f"Antes: {len(df_majority)} homens, {len(df_minority)} mulheres")
# Resample minority com reposição até igualar
df_minority_upsampled = resample(
df_minority,
n_samples=len(df_majority),
random_state=42,
replace=True
)
df_balanced = pd.concat([df_majority, df_minority_upsampled])
print(f"Depois: {len(df_balanced) / 2} homens, {len(df_balanced) / 2} mulheres")
# Treina em df_balanced
# Resultado: modelo vê 50-50, aprende features de ambos grupos melhor
Tradeoff: se minority class é raro (2% do dataset), upsampling cria dados duplicados. Modelo memoriza.
2. Reponderação (weighting)
Em vez de duplicar, aumenta peso de exemplos minority em loss.
from sklearn.utils.class_weight import compute_sample_weight
X = df[['renda', 'idade', 'score_credit', ...]]
y = df['aprovado']
grupos = df['genero']
# Compute weight: cada grupo raro pesa mais
class_weight = 'balanced' # Sklearn balanceia automaticamente
sample_weight = compute_sample_weight(class_weight, y)
# Mais peso ainda pra minority
for i, group in enumerate(grupos):
if group == 'Feminino':
sample_weight[i] *= 1.5 # Mulheres pesam 50% mais
model = LogisticRegression()
model.fit(X, y, sample_weight=sample_weight)
Tradeoff: não cria dados fake, mas modelo pode underfittar (peso demais em minoria causa overfitting nela).
3. Ajuste de threshold por grupo
Treina modelo igual pra todos, mas threshold de decisão varia.
from sklearn.linear_model import LogisticRegression
# Treina modelo único
model = LogisticRegression()
model.fit(X, y)
# Produz scores (probabilidade, 0 a 1)
scores = model.predict_proba(X)[:, 1]
# Threshold padrão: 0.5
# Mas para igualdade de oportunidade, ajusta por grupo
def predict_fair(scores, genero, target_recall=0.80):
predictions = []
for score, g in zip(scores, genero):
if g == 'Feminino':
threshold = 0.45 # Mais tolerante com mulheres
else:
threshold = 0.50 # Padrão com homens
predictions.append(1 if score > threshold else 0)
return np.array(predictions)
y_pred_fair = predict_fair(scores, grupos)
Tradeoff: muda a semântica de "score de risco". Score 0.48 em mulher significa "aprova", mas 0.48 em homem significa "rejeita", mesmo pessoa. Defensável legalmente? Depende da lei.
4. Pós-processamento (post-processing)
Após treinar, aplica regra corretiva.
# Modelo treina normalmente
model.fit(X, y)
y_pred = model.predict(X)
# Diagnóstico: mulheres aprovadas com recall 60%, homens com 80%
# Pós-processamento: "inverte alguns rejeitados de mulheres"
target_recall = 0.80
# Mulheres rejeitadas com score alto
rejected_women_high_score = (
(y_pred == 0) & (grupos == 'Feminino') & (scores > 0.40)
)
# Aprova 25% delas (a mais pra aumentar recall)
n_to_approve = int(rejected_women_high_score.sum() * 0.25)
indices_to_flip = np.random.choice(
np.where(rejected_women_high_score)[0],
n_to_approve,
replace=False
)
y_pred[indices_to_flip] = 1
# Agora recall mulheres aumentou
Tradeoff: pode aumentar falsos positivos.
Armadilhas comuns
Ignorar contexto empresarial: "paridade demográfica" é impossível de atingir se base tem 60% de homens bons e 40% de mulheres boas. Você vai rejeitando homens bons pra igualar, loss function cresce.
Medir em whole dataset, não em subgrupos: accuracy global 92% mascara que accuracy pra mulher negra é 70%.
# RUIM: acurácia global
accuracy = (y_true == y_pred).mean()
# BOM: acurácia por subgrupo
for genero in df['genero'].unique():
mask = df['genero'] == genero
acc = (y_true[mask] == y_pred[mask]).mean()
print(f"Acurácia {genero}: {acc:.2%}")
Confundir correlação com causalidade: se modelo aprova mais homens, não é culpa de estar "homem", é culpa de dataset ter correlações históricas.
Conexão com LGPD e decisão automatizada
Lei Geral de Proteção de Dados (LGPD Art. 9-10) proíbe decisão exclusivamente automatizada que afete direitos. Crédito é um direito. Você tem que:
- Informar pessoa que decisão foi automatizada
- Oferecer explicação compreensível
- Oferecer direito de contestação (humano revê)
Se modelo tem viés flagrante (gap > 20%), é munição pra processo. Melhor prevenir: meça, corrija, documente.
Quando NÃO corrigir agressivamente
Se corrigir viés piora performance do modelo no geral, trade-off não vale. Exemplo: credit scoring com 5% de acurácia só pra ter paridade demográfica perfeita.
Se dados naturalmente têm diferenças (não viés). Exemplo: taxa de devolução de empréstimo genuinamente é maior pra certo grupo por razões econômicas legítimas (não discriminatórias). Não força paridade artificial.
Se impacto do viés é pequeno (gap < 5%) e população é pequena (< 100 exemplos). Ruído estatístico.
Próximos passos
Estude técnicas avançadas com Engenharia de prompt em produção, que inclui viés em respostas de LLMs.
Implemente observabilidade de fairness com Observabilidade em aplicações com LLM, monitorando métricas por grupo em produção.
Use dados para decidir: RAG, fine-tuning ou prompt? Guia de decisão analisa trade-offs similares em IA.
Comece simples: calcule recall e precisão por grupo mensalmente. Se gap > 10%, invira em correção. Dados falam.