Regressão Logística
Objetivos de Aprendizagem
- ✅ Treinar primeiro modelo
- ✅ Entender processo de modelagem
- ✅ Estabelecer baseline
# Carregar dados
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, roc_auc_score, roc_curve, confusion_matrix
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')
# Carregar dataset com features de engenharia
df = pd.read_csv("credit_risk_features_engineered.csv")
print(f"✅ Dataset carregado: {len(df):,} registros × {df.shape[1]} features")
✅ Dataset carregado: 1,148,198 registros × 109 features
# Explorar features disponíveis
print("🔍 EXPLORAÇÃO DAS FEATURES DISPONÍVEIS")
print("=" * 60)
print(f"\n📊 Informações gerais:")
print(f" - Total de features: {df.shape[1]}")
print(f" - Total de registros: {len(df):,}")
# Separar todas as colunas (exceto target)
all_cols = [col for col in df.columns if col != 'default']
numeric_cols = df.select_dtypes(include=[np.number]).columns.tolist()
categorical_cols = df.select_dtypes(include=['object']).columns.tolist()
# Remover target das numéricas
if 'default' in numeric_cols:
numeric_cols.remove('default')
print(f"\n📊 Tipos de features:")
print(f" - Numéricas: {len(numeric_cols)}")
print(f" - Categóricas: {len(categorical_cols)}")
print(f" - Target: 1 (default)")
# Identificar features originais, derivadas e one-hot
features_originais = ["idade", "renda", "score_credito", "tempo_emprego", "valor_emprestimo",
"prazo_emprestimo", "tipo_residencia"]
features_derivadas = ["razao_emprestimo_renda", "comprometimento_mensal", "e_jovem", "score_normalizado"]
# Features one-hot (começam com genero_, estado_civil_, tipo_emprego_, regiao_)
features_onehot = [col for col in all_cols if any(col.startswith(prefix) for prefix in
["genero_", "estado_civil_", "tipo_emprego_", "regiao_"])]
# Features que não se encaixam nas categorias acima
features_outras = [col for col in all_cols if col not in features_originais
and col not in features_derivadas and col not in features_onehot]
print(f"\n📋 Categorização das features:")
print(f" - Features originais: {len([f for f in features_originais if f in all_cols])}")
print(f" - Features derivadas: {len([f for f in features_derivadas if f in all_cols])}")
print(f" - Features One-Hot Encoding: {len(features_onehot)}")
print(f" - Outras features: {len(features_outras)}")
# Mostrar exemplos de cada categoria
print(f"\n📋 Exemplos de features originais:")
for col in features_originais[:5]:
if col in all_cols:
print(f" ✅ {col}")
print(f"\n📋 Features derivadas:")
for col in features_derivadas:
if col in all_cols:
print(f" ✅ {col}")
print(f"\n📋 Exemplos de features One-Hot ({len(features_onehot)} no total):")
for col in features_onehot[:10]:
print(f" ✅ {col}")
if len(features_onehot) > 10:
print(f" ... e mais {len(features_onehot) - 10} features One-Hot")
# Verificar variável target
if 'default' in df.columns:
print(f"\n✅ Variável target encontrada: 'default'")
print(f" - Taxa de default: {df['default'].mean():.2%}")
print(f" - Total de defaults: {df['default'].sum():,}")
else:
print(f"\n⚠️ Variável target 'default' não encontrada!")
🔍 EXPLORAÇÃO DAS FEATURES DISPONÍVEIS ============================================================ 📊 Informações gerais: - Total de features: 109 - Total de registros: 1,148,198 📊 Tipos de features: - Numéricas: 12 - Categóricas: 1 - Target: 1 (default) 📋 Categorização das features: - Features originais: 6 - Features derivadas: 4 - Features One-Hot Encoding: 95 - Outras features: 3 📋 Exemplos de features originais: ✅ idade ✅ renda ✅ score_credito ✅ tempo_emprego ✅ valor_emprestimo 📋 Features derivadas: ✅ razao_emprestimo_renda ✅ comprometimento_mensal ✅ e_jovem ✅ score_normalizado 📋 Exemplos de features One-Hot (95 no total): ✅ genero_m ✅ genero_outro ✅ estado_civil_divorciado ✅ estado_civil_solteiro ✅ estado_civil_viuvo ✅ tipo_emprego_CLT ✅ tipo_emprego_DESEMPREGADO ✅ tipo_emprego_PUBLICO ✅ tipo_emprego_TEMPORARIO ✅ tipo_emprego_autonomo ... e mais 85 features One-Hot ✅ Variável target encontrada: 'default' - Taxa de default: 2.96% - Total de defaults: 33,959
# Visualizar a base de dados
print("👀 VISUALIZAÇÃO DA BASE DE DADOS")
print("=" * 60)
print(f"\n📊 Primeiras 10 linhas do dataset:")
print(df.head())
print(f"\n📊 Informações sobre as colunas:")
print(f" - Total de colunas: {df.shape[1]}")
print(f" - Total de linhas: {len(df):,}")
# Mostrar estatísticas descritivas básicas
print(f"\n📊 Estatísticas descritivas (primeiras features numéricas):")
numeric_cols_basic = ["idade", "renda", "score_credito", "tempo_emprego", "valor_emprestimo", "prazo_emprestimo"]
cols_disponiveis = [col for col in numeric_cols_basic if col in df.columns]
if cols_disponiveis:
print(df[cols_disponiveis].describe())
👀 VISUALIZAÇÃO DA BASE DE DADOS
============================================================
📊 Primeiras 10 linhas do dataset:
idade renda valor_emprestimo prazo_emprestimo score_credito \
0 38.656605 3546.090286 5860.251363 12 880.895391
1 22.520191 1999.150798 1076.385141 48 852.732850
2 44.005414 2506.186377 1938.856321 24 877.591430
3 46.286777 1328.870306 1126.484639 24 702.932996
4 18.000000 1580.062114 1653.015650 12 785.216550
historico_inadimplencia tempo_emprego data_emprestimo prob_default \
0 2 47.471173 2022-08-25 0.005607
1 3 25.002385 2023-05-18 0.002411
2 2 1.574208 2022-11-12 0.000867
3 0 16.645324 2022-03-15 0.003095
4 2 18.335196 2022-12-23 0.008084
default ... regiao_sudestx regiao_sudesxe regiao_sudexte \
0 0 ... False False False
1 0 ... False False False
2 0 ... False False False
3 0 ... False False False
4 0 ... False False False
regiao_sudxste regiao_sul regiao_sul regiao_sux regiao_suxeste \
0 False False False False False
1 False False False False False
2 False True False False False
3 False False False False False
4 False False False False False
regiao_sxdeste regiao_sxl
0 False False
1 False False
2 False False
3 False False
4 False False
[5 rows x 109 columns]
📊 Informações sobre as colunas:
- Total de colunas: 109
- Total de linhas: 1,148,198
📊 Estatísticas descritivas (primeiras features numéricas):
idade renda score_credito tempo_emprego \
count 1.148198e+06 1.148198e+06 1.148198e+06 1.148198e+06
mean 3.555684e+01 3.360433e+03 6.998493e+02 2.183346e+01
std 1.168154e+01 1.726994e+03 1.419418e+02 1.822587e+01
min 1.000000e+00 8.000000e+02 3.534970e+02 2.654768e-01
25% 2.692719e+01 2.106265e+03 5.994236e+02 7.828929e+00
50% 3.501008e+01 2.980650e+03 7.243504e+02 1.664532e+01
75% 4.314182e+01 4.219155e+03 8.199420e+02 3.073358e+01
max 1.210000e+02 7.388491e+03 8.975794e+02 6.509055e+01
valor_emprestimo prazo_emprestimo
count 1.148198e+06 1.148198e+06
mean 3.911843e+03 3.255886e+01
std 2.704691e+03 6.878449e+01
min 5.000000e+02 -3.600000e+02
25% 1.818060e+03 1.200000e+01
50% 3.163847e+03 2.400000e+01
75% 5.306037e+03 3.600000e+01
max 1.053800e+04 7.200000e+03
# Selecionar features para o modelo baseline
print("🔧 SELEÇÃO DE FEATURES PARA MODELO BASELINE")
print("=" * 60)
# Para o modelo baseline, vamos usar features básicas e importantes
feature_cols = ["renda", "tempo_emprego", "valor_emprestimo", "prazo_emprestimo"]
# Verificar quais features estão disponíveis
features_disponiveis = [col for col in feature_cols if col in df.columns]
features_faltando = [col for col in feature_cols if col not in df.columns]
print(f"\n📋 Features selecionadas para o modelo baseline:")
for col in features_disponiveis:
print(f" ✅ {col}")
if features_faltando:
print(f"\n⚠️ Features não encontradas: {features_faltando}")
# Preparar dados
X = df[features_disponiveis].copy()
y = df["default"].copy()
# Remover valores ausentes
X_limpo = X.dropna()
y_limpo = y[X_limpo.index]
print(f"\n✅ Dados preparados:")
print(f" - Features selecionadas: {len(features_disponiveis)}")
print(f" - Registros válidos: {len(X_limpo):,}")
print(f" - Taxa de default: {y_limpo.mean():.2%}")
🔧 SELEÇÃO DE FEATURES PARA MODELO BASELINE ============================================================ 📋 Features selecionadas para o modelo baseline: ✅ renda ✅ tempo_emprego ✅ valor_emprestimo ✅ prazo_emprestimo ✅ Dados preparados: - Features selecionadas: 4 - Registros válidos: 1,148,198 - Taxa de default: 2.96%
# Dividir em treino e teste
X_train, X_test, y_train, y_test = train_test_split(
X_limpo, y_limpo, test_size=0.2, random_state=42, stratify=y_limpo
)
print(f"📊 Treino: {X_train.shape[0]:,} amostras")
print(f"📊 Teste: {X_test.shape[0]:,} amostras")
print(f"📊 Taxa default treino: {y_train.mean():.2%}")
print(f"📊 Taxa default teste: {y_test.mean():.2%}")
📊 Treino: 918,558 amostras 📊 Teste: 229,640 amostras 📊 Taxa default treino: 2.96% 📊 Taxa default teste: 2.96%
# Treinar modelo
modelo = LogisticRegression(random_state=42, max_iter=1000)
modelo.fit(X_train, y_train)
print("✅ Modelo treinado!")
# Calcular p-valores usando statsmodels
import statsmodels.api as sm
# Preparar dados para statsmodels (adiciona intercepto)
X_train_sm = sm.add_constant(X_train)
logit_model = sm.Logit(y_train, X_train_sm)
result = logit_model.fit(disp=0)
# Obter p-valores
p_values = result.pvalues[1:] # Remove o intercepto
# Calcular importância relativa (valor absoluto normalizado)
coef_abs = np.abs(modelo.coef_[0])
importancia_relativa = (coef_abs / coef_abs.sum()) * 100
# Calcular Odds Ratio (exponencial dos coeficientes)
odds_ratio = np.exp(modelo.coef_[0])
# Mostrar informações do modelo
print("\n📊 Informações Detalhadas do Modelo:")
print("=" * 80)
print(f"{'Feature':<20} {'Coeficiente':<15} {'P-valor':<15} {'Importância %':<15} {'Odds Ratio':<15}")
print("-" * 80)
for i, col in enumerate(features_disponiveis):
p_val = p_values[i]
significancia = "***" if p_val < 0.001 else "**" if p_val < 0.01 else "*" if p_val < 0.05 else ""
print(f"{col:<20} {modelo.coef_[0][i]:>14.4f} {p_val:>14.4f}{significancia:<1} {importancia_relativa[i]:>14.2f}% {odds_ratio[i]:>14.4f}")
print("\nLegenda de significância:")
print(" *** p < 0.001 (altamente significativo)")
print(" ** p < 0.01 (muito significativo)")
print(" * p < 0.05 (significativo)")
print(" (sem *) p >= 0.05 (não significativo)")
# Resumo estatístico
print(f"\n📊 Resumo Estatístico:")
print(f" - Features significativas (p < 0.05): {sum(p_values < 0.05)}/{len(features_disponiveis)}")
print(f" - Feature mais importante: {features_disponiveis[np.argmax(importancia_relativa)]} ({importancia_relativa.max():.2f}%)")
print(f" - Feature menos importante: {features_disponiveis[np.argmin(importancia_relativa)]} ({importancia_relativa.min():.2f}%)")
✅ Modelo treinado! 📊 Informações Detalhadas do Modelo: ================================================================================ Feature Coeficiente P-valor Importância % Odds Ratio -------------------------------------------------------------------------------- renda -0.0005 0.0000*** 24.38% 0.9995 tempo_emprego -0.0008 0.0132* 44.32% 0.9992 valor_emprestimo 0.0003 0.0000*** 15.04% 1.0003 prazo_emprestimo 0.0003 0.0000*** 16.25% 1.0003 Legenda de significância: *** p < 0.001 (altamente significativo) ** p < 0.01 (muito significativo) * p < 0.05 (significativo) (sem *) p >= 0.05 (não significativo) 📊 Resumo Estatístico: - Features significativas (p < 0.05): 4/4 - Feature mais importante: tempo_emprego (44.32%) - Feature menos importante: valor_emprestimo (15.04%)
# Predições
y_pred_proba = modelo.predict_proba(X_test)[:, 1]
y_pred = modelo.predict(X_test)
print("✅ Predições realizadas!")
print(f"📊 Probabilidade média: {y_pred_proba.mean():.4f}")
print(f"📊 Predições classe 1: {y_pred.sum():,}")
✅ Predições realizadas! 📊 Probabilidade média: 0.0295 📊 Predições classe 1: 0
# Avaliação do modelo
print("📊 AVALIAÇÃO DO MODELO BASELINE")
print("=" * 60)
# Predições para treino e teste
y_train_pred = modelo.predict(X_train)
y_train_proba = modelo.predict_proba(X_train)[:, 1]
y_test_pred = modelo.predict(X_test)
y_test_proba = modelo.predict_proba(X_test)[:, 1]
# Métricas para treino
acc_train = accuracy_score(y_train, y_train_pred)
prec_train = precision_score(y_train, y_train_pred, zero_division=0)
rec_train = recall_score(y_train, y_train_pred, zero_division=0)
f1_train = f1_score(y_train, y_train_pred, zero_division=0)
roc_auc_train = roc_auc_score(y_train, y_train_proba)
# Métricas para teste
acc_test = accuracy_score(y_test, y_test_pred)
prec_test = precision_score(y_test, y_test_pred, zero_division=0)
rec_test = recall_score(y_test, y_test_pred, zero_division=0)
f1_test = f1_score(y_test, y_test_pred, zero_division=0)
roc_auc_test = roc_auc_score(y_test, y_test_proba)
# Mostrar métricas
print("\n📊 Métricas de Avaliação:")
print(f"\n{'Métrica':<20} {'Treino':<15} {'Teste':<15}")
print("-" * 50)
print(f"{'Acurácia':<20} {acc_train:<15.4f} {acc_test:<15.4f}")
print(f"{'Precisão':<20} {prec_train:<15.4f} {prec_test:<15.4f}")
print(f"{'Recall':<20} {rec_train:<15.4f} {rec_test:<15.4f}")
print(f"{'F1-Score':<20} {f1_train:<15.4f} {f1_test:<15.4f}")
print(f"{'ROC-AUC':<20} {roc_auc_train:<15.4f} {roc_auc_test:<15.4f}")
# Curvas ROC
fpr_train, tpr_train, _ = roc_curve(y_train, y_train_proba)
fpr_test, tpr_test, _ = roc_curve(y_test, y_test_proba)
plt.figure(figsize=(12, 5))
# Curva ROC - Treino
plt.subplot(1, 2, 1)
plt.plot(fpr_train, tpr_train, label=f'Treino (AUC = {roc_auc_train:.4f})', linewidth=2)
plt.plot([0, 1], [0, 1], 'k--', label='Classificador Aleatório')
plt.xlabel('Taxa de Falsos Positivos (FPR)')
plt.ylabel('Taxa de Verdadeiros Positivos (TPR)')
plt.title('Curva ROC - Treino')
plt.legend()
plt.grid(alpha=0.3)
# Curva ROC - Teste
plt.subplot(1, 2, 2)
plt.plot(fpr_test, tpr_test, label=f'Teste (AUC = {roc_auc_test:.4f})', linewidth=2, color='orange')
plt.plot([0, 1], [0, 1], 'k--', label='Classificador Aleatório')
plt.xlabel('Taxa de Falsos Positivos (FPR)')
plt.ylabel('Taxa de Verdadeiros Positivos (TPR)')
plt.title('Curva ROC - Teste')
plt.legend()
plt.grid(alpha=0.3)
plt.tight_layout()
plt.show()
# Matriz de Confusão
cm = confusion_matrix(y_test, y_test_pred)
plt.figure(figsize=(8, 6))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', cbar=True)
plt.title('Matriz de Confusão - Teste')
plt.ylabel('Valor Real')
plt.xlabel('Valor Predito')
plt.show()
# Mostrar valores da matriz de confusão
print("\n📊 Matriz de Confusão (Teste):")
print(f" Verdadeiros Negativos (TN): {cm[0,0]:,}")
print(f" Falsos Positivos (FP): {cm[0,1]:,}")
print(f" Falsos Negativos (FN): {cm[1,0]:,}")
print(f" Verdadeiros Positivos (TP): {cm[1,1]:,}")
📊 AVALIAÇÃO DO MODELO BASELINE ============================================================ 📊 Métricas de Avaliação: Métrica Treino Teste -------------------------------------------------- Acurácia 0.9704 0.9704 Precisão 0.0000 0.0000 Recall 0.0000 0.0000 F1-Score 0.0000 0.0000 ROC-AUC 0.6931 0.6913
📊 Matriz de Confusão (Teste): Verdadeiros Negativos (TN): 222,848 Falsos Positivos (FP): 0 Falsos Negativos (FN): 6,792 Verdadeiros Positivos (TP): 0
📝 Interpretação dos Resultados
⚠️ Atenção: Base Desbalanceada
Este dataset apresenta um desequilíbrio significativo entre as classes:
- Classe 0 (Não-default): ~97% dos casos
- Classe 1 (Default): ~3% dos casos
Por que não focar apenas na Acurácia?
A acurácia de 97.04% pode parecer excelente, mas é completamente enganosa neste contexto. Um modelo que simplesmente previsse sempre "não-default" teria ~97% de acurácia, mas seria totalmente inútil para identificar clientes com risco de inadimplência.
⚠️ PROBLEMA CRÍTICO IDENTIFICADO:
O modelo está prevendo sempre a classe 0 (não-default). Isso é evidenciado por:
- Precisão = 0%: Não há nenhuma previsão positiva, então a precisão não pode ser calculada
- Recall = 0%: O modelo não identificou nenhum caso de default
- F1-Score = 0%: Como não há predições positivas, o F1-Score é zero
Interpretação das Métricas:
Acurácia (97.04%): Alta, mas completamente enganosa. O modelo simplesmente prevê sempre "não-default" e acerta 97% dos casos por acaso, já que 97% dos dados são da classe 0. Esta métrica não deve ser usada para avaliar o modelo.
Precisão (0%): O modelo não fez nenhuma previsão de default, então a precisão é indefinida (0%). Isso indica que o threshold padrão (0.5) está muito alto para este problema desbalanceado.
Recall (0%): O modelo não identificou nenhum dos 6.792 casos de default reais. Isso é um problema crítico - o modelo está falhando completamente em sua função principal.
F1-Score (0%): Zero porque não há predições positivas. O modelo não está balanceando precisão e recall porque não está fazendo nenhuma predição positiva.
ROC-AUC (69.13%): Esta é a métrica mais relevante aqui! Um AUC de 69% indica que o modelo tem capacidade discriminativa moderada - melhor que aleatório (50%), mas ainda precisa melhorar. O fato de o AUC ser > 50% mostra que o modelo tem potencial, mas o threshold precisa ser ajustado.
Interpretação da Matriz de Confusão (Teste):
- Verdadeiros Negativos (TN): 222,848 - Clientes sem risco corretamente identificados (100% dos não-defaults)
- Falsos Positivos (FP): 0 - Nenhum cliente foi incorretamente classificado como risco
- Falsos Negativos (FN): 6,792 - TODOS os 6.792 casos de default foram perdidos ⚠️ PROBLEMA CRÍTICO
- Verdadeiros Positivos (TP): 0 - O modelo não identificou nenhum caso de default ⚠️ PROBLEMA CRÍTICO
Percentuais da Matriz de Confusão:
- Taxa de Verdadeiros Negativos: 222,848 / 222,848 = 100% ✅ (mas isso é esperado já que prevê sempre 0)
- Taxa de Falsos Positivos: 0 / 222,848 = 0% (zero porque não há predições positivas)
- Taxa de Falsos Negativos: 6,792 / 6,792 = 100% ⚠️ CRÍTICO - PERDE TODOS OS DEFAULTS
- Taxa de Verdadeiros Positivos: 0 / 6,792 = 0% ⚠️ CRÍTICO - NÃO IDENTIFICA NENHUM DEFAULT
Conclusão:
O modelo baseline atual está completamente inoperante para identificar defaults:
- ❌ Não identifica nenhum caso de default (TP = 0, Recall = 0%)
- ❌ Perde todos os 6.792 casos de default (FN = 6.792)
- ⚠️ O threshold padrão (0.5) é inadequado para dados tão desbalanceados
Porém, há esperança:
- ✅ O ROC-AUC de 69% mostra que o modelo tem capacidade discriminativa (melhor que 50%)
- ✅ O problema é principalmente o threshold de decisão, não a capacidade do modelo
Próximos passos urgentes:
- Ajustar o threshold de decisão - Reduzir de 0.5 para um valor menor (ex: 0.03) para capturar mais defaults
- Usar técnicas de balanceamento de classes - SMOTE, undersampling, ou class_weight='balanced'
- Considerar outras métricas - Focar em Recall ou Precision-Recall Curve em vez de ROC-AUC
- Feature engineering adicional - Pode ser necessário criar features mais discriminativas
💼 Implicações de Negócio
Modelo baseline estabelecido. Este modelo simples serve como referência - qualquer modelo futuro deve superá-lo. Um baseline bem feito ajuda a avaliar se vale a pena usar modelos mais complexos.
Lição Chave: Sempre comece simples. Um modelo baseline robusto é melhor que um modelo complexo mal ajustado. A simplicidade muitas vezes vence na indústria, especialmente quando precisa ser explicado para stakeholders.