igor-millis~/work/credit-default-api
EN/PT
← todos os projetos · machine learning · serving de modelos

Credit Default API

Estimar a probabilidade de um cliente de cartão ficar inadimplente no próximo mês, escolher o limiar de decisão de propósito e servir tudo como uma API REST validada.

XGBoostscikit-learnSHAPFastAPIPydanticDocker
30.000
clientes · 23 preditoras
22,1%
de inadimplência (desbalanceamento moderado)
0,777
ROC AUC no teste separado
60,1%
dos inadimplentes identificados
dataset
Default of Credit Card Clients (UCI)
tarefa
classificação binária · default no próximo mês
modelo
XGBoost (grid search) + threshold 0,24
serving
FastAPI · POST /predict · Docker
interface
cliente Streamlit via Docker Compose
status
roda localmente · sem deploy público ainda
01 · problema

Acurácia é o alvo errado

Com 77,9% dos clientes adimplentes, um modelo que prevê “não inadimplente” para todos já tem 78% de acurácia — e não serve para nada. Os custos dos dois erros não são simétricos: deixar passar um inadimplente costuma ser pior do que mandar um bom cliente para revisão.

Por isso o projeto avalia precision, recall, F1 e ROC AUC, mantém um conjunto de teste estratificado (6.000 clientes) intocado até o fim e trata o limiar de decisão como uma escolha de modelagem.

02 · método

Um pipeline, quatro famílias de modelos

O pré-processamento fica dentro de um Pipeline do scikit-learn, para que treino e inferência apliquem as mesmas transformações: StandardScaler nos valores, OneHotEncoder em sexo, escolaridade e estado civil, e o histórico ordinal de pagamentos (PAY_0…PAY_6) mantido na escala original. Cada família foi treinada como baseline e com grid search.

modelovarianteacuráciaprecisionrecallF1ROC AUC
Logistic Regressionbaseline0,80950,69660,24570,36320,7100
Logistic Regressiongrid search0,67800,36750,63230,46480,7105
Decision Treetuned0,8173———0,7477
Random Forestbaseline0,8135———0,7546
Random Forestgrid search0,7867———0,7735
XGBoostbaseline0,81830,66260,36400,46980,7731
XGBoostgrid search0,81730,66200,35570,46270,7770

Conjunto de teste, threshold 0,50. Árvore de Decisão e Random Forest têm apenas acurácia e AUC na fonte. Fonte: README.md, notebooks/09_model_selection.ipynb.

melhores hiperparâmetros · 72 combinações · CV ROC AUC 0,7853

colsample_bytree  = 0.8
learning_rate     = 0.05
max_depth         = 7
min_child_weight  = 5
n_estimators      = 100
reg_lambda        = 5
subsample         = 0.8
03 · threshold

Mover o limiar é o ganho de verdade

No corte padrão de 0,50, o grid search quase não mudou o XGBoost (AUC 0,773 → 0,777) e o recall ficou perto de 36%. A ordenação das probabilidades era boa; o gargalo era a regra de decisão. Arraste o threshold:

xgboost (grid search) · teste · grade de 0,05n = 6.000
0,50
precisionrecallF1
0,000,250,500,751,000,10,30,50,70,9τ=0,24
—
precision
—
recall
—
F1
—
cFN/cFP implícito
Curva: notebooks/09_model_selection.ipynb (teste, n = 6.000). O threshold operacional 0,24 foi escolhido maximizando o F1 na validação (F1 = 0,5405) em notebooks/010_model_validation.ipynb. Razão de custos implícita = (1 − τ)/τ, válida só se as probabilidades forem calibradas.

O modelo final usa τ = 0,24: passa a encontrar 60,1% dos inadimplentes em vez de 35,6%, trocando precision (0,66 → 0,49) por recall — uma decisão de negócio explicitada. Por que 0,24 e o que isso implica está no artigo “Um threshold é uma decisão”.

Matriz de confusão final — teste, τ = 0,24

pred 0
pred 1
real 0
3.843verd. neg.
830falso pos.
real 1
530falso neg.
797verd. pos.
métricavalor
acurácia0,7733
precision0,4899
recall0,6006
F10,5396
ROC AUC0,7770
04 · interpretabilidade

O que move a previsão

A análise SHAP e a importância de variáveis do modelo concordam: o histórico recente de pagamentos domina. Só o PAY_0 (status do mês mais recente) concentra cerca de 44% da importância.

PAY_00,436PAY_20,140PAY_30,045PAY_40,038PAY_50,030PAY_60,021SEX_20,019EDUCATION_50,018PAY_AMT20,017LIMIT_BAL0,017
Importância de variáveis do XGBoost, top 10 (colunas one-hot com seu nível). Fonte: models/xgboost_tuned_feature_importance.csv.
05 · serving

Do pickle à API

O pipeline e o threshold são carregados uma vez com joblib. POST /predict recebe as 23 variáveis do cliente, validadas por um schema Pydantic com domínios Literal para os códigos categóricos — SEX = 7 é recusado com HTTP 422 antes de chegar ao modelo.

POST /predict
{
  "LIMIT_BAL": 20000, "SEX": 1, "EDUCATION": 2, "MARRIAGE": 1, "AGE": 24,
  "PAY_0": 2, "PAY_2": 2, "PAY_3": 0, "PAY_4": 0, "PAY_5": 0, "PAY_6": 0,
  "BILL_AMT1": 3913, ... "PAY_AMT6": 0
}

→ 200 OK
{
  "probability": 0.3297,
  "threshold": 0.24,
  "prediction": 1,
  "classification": "inadimplente"
}
  • Testes: pytest + TestClient do FastAPI — entrada válida retorna 200 com probabilidade e threshold em [0, 1]; categoria inválida retorna 422.
  • Docker: imagem Python 3.13 com um requirements-api.txt mínimo; só os artefatos do modelo final são copiados.
  • Compose: um cliente Streamlit conversa com o contêiner da API pela rede interna.
06 · próximos

Limitações e próximos passos

  • Ainda sem deploy público — a API roda localmente ou no Docker.
  • Calibração verificada depois (artigo): os escores são calibrados (ECE ≈ 0,008 no teste), então τ = 0,24 pode ser lido como uma razão de custos de cerca de 3×.
  • Planejado: deploy, integração da interface com a API hospedada e documentação final.