← voltar à série sobre o projeto
📄

Documentação técnica do projeto. A versão narrativa está na série de 12 artigos — comece pelo mapa da série. Para entender curva ROC e limiar na prática, há o explicador interativo. O código, os dados tratados e as saídas estão no repositório no GitHub ↗.

Aurora Crédito Digital · caso de risco de crédito ao consumo

Prevendo inadimplência de 90+ dias em uma carteira de crédito não garantido

Documentação de um projeto conduzido pelas seis fases do CRISP-DM, do entendimento do negócio ao deploy. Cada decisão registrada aqui foi tomada a partir de um número medido na base — nenhuma a partir de boa prática.

Base
150.000 × 11
Alvo
90+ DPD / 24m
Prevalência
6,684%
Custo FN : FP
10 : 1
Meta
ROC AUC ≥ 0,85
FASE 1

Entendimento do negócio

o que a decisão precisa ser, antes de existir modelo

A Aurora é uma fintech de crédito pessoal não garantido para renda baixa e média. Cresceu em volume, mas a margem líquida oscila. A pergunta do comitê não é "quem é bom pagador" — é qual política de aprovação gera mais valor sem assumir risco incompatível com a estratégia.

Três objetivos em conflito: crescimento (manter volume de aprovações), risco (reduzir exposição) e rentabilidade (o árbitro entre os dois). O modelo não resolve o conflito — ele dá a linguagem comum para negociá-lo.

A saída do modelo é uma probabilidade, não uma classe

O modelo estima a probabilidade de inadimplência. A decisão de aprovar ou negar vem depois, aplicando um limiar econômico. Isso torna a calibração um requisito: um modelo que ranqueia bem mas cuja probabilidade não corresponde à realidade invalida qualquer limiar.

A assimetria de custo e o que ela implica

Um falso negativo (aprovar quem dá calote) custa 10× um falso positivo (negar quem pagaria). Duas consequências saem direto dessa razão:

  • O limiar ótimo teórico é p* = 1 / (1 + 10) = 9,09%, não 50%.
  • Acurácia está descartada como critério — ela pesa os dois erros igualmente.
Descoberta que mudou o discurso

Negar todos custa mais caro que aprovar todos: 0,9332 contra 0,6684 por cliente, na escala FN=10 / FP=1. Aritmética direta — o custo esperado de aprovar um cliente aleatório é 10 × 6,68% = 0,668; o de negá-lo é 1 × 93,32% = 0,933.

A assimetria 10:1 não autoriza uma política ultraconservadora. O valor do modelo não está em negar mais, está em negar os certos.

Baselines simulados

A base não tem coluna de decisão histórica, então o custo da política atual não é observável. Foram simulados quatro pontos de referência:

PolíticaFNFPCusto/cliente% negada
Aprovar todos10.02600,66840,00%
Negar todos0139.9740,9332100%
Negar se atrasos 90+ > 06.5544.8660,46945,56%
Negar se qualquer atraso > 03.26423.6010,374920,24%

A última linha é a barra a bater: 0,3749 por cliente.

FASE 2

Entendimento dos dados

o arquivo se chama credito_tratado.csv e não está tratado

O arquivo não abre com leitura padrão

pandas.errors.ParserError: Error tokenizing data.
        C error: Expected 1 fields in line 57579, saw 2

Separador é ;, quebra de linha CRLF. Corrigido isso, apareceu algo pior: duas colunas contínuas chegam como texto.

O achado mais caro do projeto

Exportação em locale pt-BR gravou os valores ≥ 1 com ponto de milhar — 1.046.279.103 é na verdade 1,046279103. São 2.952 registros em uso_limite_rotativo e 3.915 em razao_divida.

O reflexo natural diante de uma coluna-texto é pd.to_numeric(errors='coerce'). Isso apagaria em silêncio 2.950 clientes cuja taxa de inadimplência é 40,07% — 6,7× a média da carteira. Sem mensagem de erro. O modelo perderia o subgrupo mais arriscado da base e ninguém veria.

Um teto artificial em 2,0

Distribuição de razao_divida acima de 1: existem 34.779 registros entre 1 e 2, zero entre 2 e 10, e 358 acima de 10. Distribuição contínua não tem buraco assim. Alguém winsorizou em 2,0 ao produzir o arquivo, e os registros gravados como texto escaparam do clip — 355 dos 358 valores acima do teto são exatamente eles.

Resultado: 31.215 clientes (20,81% da base) empilhados em razao_divida == 2,0, com valor sintético.

Ausência de dado é sinal de risco menor

ColunaNulos%Inadimpl. com dadoInadimpl. sem dadoRazão
renda_mensal29.73119,82%6,9486%5,6137%0,81×
dependentes3.9242,62%6,7410%4,5617%0,68×

Contraintuitivo e decisivo: quem não informou renda inadimple menos. Imputar pela mediana sem sinalizar destruiria essa informação. A ausência também é aninhada — todos os 3.924 sem dependentes estão dentro dos 29.731 sem renda_mensal, nenhum ao contrário. É falha estrutural de coleta, não acaso.

Descartar as linhas com nulo não é opção

Custaria 29.731 clientes (19,82%) e 1.669 inadimplentes — 16,65% de toda a evidência de calote da base. E os descartados são um perfil específico: mediana de idade 57 contra 51, razao_divida 6,76× maior. Além disso, em produção esse cliente vai chegar: uma em cada cinco propostas não traz renda informada.

Outros problemas catalogados

  • Sentinelas 96 e 98 nas colunas de atraso — 269 clientes, sempre nas três colunas ao mesmo tempo (prova de que é marcação de registro, não comportamento), com 54,65% de inadimplência contra 6,60% do restante.
  • Renda máxima de R$ 3.008.750 contra p99 de R$ 25.000 — 120×. E 1.634 clientes com renda exatamente 0, que é ausência disfarçada.
  • 1.573 linhas 100% duplicadas (1,05%). Sem id_cliente, não há como saber se é a mesma pessoa.
  • Caudas impossíveis: um cliente com 54 financiamentos imobiliários (p99 = 4).
  • Sem id_cliente e sem coluna temporal — impede split temporal, que seria o correto para um alvo de janela de 2 anos. Limitação declarada.
FASE 3

Preparação dos dados

uma função, chamada por todo mundo — inclusive pelo app

Split 75/25 estratificado no alvo. A assimetria foi preservada com 0,0018 ponto percentual de diferença entre treino (6,684444%) e teste (6,682667%).

Toda a preparação vive em uma função única, em módulo autocontido que o app Streamlit importa direto. Passar parametros=None faz a função aprender; passar o dicionário do treino faz apenas aplicar.

# os sinalizadores precisam ser criados antes da imputacao
        base["renda_ausente"] = base["renda_mensal"].isna().astype("int8")
        base["dependentes_ausente"] = base["dependentes"].isna().astype("int8")

        base["renda_mensal"] = base["renda_mensal"].fillna(parametros["mediana_renda"])

        # as sentinelas aparecem sempre nas tres colunas ao mesmo tempo
        base["historico_atraso_sentinela"] = (
            base[COLUNAS_ATRASOS].isin(parametros["valores_sentinela"]).any(axis=1).astype("int8"))
        for coluna in COLUNAS_ATRASOS:
            base[coluna] = base[coluna].clip(upper=parametros["limite_atrasos"])

        base["renda_por_dependente"] = base["renda_mensal"] / (base["dependentes"] + 1)
        base["sobra_caixa"] = base["renda_mensal"] * (1 - base["razao_divida"])

Resultado: 112.500 × 17 no treino, zero nulos, zero linhas perdidas. Seis colunas novas.

As colunas novas carregam sinal?

Coluna criadaClientes = 1Inadimpl. grupoInadimpl. restoVeredito
historico_atraso_sentinela20253,4653%6,6003%separa 8×
renda_ausente22.1875,5393%6,9658%sinal real
dependentes_ausente2.9454,4482%6,7446%sinal real
renda_no_teto2256,2222%6,6854%só rastreabilidade

Das duas features contínuas, renda_por_dependente tem gradiente monotônico limpo por quartil — 9,66% / 6,79% / 5,49% / 4,64%. Já sobra_caixa saiu contaminada: 6,62% / 9,47% / 6,44% / 4,20%, com o quartil mais baixo tendo risco menor que o do meio.

Pendência conhecida

O problema de sobra_caixa é herdado. Como ela é renda × (1 − razao_divida), os 20,81% de clientes empilhados no teto artificial de razao_divida caem todos no mesmo ponto negativo. A feature não mede sobra de caixa, mede o artefato do clip. Fica registrada para tratamento por binning.

O pipeline

Três passos: preparo → imputação → modelo. O preparo é uma casca em volta da função acima e não trata nada por conta própria. Como o fit acontece dentro de cada dobra da validação cruzada, o vazamento fica impossível por construção — na prova, a dobra 1 aprendeu mediana de renda 5.386 enquanto as outras aprenderam 5.400.

O SimpleImputer é rede de segurança, não tratamento: hoje recebe zero nulos. Existe para o campo vazio inesperado vindo do app. E todos os passos devolvem DataFrame, para que o SHAP explique razao_divida e não feature 4.

FASE 4

Modelagem

validação cruzada estratificada, 5 dobras, sempre sobre o treino

Métricas: ROC AUC, PR AUC e acurácia. Nenhuma delas significa nada sem o chão ao lado — dado que a prevalência é 6,684%, o chão é ROC AUC 0,5000, PR AUC 0,066844 e acurácia 0,933156.

Primeira rodada: o chão e a árvore sem poda

ModeloROC AUCPR AUCAcuráciaROC AUC treinoGap
Dummy (prior)0,5000000,0668440,9331560,5000000,0000
Decision Tree0,6148170,1231100,8984000,9999970,3852
Terceira vez que a acurácia aponta para o lado errado

A árvore tem acurácia menor que o Dummy — 0,8984 contra 0,9332 — e mesmo assim é o modelo melhor. Antes disso, a mesma inversão já tinha aparecido nas políticas manuais da Fase 2, e no F1, que elege como melhor a política de custo pior.

ROC AUC de 0,999997 no treino contra 0,614817 na validação. A árvore com max_depth=None cresce até isolar cada cliente numa folha própria: não aprendeu o padrão da inadimplência, decorou os 112.500 clientes do treino.

Onde exatamente começa o overfitting

Catorze tamanhos testados, cada um com validação cruzada de 5 dobras.

Dois gráficos lado a lado. À esquerda, ROC AUC de treino e de validação por profundidade máxima da árvore: as duas curvas sobem juntas até profundidade 7, depois a de treino segue até 1,0 enquanto a de validação despenca, e a área entre elas é preenchida em vermelho claro. À direita, zoom na curva de validação com ROC AUC e PR AUC, ambas formando um arco com pico entre 5 e 7.
A tesoura do overfitting. Até a profundidade 7 as duas curvas caminham coladas — a árvore está aprendendo estrutura real. A partir daí elas se separam: o treino continua subindo até 1,0 e a validação desaba. A área vermelha é o que o modelo decorou e não sabe generalizar.
max_depthROC AUC val.PR AUC val.AcuráciaROC AUC treinoGap
10,6567650,1887470,9331560,6567650,000000
20,7771620,2564170,9351110,7785880,001426
30,8044090,2958870,9348000,8061640,001756
40,8277890,3301260,9350670,8286560,000866
50,8434370,3485100,9352980,8455230,002086
60,8454240,3509960,9355470,8511830,005759
70,8458380,3483200,9350130,8577090,011872
80,8406760,3377120,9345420,8626080,021932
100,8111310,3063810,9325780,8765300,065399
120,7658530,2777350,9302670,8935340,127681
150,6839840,2285950,9257240,9264920,242508
200,6098350,1730110,9150220,9786300,368795
300,6175910,1293910,9005870,9997470,382157
sem limite0,6148170,1231100,8984000,9999970,385180
Resultado da varredura

Melhor por ROC AUC: max_depth = 7 → 0,845838. Ganho de +0,231 sobre a árvore sem poda (0,614817), e o gap treino-validação cai de 0,385180 para 0,011872 — uma redução de 97%.

Pela regra de parcimônia de um desvio-padrão, max_depth = 5 já entrega 0,843437, dentro de um desvio do melhor, com metade da complexidade.

Nenhum dos 14 tamanhos atinge a meta contratada de 0,85. A árvore isolada chega perto do teto do que consegue — falta o passo para ensemble e boosting.

A árvore por dentro: três níveis

Uma árvore de profundidade 3, treinada na base completa de treino. Oito folhas, e apenas 3 das 16 features entram nela.

Diagrama de árvore de decisão com três níveis e oito folhas. A raiz testa atrasos_90_mais_dias menor ou igual a 0,5. O ramo esquerdo, com 94,4% dos clientes, testa uso_limite_rotativo e depois atrasos_60_89_dias, terminando em folhas laranja escuro de baixa inadimplência. O ramo direito, com 5,6% dos clientes, volta a testar atrasos_90_mais_dias e termina em duas folhas azuis classificadas como inadimplente.
A primeira pergunta que a árvore faz é sobre atraso de 90+ dias no histórico. Ela separa 94,4% da carteira de um lado e 5,6% do outro, e a inadimplência salta de 4,6% para 41,6% entre os dois ramos. As folhas azuis à direita são as únicas classificadas como inadimplente.
FeatureImportância
atrasos_90_mais_dias0,712731
uso_limite_rotativo0,153212
atrasos_60_89_dias0,134057

Com três níveis a árvore não tem espaço para renda, idade, dependentes ou as features criadas. Ela gasta toda a profundidade disponível em comportamento de pagamento — o que é coerente com o negócio, e mostra por que a explicabilidade por SHAP será necessária: a importância global esconde que o resto das variáveis só aparece em árvores mais profundas.

Random Forest: profundidade contra desbalanceamento

Vinte e quatro configurações — oito profundidades por três tratamentos de class_weight. Com prevalência de 6,684%, balanced daria à classe inadimplente 14× mais peso que à adimplente.

max_depthsem pesobalancedbalanced_subsample
40,8554170,8560140,856008
60,8596840,8590050,858846
80,8615130,8591530,858964
100,8628850,8559580,856160
120,8625470,8505090,850461
150,8605360,8415220,841628
200,8525620,8362170,837266
sem limite0,8378590,8338710,834146
Meta atingida

17 das 24 configurações passam de 0,85. A melhor é max_depth=10 sem class_weight: ROC AUC 0,862885 e PR AUC 0,397012 — 5,9× o acaso. Ganho de +0,017 sobre a melhor árvore isolada e +0,248 sobre a árvore sem poda.

Pela regra de um desvio-padrão, max_depth=6 (0,859684) já basta, com gap treino-validação de 0,0056 contra 0,0322 da profundidade 10.

Três gráficos lado a lado para o Random Forest. À esquerda, ROC AUC por profundidade com três curvas de validação quase sobrepostas até a profundidade 8 e depois separando, mais curvas tracejadas de treino subindo até 1,0. Ao centro, PR AUC por profundidade: a curva sem peso fica acima das duas balanceadas a partir da profundidade 8. À direita, acurácia por profundidade: a curva sem peso fica plana em 0,935 enquanto as balanceadas partem de 0,78 e sobem.
As curvas laranja e verde estão sobrepostas nos três painéisbalanced e balanced_subsample produzem resultados praticamente idênticos. No painel da direita, a acurácia dos modelos balanceados sobe conforme a floresta piora, chegando ao valor do modelo sem peso justamente onde o ROC AUC é o pior de todos.
O class_weight destrói a calibração

Split interno do treino (80/20), max_depth=10. A taxa real de inadimplência nesse recorte é 6,6844%.

class_weightROC AUCBrierProb. médiaProb. medianaNegaria com p* = 9,09%
sem peso0,8667440,0486690,0669680,02294518,80%
balanced0,8611740,1250510,3043030,21809286,97%
balanced_subsample0,8612530,1251710,3040580,21847386,37%

Sem peso, a probabilidade média (0,066968) bate com a taxa real (0,066844) na terceira casa decimal. Com balanced, o modelo prevê 30,4% de inadimplência onde a realidade é 6,7% — e o Brier mais que dobra. A consequência é operacional, não estatística: aplicando o limiar econômico de 9,09% definido na Fase 1, o modelo balanceado negaria 87% da carteira contra 18,8% do modelo sem peso.

É o mesmo raciocínio que já apareceu na Fase 1 por outro caminho: negar demais custa mais caro que aprovar demais. Decisão: Random Forest sem class_weight. O desbalanceamento se trata no limiar, que é onde ele é um problema de decisão — não nos pesos, onde ele vira um problema de probabilidade errada.

O que cada folha significa para a operação

FolhaClientesInadimplentesTaxa× a base
1436525168,77%10,29×
131.9751.02952,10%7,79×
111.00549549,25%7,37×
71.90365334,31%5,13×
102.95184628,67%4,29×
41.72527616,00%2,39×
622.0882.28310,34%1,55×
380.4881.6872,10%0,31×

Oito perguntas separam uma folha com 68,77% de inadimplência de outra com 2,10% — uma diferença de 33×. A folha 3 sozinha carrega 80.488 clientes, 71,5% da carteira, com risco três vezes menor que a média. É esse contraste que uma política de crédito consegue explorar.

Repare que só duas folhas saem rotuladas como inadimplente. As folhas com 52,10% e 49,25% são classificadas como adimplente porque o predict() padrão usa limiar 0,5. Com o limiar econômico de 9,09%, seis das oito folhas seriam negadas. O limiar não é detalhe de implementação — é a decisão de negócio.

Boosting: XGBoost e LightGBM

Busca aleatória com 25 sorteios por modelo, mesmas cinco dobras, scoring='roc_auc' e random_state=42. O pipeline inteiro entra na busca, então cada candidato refaz a preparação por dobra.

ModeloPadrãoAjustadoGanhoGap antesGap depois
XGBoost0,8511630,864800+0,0136370,0771530,014923
LightGBM0,8615760,865079+0,0035040,0406580,012633

Os dois convergiram para o mesmo desenho, por caminhos diferentes: taxa de aprendizado baixa (~0,018), muitas árvores e regularização forte. O LightGBM conteve a complexidade com num_leaves=15; o XGBoost, com min_child_weight=50. Nos dois casos a busca ganhou menos em desempenho do que em estabilidade — o gap treino-validação do XGBoost caiu de 0,077 para 0,015.

O desbalanceamento repete o padrão pela terceira vez

Taxa real de inadimplência no recorte de calibração: 0,066844. Sem ajuste, os dois modelos acertam a probabilidade média na terceira casa decimal. Com scale_pos_weight ou is_unbalance, o Brier quase triplica e o limiar econômico passaria a negar quase 80% da carteira.

ModeloAjusteROC AUCAcuráciaBrierProb. médiaNegaria com p* = 9,09%
XGBoostsem ajuste0,8648000,9374580,0482920,06720218,72%
XGBoostscale_pos_weight = 13,960,8625430,8160440,1315690,30207677,64%
LightGBMsem ajuste0,8650790,9372090,0482780,06718518,71%
LightGBMis_unbalance = True0,8645820,7956180,1420280,31689778,44%
LightGBMclass_weight = balanced0,8645280,7989240,1417570,31842680,47%

Comparação final dos cinco candidatos

Mesmas dobras para todos, cada um na melhor configuração encontrada.

ModeloROC AUCPR AUCAcuráciaPR AUC / acasoGapMeta 0,85
Dummy (prior)0,5000000,0668440,9331561,000×0,0000não
Decision Tree (d=7)0,8458380,3483200,9350135,211×0,0119não
Random Forest (d=10)0,8628850,3970120,9371205,939×0,0322sim
XGBoost0,8648000,4028210,9374586,026×0,0149sim
LightGBM0,8650790,4025280,9372096,022×0,0126sim
Três painéis. À esquerda, barras horizontais de ROC AUC por modelo com barras de erro: Dummy em 0,5000, Decision Tree 0,8458, Random Forest 0,8629, XGBoost 0,8648 e LightGBM 0,8651 destacado em verde, com a linha da meta em 0,85. Ao centro, o mesmo em PR AUC, do Dummy em 0,0668 até LightGBM e XGBoost em torno de 0,402. À direita, ROC AUC dobra a dobra: as três curvas de Random Forest, XGBoost e LightGBM caminham praticamente sobrepostas, muito acima da Decision Tree.
No painel da direita, as três curvas superiores sobem e descem juntas. Todas caem na dobra 3 e sobem na dobra 4 — o que varia entre elas é menor que o que varia entre as dobras. A dobra, não o modelo, é a maior fonte de variação nesta altura.
Empate técnico no topo

LightGBM e XGBoost estão separados por 0,00028 de ROC AUC, contra um desvio de 0,0034 entre dobras. O XGBoost venceu em uma das cinco dobras e lidera em PR AUC (0,402821) e em acurácia (0,937458).

Declarar vencedor por essa margem seria escolher ruído. O desempate real é o custo da carteira sob o limiar econômico, e isso é Fase 5. Por ora o LightGBM segue como candidato principal — com o XGBoost mantido como alternativa viva, não descartado.

FASE 5

Avaliação

37.500 clientes nunca vistos — a leitura que vale

A base de teste foi separada na Fase 3 e não foi tocada desde então: nenhuma mediana, nenhum hiperparâmetro, nenhuma escolha de modelo passou por ela. Cada candidato foi treinado no conjunto completo de treino e avaliado uma única vez aqui. Depois desta leitura o teste está gasto — usá-lo de novo para escolher qualquer coisa o transformaria em um segundo conjunto de validação.

De onde sai o desvio padrão num teste único

Na validação cruzada o desvio vinha das cinco dobras. Aqui há uma leitura por modelo — não existe desvio entre dobras. O desvio vem de bootstrap: 2.000 reamostragens do teste com reposição, recalculando a métrica em cada uma.

O detalhe que faz a comparação funcionar: as mesmas reamostras valem para todos os modelos. Assim a diferença entre dois modelos é medida cliente a cliente, e não como a diferença de duas médias independentes.

ModeloROC AUCDesvioIC 95%PR AUCAcuráciaBrier
Dummy (prior)0,5000000,0000000,0668270,9331730,062361
Decision Tree0,8484060,0041720,840424 – 0,8566740,3490510,9349870,050820
Random Forest0,8658080,0037690,858411 – 0,8731130,4046270,9370130,048831
XGBoost0,8692500,0036790,861951 – 0,8763740,4090900,9377600,048514
LightGBM0,8691710,0036880,862037 – 0,8764160,4101060,9383730,048472
Três painéis da avaliação no teste. À esquerda, barras horizontais de ROC AUC com intervalos de confiança: Dummy 0,5000, Decision Tree 0,8484, Random Forest 0,8658, LightGBM 0,8692 e XGBoost 0,8692 destacado. Ao centro, barras pareadas comparando validação cruzada e teste para cada modelo, com o teste sempre ligeiramente maior. À direita, dois histogramas da diferença pareada de ROC AUC contra o XGBoost: o do Random Forest está inteiramente à direita do zero, e o do LightGBM está centrado no zero e o cruza.
O painel da direita resolve a pergunta. O histograma da diferença contra o Random Forest não encosta no zero — a vantagem é real. O histograma contra o LightGBM está centrado no zero e o atravessa dos dois lados — não há vantagem a declarar.

O teste confirmou a validação cruzada

ModeloValidação cruzadaTesteDiferença
Decision Tree0,8458380,848406+0,002568
Random Forest0,8628850,865808+0,002923
XGBoost0,8648000,869250+0,004450
LightGBM0,8650790,869171+0,004092

Os quatro ficaram acima da validação cruzada. Nenhuma degradação: o protocolo da Fase 3 — preparação aprendida dentro de cada dobra, teste isolado desde o início — segurou. A diferença positiva soma dois efeitos esperados: o modelo final treina com 100% do treino, contra 80% em cada dobra, e este recorte de teste saiu marginalmente mais fácil que a média das dobras.

Existe diferença real entre os modelos?

Diferença pareada contra o XGBoost, que teve o maior ROC AUC. Se o intervalo de confiança cruza o zero, a vantagem não se distingue de ruído.

ConcorrenteDiferença médiaDesvioIC 95%XGBoost venceu emDiferença real?
Dummy (prior)0,3693500,0036790,361951 – 0,376374100,0%sim
Decision Tree0,0208540,0019560,016998 – 0,024608100,0%sim
Random Forest0,0034410,0007860,001916 – 0,004937100,0%sim
LightGBM0,0000620,000349−0,000625 – 0,00074156,6%não
Duas conclusões, uma de cada sinal

A vantagem do boosting sobre o Random Forest é real. O intervalo vai de 0,0019 a 0,0049 sem tocar o zero, e o XGBoost venceu em 2.000 de 2.000 reamostras. Pequena, mas não é sorte.

A vantagem do XGBoost sobre o LightGBM não é real. O intervalo cruza o zero e o XGBoost venceu em 56,6% das reamostras — quase um cara ou coroa. O empate que a validação cruzada sugeriu está confirmado em dado novo.

Calibração em dados nunca vistos

ModeloBrierProb. médiaTaxa realErroNegaria com p* = 9,09%
Decision Tree0,0508200,0668110,066827−0,00001621,52%
Random Forest0,0488310,0663430,066827−0,00048418,49%
XGBoost0,0485140,0665030,066827−0,00032418,20%
LightGBM0,0484720,0665630,066827−0,00026318,14%

A decisão de não usar peso de classe se confirma da forma mais direta possível: os quatro erram a probabilidade média por menos de 0,0005 em dados que nunca viram. O limiar econômico de 9,09% definido na Fase 1 é aplicável como está — negaria cerca de 18% da carteira.

Modelo recomendado

LightGBM. Lidera em PR AUC (0,410106), acurácia (0,938373) e Brier (0,048472); perde em ROC AUC por 0,000062 — diferença que o bootstrap mostra ser ruído. Quando o critério principal empata, decidem os secundários.

O XGBoost permanece como alternativa equivalente. Trocar um pelo outro não muda o resultado de forma mensurável.

ESTADO

Onde o projeto está

e o que vem em seguida

1 · Negócio 2 · Dados 3 · Preparação 4 · Modelagem 5 · Avaliação 6 · Deploy

Os cinco candidatos foram testados. A árvore isolada parou em 0,8458, abaixo da meta; o Random Forest atravessou a linha com 0,8629; e o boosting fechou em 0,8651 (LightGBM) e 0,8648 (XGBoost) — empate técnico entre os dois.

Vale registrar a escala do que sobrou: entre a árvore podada e o melhor boosting há 0,019 de ROC AUC. Entre a árvore sem poda e a árvore podada havia 0,231. A escolha do algoritmo importou dez vezes menos que a decisão de podar — e as duas juntas importam menos que os problemas de dados encontrados na Fase 2.

O teste foi aberto e confirmou tudo: 0,8692 de ROC AUC contra a meta contratada de 0,85, com calibração intacta. O que falta da Fase 5 é traduzir isso em decisão: varrer o limiar pelo custo da carteira — não pela acurácia — e produzir a explicabilidade individual por SHAP, que é o que permite dizer a um cliente e a um regulador por que aquele crédito foi negado.

O teste, a partir de agora, está gasto. Qualquer ajuste que venha a ser feito olhando para ele deixa de ser avaliação e vira seleção.