Até aqui, o desbalanceamento foi tratado como um problema de contagem: uma classe tem poucos exemplos, então o modelo aprende a ignorá-la. Mas o desbalanceamento quase sempre vem acompanhado de um problema mais fundamental, e é esse que realmente importa: os dois tipos de erro não custam a mesma coisa.

Deixar uma fraude passar custa o valor da transação. Bloquear um cliente legítimo custa o atrito, o atendimento e a chance de perdê-lo. Não diagnosticar uma doença grave custa uma vida; um falso alarme custa um exame a mais. Tratar esses pares como equivalentes — que é exatamente o que a acurácia faz — é o erro conceitual mais caro em aprendizado supervisionado aplicado.

A aprendizagem sensível ao custo troca a pergunta "quantos acertos?" por "qual o prejuízo esperado?".

Formular o problema em termos de matriz de custos, aplicar as três estratégias de incorporação de custo (pesos, limiar ótimo e reamostragem), calcular o limiar que minimiza o prejuízo esperado, e avaliar modelos por custo em vez de por métricas de contagem.

A matriz de custos

A matriz de confusão conta ocorrências. A matriz de custos atribui valor a cada célula:

Predito negativo Predito positivo
Real negativo0C_FP
Real positivoC_FN0

Numa detecção de fraude com ticket médio de R$ 800 e custo de atrito de R$ 20 por bloqueio indevido:

C_FN = 800   # fraude que passou: perdemos o valor
C_FP =  20   # cliente legítimo bloqueado: atrito e atendimento

def custo_total(y_real, y_pred, c_fn=C_FN, c_fp=C_FP):
    fn = ((y_real == 1) & (y_pred == 0)).sum()
    fp = ((y_real == 0) & (y_pred == 1)).sum()
    return fn * c_fn + fp * c_fp

A razão C_FN / C_FP = 40 é a informação central: um falso negativo custa 40 falsos positivos. Ela determina tudo o que vem depois.

Levantar esses números é trabalho de negócio, não de modelagem, e costuma ser a parte mais difícil — mas mesmo uma estimativa grosseira da ordem de grandeza é infinitamente melhor que a suposição implícita de custos iguais, que é o que a acurácia assume.

Três formas de incorporar custo

Pesos na função de perda

A mais direta: dizer ao algoritmo que errar um positivo dói mais.

from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier

pesos = {0: 1.0, 1: C_FN / C_FP}     # {0: 1, 1: 40}

modelo = LogisticRegression(class_weight=pesos, max_iter=2000)
floresta = RandomForestClassifier(class_weight=pesos, n_estimators=400)

O atalho class_weight="balanced" define os pesos pela frequência inversa das classes. É um padrão razoável quando você não conhece os custos — mas note que ele responde a uma pergunta diferente: equilibra a representação, não o prejuízo. Se você tem os custos, use-os.

Em R, com tidymodels, o mesmo se expressa por pesos de caso:

library(tidymodels)

dados <- dados |>
    mutate(peso = importance_weights(ifelse(fraude == "sim", 40, 1)))

ajuste <- workflow() |>
    add_recipe(receita) |>
    add_model(logistic_reg()) |>
    add_case_weights(peso) |>
    fit(treino)

Limiar ótimo: a estratégia mais subestimada

Quase todo classificador produz uma probabilidade, e o corte em 0,5 é uma convenção — não um resultado. Mover o limiar é a forma mais barata e mais eficaz de incorporar custo, porque não exige retreinar nada.

A teoria dá o ponto exato. Classificar como positivo compensa quando o prejuízo esperado de fazê-lo é menor que o de não fazer:

$$p \cdot 0 + (1-p) \cdot C_{FP} < p \cdot C_{FN} + (1-p) \cdot 0$$

Resolvendo:

$$p^* = \frac{C_{FP}}{C_{FP} + C_{FN}}$$

Com os números do exemplo: 20 / (20 + 800) ≈ 0,024. Ou seja, vale bloquear a transação quando a probabilidade de fraude passa de 2,4% — muito longe dos 50% do padrão.

import numpy as np

limiar_teorico = C_FP / (C_FP + C_FN)

# e o empírico, que respeita a calibração real do modelo
def limiar_otimo(y_real, prob, c_fn=C_FN, c_fp=C_FP):
    candidatos = np.unique(prob)
    custos = [custo_total(y_real, (prob >= t).astype(int), c_fn, c_fp) for t in candidatos]
    return candidatos[int(np.argmin(custos))]

t = limiar_otimo(y_val, modelo.predict_proba(X_val)[:, 1])

A fórmula teórica vale se o modelo estiver calibrado — se, quando ele diz 0,3, a frequência real for mesmo 30%. Muitos modelos não estão (florestas e SVMs notoriamente). Por isso, calcule também o limiar empírico num conjunto de validação e compare: uma divergência grande entre os dois é um sinal de que vale calibrar o modelo antes.

Regra que evita vazamento: o limiar é um hiperparâmetro. Escolha-o em validação, avalie-o no teste. Escolher o limiar no teste e reportar o custo desse mesmo teste é auto-engano.

Reamostragem proporcional ao custo

Replicar exemplos da classe cara, na proporção do custo, aproxima o efeito dos pesos. É a alternativa para algoritmos que não aceitam class_weight. Tem duas desvantagens frente aos pesos: aumenta o tempo de treino e, com duplicação simples, favorece o sobreajuste nos exemplos replicados. Prefira pesos quando disponíveis.

Custos que variam por exemplo

O caso realista é mais incômodo: numa fraude, C_FN é o valor da transação. Deixar passar uma de R$ 50 não é o mesmo que deixar passar uma de R$ 12.000.

Com custo por exemplo, o limiar deixa de ser único e passa a ser calculado por transação:

def decidir(prob, valor, c_fp=20.0):
    """Bloqueia quando o prejuízo esperado de deixar passar supera o do atrito."""
    limiar = c_fp / (c_fp + valor)
    return prob >= limiar

Uma transação de R$ 12.000 tem limiar de 0,17%; uma de R$ 50, de 28,6%. O mesmo modelo, a mesma probabilidade — decisões opostas, e ambas corretas.

Esse é o ponto em que a separação entre modelo e decisão deixa de ser elegância arquitetural e vira necessidade. O modelo estima probabilidade; a política de decisão combina essa probabilidade com o contexto econômico de cada caso. Misturar as duas coisas num classificador que cospe 0 ou 1 joga fora a informação que torna a decisão boa.

Avaliar por custo

Se a decisão é por custo, a avaliação também precisa ser — e a comparação entre modelos muda de cara:

from sklearn.model_selection import StratifiedKFold
import numpy as np

def custo_medio_cv(modelo, X, y, valores, c_fp=20.0, folds=5):
    cv = StratifiedKFold(n_splits=folds, shuffle=True, random_state=42)
    custos = []
    for treino_idx, teste_idx in cv.split(X, y):
        modelo.fit(X[treino_idx], y[treino_idx])
        prob = modelo.predict_proba(X[teste_idx])[:, 1]

        limiar = c_fp / (c_fp + valores[teste_idx])
        pred = (prob >= limiar).astype(int)

        fn = ((y[teste_idx] == 1) & (pred == 0))
        fp = ((y[teste_idx] == 0) & (pred == 1))
        custos.append((valores[teste_idx][fn].sum()) + fp.sum() * c_fp)
    return np.mean(custos), np.std(custos)

É comum e saudável que o modelo com melhor AUC não seja o de menor custo. AUC resume o desempenho em todos os limiares; o custo avalia o desempenho no limiar que você vai usar, com os pesos que importam. Quando os dois discordam, o custo ganha.

Vale sempre reportar junto a linha de base trivial: qual o custo de não ter modelo nenhum (aprovar tudo, ou bloquear tudo). Um modelo que reduz o prejuízo em 3% frente a "aprovar tudo" pode não pagar o próprio custo de manutenção — e isso precisa aparecer no relatório.

Erros frequentes

Otimizar F1 quando existe matriz de custos. F1 assume implicitamente que precisão e revocação valem o mesmo. Se você sabe que um erro custa 40 vezes o outro, F1 está respondendo a outra pergunta.

Aplicar class_weight="balanced" achando que resolve o custo. Ele equilibra frequência, não prejuízo. Numa base com 1% de positivos e razão de custo 40:1, a coincidência entre os dois é acidental.

Reportar acurácia depois de todo o trabalho. Um modelo sensível a custo vai ter acurácia pior que o ingênuo — ele troca muitos falsos positivos baratos por poucos falsos negativos caros, de propósito. Reportar acurácia desfaz a narrativa e confunde quem decide.

O desbalanceamento raramente é só de contagem: os erros têm custos diferentes, e a razão C_FN / C_FP é a informação que organiza tudo o mais. Há três estratégias — pesos na perda, limiar ótimo e reamostragem proporcional —, e as duas primeiras são preferíveis; a reamostragem fica para quando o algoritmo não aceita peso. O limiar que minimiza o prejuízo é p* = C_FP / (C_FP + C_FN): com custos de 20 e 800, dá 0,024, não 0,5. Mover o limiar é a intervenção mais barata que existe, porque não exige retreinar nada. A fórmula supõe um modelo calibrado, então calcule também o limiar empírico em validação — divergência grande é sinal de que vale calibrar. Limiar é hiperparâmetro: escolhe-se em validação e avalia-se no teste. Quando o custo varia por exemplo, como o valor de cada transação, ele passa a ser calculado caso a caso, e aí é preciso separar a estimativa de probabilidade da política de decisão. Avalie por custo, não por AUC ou F1, e compare sempre com a linha de base trivial. Por fim, saiba de antemão: o modelo sensível a custo terá acurácia pior, de propósito — reportar acurácia desfaz o próprio argumento.

Exercícios

  1. Num diagnóstico, C_FN = 5000 (doença não detectada) e C_FP = 50 (exame adicional). Calcule o limiar ótimo e interprete.

    ✓ Resposta:

    p* = C_FP / (C_FP + C_FN) = 50 / (50 + 5000) = 50 / 5050 ≈ 0,0099, ou seja, cerca de 1%. A interpretação: vale encaminhar para investigação sempre que a probabilidade estimada de doença passar de 1%, porque o prejuízo esperado de investigar (R$ 50 com 99% de chance de ser desnecessário) continua menor que o de não investigar (R$ 5.000 com 1% de chance). Isso gera muitos falsos positivos — e essa é a decisão correta dada a assimetria. O limiar padrão de 0,5 é uma escolha implícita de que os dois erros custam o mesmo, o que aqui é claramente falso.

  2. Por que class_weight="balanced" não substitui a matriz de custos?

    ✓ Resposta:

    Porque ele responde a outra pergunta. O "balanced" define os pesos pela frequência inversa das classes — ele corrige a sub-representação no treino, um problema de amostragem. A matriz de custos descreve a assimetria econômica dos erros, que é independente da frequência. Uma classe rara pode ter erro barato (um evento incomum e inofensivo) e uma classe comum pode ter erro caro. Os dois só coincidem por acaso. Quando os custos são conhecidos, passe-os explicitamente ({0: 1, 1: C_FN/C_FP}); o "balanced" é o padrão razoável apenas na ausência dessa informação.

  3. Explique por que o limiar deve ser escolhido em validação e não no conjunto de teste.

    ✓ Resposta:

    Porque o limiar é um hiperparâmetro — um valor ajustado a partir dos dados. Escolhê-lo varrendo o conjunto de teste em busca do menor custo e depois reportar esse custo é selecionar o melhor resultado no mesmo conjunto usado para medir, o que produz uma estimativa otimista que não se sustenta em produção. É a mesma forma de vazamento que ocorre ao escolher hiperparâmetros no teste. O procedimento correto é: treinar no treino, escolher o limiar na validação (ou por validação cruzada dentro do treino), e aplicar esse limiar fixo ao teste, reportando o custo obtido ali.

  4. Num cenário em que C_FN é o valor da transação, por que não existe um limiar único?

    ✓ Resposta:

    Porque o limiar ótimo depende do custo, e o custo varia por exemplo: limiar = C_FP / (C_FP + valor). Com C_FP = 20, uma transação de R$ 12.000 tem limiar ≈ 0,0017 e uma de R$ 50 tem limiar ≈ 0,286 — quase 170 vezes maior. O mesmo modelo, diante da mesma probabilidade estimada de 5%, bloqueia a primeira e aprova a segunda, e ambas as decisões são corretas. Isso força a separação entre o modelo (que estima probabilidade) e a política de decisão (que combina probabilidade com o contexto econômico do caso). Um classificador que devolve apenas 0 ou 1 descarta exatamente a informação necessária para essa distinção.

  5. Seu modelo sensível a custo tem acurácia de 91% contra 99,2% do ingênuo. Como apresentar isso à diretoria?

    ✓ Resposta:

    Não apresentando acurácia. Numa base com 1% de positivos, o modelo ingênuo que prevê "tudo negativo" atinge 99% de acurácia sem detectar uma única fraude — a métrica é vazia nesse contexto. A apresentação correta é em dinheiro: prejuízo esperado por mês com o modelo atual, com o novo, e com a linha de base trivial de não fazer nada. Complementa-se com o que a diretoria consegue acionar: quantas fraudes a mais são detectadas, quantos clientes legítimos a mais são incomodados, e qual o saldo líquido. A queda de acurácia é uma consequência desejada do desenho — trocar muitos falsos positivos baratos por poucos falsos negativos caros — e deve ser explicada nesses termos, não escondida.

Fontes e leituras recomendadas