EDA e Visualizações Informativas
Objetivos de Aprendizagem
Ao final deste notebook, você será capaz de:
- ✅ Realizar análise exploratória completa dos dados
- ✅ Identificar valores ausentes e duplicados
- ✅ Criar visualizações informativas
- ✅ Analisar a variável target
- ✅ Identificar outliers e padrões
Pré-requisitos
- Notebook 01: Dados carregados
- Dataset disponível
Nota: Execute as células de carregamento primeiro!
# Carregar dados
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings("ignore")
try:
df
print("✅ Dataset já carregado")
except NameError:
df = pd.read_csv("credit_risk_full_1_200_000.csv")
df["renda"] = pd.to_numeric(df["renda"], errors="coerce")
df["valor_emprestimo"] = pd.to_numeric(df["valor_emprestimo"], errors="coerce")
print("✅ Dataset carregado")
# Análise de valores ausentes
print("🔍 ANÁLISE DE QUALIDADE")
missing = df.isnull().sum()
missing_pct = (missing / len(df)) * 100
print("Valores ausentes por coluna:")
for col in df.columns:
if missing[col] > 0:
print(f"{col:25} {missing[col]:>10,} ({missing_pct[col]:>5.1f}%)")
✅ Dataset carregado 🔍 ANÁLISE DE QUALIDADE Valores ausentes por coluna: genero 5,848 ( 0.5%) estado_civil 11,938 ( 1.0%) renda 141,765 ( 11.8%) valor_emprestimo 4,816 ( 0.4%) score_credito 29,891 ( 2.5%) tempo_emprego 120,510 ( 10.0%)
# Distribuição do target
print("📊 DISTRIBUIÇÃO DO TARGET:")
target_dist = df["default"].value_counts()
target_pct = df["default"].value_counts(normalize=True) * 100
for val in sorted(target_dist.index):
label = "Não Default" if val == 0 else "Default"
print(f"{label:20} → {target_dist[val]:>10,} ({target_pct[val]:>5.1f}%)")
default_rate = df["default"].mean()
print(f"⚠️ Taxa de default: {default_rate:.2%}")
📊 DISTRIBUIÇÃO DO TARGET: Não Default → 1,164,501 ( 97.0%) Default → 35,499 ( 3.0%) ⚠️ Taxa de default: 2.96%
# Histogramas - Layout 2 colunas x 3 linhas
cols = ["idade", "renda", "score_credito", "tempo_emprego", "valor_emprestimo", "prazo_emprestimo"]
fig, axes = plt.subplots(3, 2, figsize=(12, 12)) # 3 linhas, 2 colunas
axes = axes.flatten()
for ax, col in zip(axes, cols):
if col in df.columns:
data = df[col].dropna()
ax.hist(data, bins=50, edgecolor="black", alpha=0.75)
ax.set_title(col.replace("_", " ").title())
ax.set_xlabel(col.replace("_", " ").title(), fontsize=10)
ax.set_ylabel("Frequência", fontsize=10)
plt.suptitle("Distribuições das Variáveis Numéricas", fontsize=14, fontweight="bold")
plt.tight_layout()
plt.show()
# Boxplots - Identificação de Outliers
cols = ["idade", "renda", "score_credito", "tempo_emprego", "valor_emprestimo", "prazo_emprestimo"]
fig, axes = plt.subplots(3, 2, figsize=(12, 12)) # 3 linhas, 2 colunas
axes = axes.flatten()
for ax, col in zip(axes, cols):
if col in df.columns:
data = df[col].dropna()
bp = ax.boxplot(data, patch_artist=True)
bp["boxes"][0].set_facecolor("#b794f6")
bp["boxes"][0].set_alpha(0.7)
ax.set_title(col.replace("_", " ").title(), fontweight="bold")
ax.set_ylabel("Valor", fontsize=10)
ax.grid(axis="y", alpha=0.3)
plt.suptitle("Boxplots - Identificação de Outliers", fontsize=14, fontweight="bold")
plt.tight_layout()
plt.show()
Análise por Faixa Etária
# Análise por faixa etária
df["idade_clean"] = df["idade"].copy()
df.loc[df["idade"] < 0, "idade_clean"] = None
df.loc[df["idade"] == 0, "idade_clean"] = None
df["faixa_etaria"] = pd.cut(df["idade_clean"], bins=[0, 25, 35, 45, 55, 65, 120],
labels=["18-25", "26-35", "36-45", "46-55", "56-65", "65+"],
include_lowest=True)
default_por_idade = df.groupby("faixa_etaria", observed=True)["default"].agg(["count", "sum", "mean"])
default_por_idade.columns = ["Total", "Defaults", "Taxa"]
print("📊 TAXA DE DEFAULT POR FAIXA ETÁRIA:")
for idx, row in default_por_idade.iterrows():
print(f"{idx:<15} {row['Total']:>12,.0f} {row['Taxa']:>10.1%}")
📊 TAXA DE DEFAULT POR FAIXA ETÁRIA: 18-25 242,550 3.4% 26-35 356,542 2.8% 36-45 355,600 2.8% 46-55 184,320 2.9% 56-65 49,874 2.8% 65+ 9,953 3.3%
# Análise de Correlação
print("🔗 MATRIZ DE CORRELAÇÃO COM TARGET:")
numeric_cols = df.select_dtypes(include=[np.number]).columns
correlations = df[numeric_cols].corr()["default"].sort_values(ascending=False)
print("\nCorrelação com Default (ordenado):")
for col, corr in correlations.items():
if col != "default":
print(f"{col:25} → {corr:>7.3f}")
# Visualização da matriz de correlação
plt.figure(figsize=(10, 8))
corr_matrix = df[numeric_cols].corr()
mask = np.triu(np.ones_like(corr_matrix, dtype=bool))
sns.heatmap(corr_matrix, mask=mask, annot=True, fmt=".2f", cmap="coolwarm",
center=0, square=True, linewidths=1, cbar_kws={"shrink": 0.8})
plt.title("Matriz de Correlação - Variáveis Numéricas", fontsize=14, fontweight="bold")
plt.tight_layout()
plt.show()
🔗 MATRIZ DE CORRELAÇÃO COM TARGET: Correlação com Default (ordenado): prob_default → 0.396 historico_inadimplencia → 0.075 valor_emprestimo → 0.037 prazo_emprestimo → 0.004 tempo_emprego → -0.004 idade → -0.008 idade_clean → -0.008 renda → -0.023 score_credito → -0.227
# Análise de Default por Variáveis Categóricas
print("📊 TAXA DE DEFAULT POR VARIÁVEIS CATEGÓRICAS:\n")
categorical_vars = ["genero", "estado_civil", "tipo_residencia"]
for var in categorical_vars:
if var in df.columns:
print(f"\n{var.replace('_', ' ').title()}:")
default_by_cat = df.groupby(var, observed=True)["default"].agg(["count", "sum", "mean"])
default_by_cat.columns = ["Total", "Defaults", "Taxa_Default"]
default_by_cat = default_by_cat.sort_values("Taxa_Default", ascending=False)
for idx, row in default_by_cat.iterrows():
print(f" {str(idx):<20} → Total: {row['Total']:>8,.0f} | Defaults: {row['Defaults']:>6,.0f} | Taxa: {row['Taxa_Default']:>6.2%}")
# Visualização
fig, axes = plt.subplots(1, len(categorical_vars), figsize=(15, 5))
for idx, var in enumerate(categorical_vars):
if var in df.columns:
default_by_cat = df.groupby(var, observed=True)["default"].mean().sort_values(ascending=False)
axes[idx].bar(range(len(default_by_cat)), default_by_cat.values, color="#b794f6", alpha=0.7, edgecolor="black")
axes[idx].set_xticks(range(len(default_by_cat)))
axes[idx].set_xticklabels(default_by_cat.index, rotation=45, ha="right")
axes[idx].set_title(f"Taxa de Default por {var.replace('_', ' ').title()}", fontweight="bold")
axes[idx].set_ylabel("Taxa de Default")
axes[idx].grid(axis="y", alpha=0.3)
axes[idx].axhline(y=df["default"].mean(), color="red", linestyle="--",
label=f"Média Geral ({df['default'].mean():.2%})")
axes[idx].legend()
plt.suptitle("Análise de Default por Variáveis Categóricas", fontsize=14, fontweight="bold")
plt.tight_layout()
plt.show()
📊 TAXA DE DEFAULT POR VARIÁVEIS CATEGÓRICAS: Genero: outro → Total: 430 | Defaults: 15 | Taxa: 3.49% M → Total: 572,352 | Defaults: 16,972 | Taxa: 2.97% F → Total: 608,837 | Defaults: 17,975 | Taxa: 2.95% Outro → Total: 12,035 | Defaults: 348 | Taxa: 2.89% M → Total: 223 | Defaults: 5 | Taxa: 2.24% F → Total: 246 | Defaults: 4 | Taxa: 1.63% NAN → Total: 2 | Defaults: 0 | Taxa: 0.00% OUTRO → Total: 8 | Defaults: 0 | Taxa: 0.00% Outro → Total: 9 | Defaults: 0 | Taxa: 0.00% Outrx → Total: 2 | Defaults: 0 | Taxa: 0.00% Outxo → Total: 2 | Defaults: 0 | Taxa: 0.00% Ouxro → Total: 3 | Defaults: 0 | Taxa: 0.00% Oxtro → Total: 1 | Defaults: 0 | Taxa: 0.00% nan → Total: 2 | Defaults: 0 | Taxa: 0.00% Estado Civil: viuvx → Total: 4 | Defaults: 1 | Taxa: 25.00% soxteiro → Total: 21 | Defaults: 3 | Taxa: 14.29% divorcxado → Total: 16 | Defaults: 2 | Taxa: 12.50% divorcixdo → Total: 11 | Defaults: 1 | Taxa: 9.09% casxdo → Total: 35 | Defaults: 3 | Taxa: 8.57% soltxiro → Total: 21 | Defaults: 1 | Taxa: 4.76% solteiro → Total: 147 | Defaults: 7 | Taxa: 4.76% VIUVO → Total: 23 | Defaults: 1 | Taxa: 4.35% solxeiro → Total: 23 | Defaults: 1 | Taxa: 4.35% SOLTEIRO → Total: 167 | Defaults: 7 | Taxa: 4.19% solteixo → Total: 24 | Defaults: 1 | Taxa: 4.17% sxlteiro → Total: 25 | Defaults: 1 | Taxa: 4.00% solteirx → Total: 26 | Defaults: 1 | Taxa: 3.85% divorciado → Total: 177,740 | Defaults: 5,639 | Taxa: 3.17% solteiro → Total: 415,848 | Defaults: 12,758 | Taxa: 3.07% viuvo → Total: 59,324 | Defaults: 1,743 | Taxa: 2.94% casado → Total: 533,338 | Defaults: 14,955 | Taxa: 2.80% outro → Total: 466 | Defaults: 13 | Taxa: 2.79% CASADO → Total: 194 | Defaults: 5 | Taxa: 2.58% DIVORCIADO → Total: 78 | Defaults: 2 | Taxa: 2.56% cxsado → Total: 47 | Defaults: 1 | Taxa: 2.13% casado → Total: 201 | Defaults: 4 | Taxa: 1.99% viuvo → Total: 18 | Defaults: 0 | Taxa: 0.00% viuxo → Total: 4 | Defaults: 0 | Taxa: 0.00% soltexro → Total: 14 | Defaults: 0 | Taxa: 0.00% vixvo → Total: 3 | Defaults: 0 | Taxa: 0.00% divxrciado → Total: 4 | Defaults: 0 | Taxa: 0.00% nan → Total: 9 | Defaults: 0 | Taxa: 0.00% dxvorciado → Total: 10 | Defaults: 0 | Taxa: 0.00% dixorciado → Total: 4 | Defaults: 0 | Taxa: 0.00% divoxciado → Total: 10 | Defaults: 0 | Taxa: 0.00% divorxiado → Total: 9 | Defaults: 0 | Taxa: 0.00% divorciaxo → Total: 9 | Defaults: 0 | Taxa: 0.00% divorciadx → Total: 7 | Defaults: 0 | Taxa: 0.00% divorciado → Total: 56 | Defaults: 0 | Taxa: 0.00% caxado → Total: 24 | Defaults: 0 | Taxa: 0.00% casaxo → Total: 51 | Defaults: 0 | Taxa: 0.00% casadx → Total: 39 | Defaults: 0 | Taxa: 0.00% NAN → Total: 6 | Defaults: 0 | Taxa: 0.00% vxuvo → Total: 6 | Defaults: 0 | Taxa: 0.00%
📝 Interpretação
A análise das variáveis categóricas revela problemas críticos de qualidade de dados que precisarão ser tratados no pré-processamento:
⚠️ Problemas Identificados:
Inconsistência de Formatação:
Gênero: Encontramos múltiplas variações do mesmo valor:
M,F(maioria dos casos)Outro,outro,OUTRO(diferentes capitalizações)Outrx,Outxo,Ouxro,Oxtro(erros de digitação)NAN,nan(valores ausentes codificados como string)
Estado Civil: Problemas similares:
solteiro,SOLTEIRO,solxeiro,soltxiro,solteixo,sxlteiro(múltiplas variações)viuvx,VIUVO(inconsistências)divorcxado,divorcixdo,casxdo(erros de digitação)
Impacto na Análise:
- Os valores mal formatados criam categorias artificiais com poucos registros
- Taxas de default calculadas sobre amostras muito pequenas (ex:
viuvxcom apenas 4 registros e 25% de default) não são estatisticamente confiáveis - A fragmentação dos dados dificulta a identificação de padrões reais
Ações Necessárias no Pré-processamento:
- Padronização: Converter todas as strings para minúsculas ou maiúsculas
- Correção de Erros: Mapear variações comuns (ex:
solxeiro→solteiro) - Tratamento de Valores Ausentes: Identificar e tratar
NAN,nancomo valores ausentes reais - Agrupamento: Consolidar categorias similares antes da modelagem
💡 Insight Importante:
Apesar dos problemas de qualidade, observamos que as categorias principais (M, F, solteiro, casado, etc.) representam a grande maioria dos dados e mostram taxas de default relativamente consistentes (~2.9-3.0%), próximas à média geral. Isso sugere que, após a padronização, essas variáveis podem ser úteis para o modelo.
# Análise Bivariada: Valor do Empréstimo vs Score de Crédito
print("📈 ANÁLISE BIVARIADA: VALOR DO EMPRÉSTIMO vs SCORE DE CRÉDITO\n")
# Criar categorias para análise
df["valor_categoria"] = pd.cut(df["valor_emprestimo"],
bins=[0, 5000, 10000, 20000, 50000, float("inf")],
labels=["0-5k", "5k-10k", "10k-20k", "20k-50k", "50k+"],
include_lowest=True)
df["score_categoria"] = pd.cut(df["score_credito"],
bins=[0, 300, 500, 700, 850, 1000],
labels=["0-300", "300-500", "500-700", "700-850", "850+"],
include_lowest=True)
# Taxa de default por categoria de valor
print("Taxa de Default por Valor do Empréstimo:")
valor_default = df.groupby("valor_categoria", observed=True)["default"].agg(["count", "mean"])
for idx, row in valor_default.iterrows():
print(f" {str(idx):<10} → Total: {row['count']:>8,.0f} | Taxa: {row['mean']:>6.2%}")
print("\nTaxa de Default por Score de Crédito:")
score_default = df.groupby("score_categoria", observed=True)["default"].agg(["count", "mean"])
for idx, row in score_default.iterrows():
print(f" {str(idx):<10} → Total: {row['count']:>8,.0f} | Taxa: {row['mean']:>6.2%}")
# Visualização
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# Scatter plot: Valor vs Score colorido por Default
ax1 = axes[0]
sample_df = df.dropna(subset=["valor_emprestimo", "score_credito", "default"]).sample(n=min(5000, len(df)))
colors = ["#22c55e" if x == 0 else "#ef4444" for x in sample_df["default"]]
ax1.scatter(sample_df["score_credito"], sample_df["valor_emprestimo"],
c=colors, alpha=0.5, s=20, edgecolors="black", linewidth=0.3)
ax1.set_xlabel("Score de Crédito", fontweight="bold")
ax1.set_ylabel("Valor do Empréstimo", fontweight="bold")
ax1.set_title("Score vs Valor do Empréstimo (por Default)", fontweight="bold")
ax1.grid(alpha=0.3)
ax1.legend(handles=[plt.Line2D([0], [0], marker="o", color="w", markerfacecolor="#22c55e",
markersize=10, label="Não Default"),
plt.Line2D([0], [0], marker="o", color="w", markerfacecolor="#ef4444",
markersize=10, label="Default")],
loc="upper right")
# Heatmap: Taxa de default por combinação de categorias
ax2 = axes[1]
pivot_table = df.groupby(["score_categoria", "valor_categoria"], observed=True)["default"].mean().unstack(fill_value=0)
sns.heatmap(pivot_table, annot=True, fmt=".2%", cmap="RdYlGn_r", ax=ax2,
cbar_kws={"label": "Taxa de Default"}, linewidths=0.5)
ax2.set_title("Taxa de Default: Score vs Valor do Empréstimo", fontweight="bold")
ax2.set_xlabel("Valor do Empréstimo", fontweight="bold")
ax2.set_ylabel("Score de Crédito", fontweight="bold")
plt.suptitle("Análise Bivariada: Fatores de Risco Combinados", fontsize=14, fontweight="bold")
plt.tight_layout()
plt.show()
📈 ANÁLISE BIVARIADA: VALOR DO EMPRÉSTIMO vs SCORE DE CRÉDITO Taxa de Default por Valor do Empréstimo: 0-5k → Total: 865,332 | Taxa: 2.58% 5k-10k → Total: 259,292 | Taxa: 4.03% 10k-20k → Total: 64,940 | Taxa: 3.77% 20k-50k → Total: 5,586 | Taxa: 2.35% 50k+ → Total: 34 | Taxa: 0.00% Taxa de Default por Score de Crédito: 0-300 → Total: 2,361 | Taxa: 2.75% 300-500 → Total: 129,851 | Taxa: 14.04% 500-700 → Total: 388,028 | Taxa: 3.35% 700-850 → Total: 461,807 | Taxa: 0.62% 850+ → Total: 188,062 | Taxa: 0.25%
Análise de Renda e Tempo de Emprego vs Default
# Histograma da Distribuição de Renda
print("📊 DISTRIBUIÇÃO DE RENDA\n")
# Remover valores ausentes para visualização
renda_clean = df["renda"].dropna()
# Estatísticas descritivas
print("Estatísticas Descritivas de Renda:")
print(f" Média: R$ {renda_clean.mean():,.2f}")
print(f" Mediana: R$ {renda_clean.median():,.2f}")
print(f" Desvio Padrão: R$ {renda_clean.std():,.2f}")
print(f" Mínimo: R$ {renda_clean.min():,.2f}")
print(f" Máximo: R$ {renda_clean.max():,.2f}")
print(f" Q1 (25%): R$ {renda_clean.quantile(0.25):,.2f}")
print(f" Q3 (75%): R$ {renda_clean.quantile(0.75):,.2f}")
# Visualização
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# Histograma padrão
ax1 = axes[0]
ax1.hist(renda_clean, bins=50, edgecolor="black", alpha=0.7, color="#b794f6")
ax1.set_xlabel("Renda (R$)", fontweight="bold")
ax1.set_ylabel("Frequência", fontweight="bold")
ax1.set_title("Distribuição de Renda - Histograma", fontweight="bold")
ax1.axvline(renda_clean.mean(), color="red", linestyle="--", linewidth=2, label=f"Média (R$ {renda_clean.mean():,.0f})")
ax1.axvline(renda_clean.median(), color="green", linestyle="--", linewidth=2, label=f"Mediana (R$ {renda_clean.median():,.0f})")
ax1.grid(alpha=0.3)
ax1.legend()
# Histograma com escala logarítmica (para melhor visualização se houver outliers)
ax2 = axes[1]
ax2.hist(renda_clean, bins=50, edgecolor="black", alpha=0.7, color="#b794f6")
ax2.set_xlabel("Renda (R$)", fontweight="bold")
ax2.set_ylabel("Frequência", fontweight="bold")
ax2.set_title("Distribuição de Renda - Escala Logarítmica", fontweight="bold")
ax2.set_yscale("log")
ax2.axvline(renda_clean.mean(), color="red", linestyle="--", linewidth=2, label=f"Média (R$ {renda_clean.mean():,.0f})")
ax2.axvline(renda_clean.median(), color="green", linestyle="--", linewidth=2, label=f"Mediana (R$ {renda_clean.median():,.0f})")
ax2.grid(alpha=0.3)
ax2.legend()
plt.suptitle("Análise da Distribuição de Renda", fontsize=14, fontweight="bold")
plt.tight_layout()
plt.show()
# Análise de outliers usando IQR
Q1 = renda_clean.quantile(0.25)
Q3 = renda_clean.quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outliers = renda_clean[(renda_clean < lower_bound) | (renda_clean > upper_bound)]
print(f"\n📈 ANÁLISE DE OUTLIERS (Método IQR):")
print(f" Limite inferior: R$ {lower_bound:,.2f}")
print(f" Limite superior: R$ {upper_bound:,.2f}")
print(f" Outliers encontrados: {len(outliers):,} ({len(outliers)/len(renda_clean)*100:.2f}%)")
📊 DISTRIBUIÇÃO DE RENDA Estatísticas Descritivas de Renda: Média: R$ 3,627.47 Mediana: R$ 2,981.26 Desvio Padrão: R$ 2,954.93 Mínimo: R$ 80.00 Máximo: R$ 268,656.83 Q1 (25%): R$ 1,985.22 Q3 (75%): R$ 4,475.71
📈 ANÁLISE DE OUTLIERS (Método IQR): Limite inferior: R$ -1,750.51 Limite superior: R$ 8,211.43 Outliers encontrados: 50,128 (4.74%)
📝 Interpretação da Distribuição de Renda
Principais observações:
Assimetria positiva: A mediana é menor que a média, indicando que a distribuição tem uma cauda longa à direita (poucos valores muito altos puxam a média para cima).
Presença de outliers: A diferença significativa entre média e mediana, além dos valores máximos extremos, sugere a existência de outliers que precisarão ser tratados no pré-processamento.
Concentração de dados: A maioria dos clientes tem renda entre R$ 2.000 e R$ 4.500 (Q1 a Q3), representando a faixa típica do dataset.
Implicações para modelagem:
- Considerar transformações (log, raiz quadrada) para normalizar a distribuição
- Avaliar técnicas de tratamento de outliers (winsorização, cap)
- Criar categorias de renda pode ser mais robusto que usar valores contínuos
Visualização da Distribuição de Tempo de Emprego
# Histograma da Distribuição de Tempo de Emprego
print("📊 DISTRIBUIÇÃO DE TEMPO DE EMPREGO\n")
# Remover valores ausentes para visualização
tempo_emprego_clean = df["tempo_emprego"].dropna()
# Estatísticas descritivas
print("Estatísticas Descritivas de Tempo de Emprego:")
print(f" Média: {tempo_emprego_clean.mean():.2f} anos")
print(f" Mediana: {tempo_emprego_clean.median():.2f} anos")
print(f" Desvio Padrão: {tempo_emprego_clean.std():.2f} anos")
print(f" Mínimo: {tempo_emprego_clean.min():.2f} anos")
print(f" Máximo: {tempo_emprego_clean.max():.2f} anos")
print(f" Q1 (25%): {tempo_emprego_clean.quantile(0.25):.2f} anos")
print(f" Q3 (75%): {tempo_emprego_clean.quantile(0.75):.2f} anos")
# Visualização
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# Histograma padrão
ax1 = axes[0]
ax1.hist(tempo_emprego_clean, bins=50, edgecolor="black", alpha=0.7, color="#b794f6")
ax1.set_xlabel("Tempo de Emprego (anos)", fontweight="bold")
ax1.set_ylabel("Frequência", fontweight="bold")
ax1.set_title("Distribuição de Tempo de Emprego - Histograma", fontweight="bold")
ax1.axvline(tempo_emprego_clean.mean(), color="red", linestyle="--", linewidth=2,
label=f"Média ({tempo_emprego_clean.mean():.1f} anos)")
ax1.axvline(tempo_emprego_clean.median(), color="green", linestyle="--", linewidth=2,
label=f"Mediana ({tempo_emprego_clean.median():.1f} anos)")
ax1.grid(alpha=0.3)
ax1.legend()
# Histograma com escala logarítmica (para melhor visualização se houver outliers)
ax2 = axes[1]
ax2.hist(tempo_emprego_clean, bins=50, edgecolor="black", alpha=0.7, color="#b794f6")
ax2.set_xlabel("Tempo de Emprego (anos)", fontweight="bold")
ax2.set_ylabel("Frequência", fontweight="bold")
ax2.set_title("Distribuição de Tempo de Emprego - Escala Logarítmica", fontweight="bold")
ax2.set_yscale("log")
ax2.axvline(tempo_emprego_clean.mean(), color="red", linestyle="--", linewidth=2,
label=f"Média ({tempo_emprego_clean.mean():.1f} anos)")
ax2.axvline(tempo_emprego_clean.median(), color="green", linestyle="--", linewidth=2,
label=f"Mediana ({tempo_emprego_clean.median():.1f} anos)")
ax2.grid(alpha=0.3)
ax2.legend()
plt.suptitle("Análise da Distribuição de Tempo de Emprego", fontsize=14, fontweight="bold")
plt.tight_layout()
plt.show()
# Análise de outliers usando IQR
Q1 = tempo_emprego_clean.quantile(0.25)
Q3 = tempo_emprego_clean.quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outliers = tempo_emprego_clean[(tempo_emprego_clean < lower_bound) | (tempo_emprego_clean > upper_bound)]
print(f"\n📈 ANÁLISE DE OUTLIERS (Método IQR):")
print(f" Limite inferior: {lower_bound:.2f} anos")
print(f" Limite superior: {upper_bound:.2f} anos")
print(f" Outliers encontrados: {len(outliers):,} ({len(outliers)/len(tempo_emprego_clean)*100:.2f}%)")
# Distribuição por faixas
print(f"\n📊 DISTRIBUIÇÃO POR FAIXAS DE TEMPO DE EMPREGO:")
faixas_tempo = pd.cut(tempo_emprego_clean,
bins=[0, 1, 3, 5, 10, 20, 50],
labels=["0-1 ano", "1-3 anos", "3-5 anos", "5-10 anos", "10-20 anos", "20+ anos"],
include_lowest=True)
dist_faixas = faixas_tempo.value_counts().sort_index()
for faixa, count in dist_faixas.items():
pct = (count / len(tempo_emprego_clean)) * 100
print(f" {str(faixa):<15} → {count:>8,} ({pct:>5.1f}%)")
📊 DISTRIBUIÇÃO DE TEMPO DE EMPREGO Estatísticas Descritivas de Tempo de Emprego: Média: 24.01 anos Mediana: 16.63 anos Desvio Padrão: 24.01 anos Mínimo: 0.00 anos Máximo: 322.53 anos Q1 (25%): 6.91 anos Q3 (75%): 33.28 anos
📈 ANÁLISE DE OUTLIERS (Método IQR): Limite inferior: -32.65 anos Limite superior: 72.84 anos Outliers encontrados: 52,133 (4.83%) 📊 DISTRIBUIÇÃO POR FAIXAS DE TEMPO DE EMPREGO: 0-1 ano → 43,954 ( 4.1%) 1-3 anos → 82,887 ( 7.7%) 3-5 anos → 75,866 ( 7.0%) 5-10 anos → 165,017 ( 15.3%) 10-20 anos → 242,847 ( 22.5%) 20+ anos → 334,596 ( 31.0%)
📝 Interpretação da Distribuição de Tempo de Emprego
Principais observações:
Distribuição assimétrica: Similar à renda, apresenta assimetria positiva com alguns valores extremos de tempo de emprego muito alto.
Concentração em faixas iniciais: A maioria dos clientes tem tempo de emprego relativamente baixo (0-5 anos), o que é esperado em um mercado de trabalho dinâmico.
Estabilidade vs. Risco: Clientes com maior tempo de emprego tendem a ser mais estáveis, mas a variável sozinha pode não ser suficiente para prever default.
Implicações para modelagem:
- Categorização em faixas é mais interpretável que valores contínuos
- Considerar interação com outras variáveis (ex: renda × tempo de emprego)
- Valores ausentes (~10%) precisarão ser tratados (imputação ou categoria especial)
# Análise de Renda e Tempo de Emprego vs Default
print("📊 ANÁLISE: RENDA E TEMPO DE EMPREGO vs DEFAULT\n")
# Criar categorias para análise
df["log_renda"] = np.log1p(df["renda"].clip(lower=0))
# Divide o log da renda em 5 grupos (quintis logarítmicos)
df["renda_faixa"] = pd.qcut(df["log_renda"], q=5,
labels=["Muito Baixa", "Baixa", "Média", "Alta", "Muito Alta"])
bins = [0, 1, 3, 5, 10, 20, 35, 60]
labels = ["Até 1 ano", "1–3 anos", "3–5 anos", "5–10 anos", "10–20 anos", "20–35 anos", "35+ anos"]
df["tempo_emprego_cat"] = pd.cut(df["tempo_emprego"], bins=bins, labels=labels, include_lowest=True)
# Taxa de default por categoria de renda
print("Taxa de Default por Categoria de Renda:")
renda_default = df.groupby("renda_categoria", observed=True)["default"].agg(["count", "mean"])
for idx, row in renda_default.iterrows():
if pd.notna(idx):
print(f" {str(idx):<15} → Total: {row['count']:>8,.0f} | Taxa: {row['mean']:>6.2%}")
print("\nTaxa de Default por Tempo de Emprego:")
tempo_default = df.groupby("tempo_emprego_categoria", observed=True)["default"].agg(["count", "mean"])
for idx, row in tempo_default.iterrows():
if pd.notna(idx):
print(f" {str(idx):<15} → Total: {row['count']:>8,.0f} | Taxa: {row['mean']:>6.2%}")
# Visualização
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# Gráfico 1: Taxa de default por renda
ax1 = axes[0, 0]
renda_plot = df.groupby("renda_categoria", observed=True)["default"].mean().sort_values()
renda_plot = renda_plot.dropna()
if len(renda_plot) > 0:
ax1.barh(range(len(renda_plot)), renda_plot.values, color="#b794f6", alpha=0.7, edgecolor="black")
ax1.set_yticks(range(len(renda_plot)))
ax1.set_yticklabels(renda_plot.index)
ax1.set_xlabel("Taxa de Default", fontweight="bold")
ax1.set_title("Taxa de Default por Categoria de Renda", fontweight="bold")
ax1.axvline(x=df["default"].mean(), color="red", linestyle="--",
label=f"Média Geral ({df['default'].mean():.2%})")
ax1.grid(axis="x", alpha=0.3)
ax1.legend()
# Gráfico 2: Taxa de default por tempo de emprego
ax2 = axes[0, 1]
tempo_plot = df.groupby("tempo_emprego_categoria", observed=True)["default"].mean()
tempo_plot = tempo_plot.dropna()
if len(tempo_plot) > 0:
ax2.bar(range(len(tempo_plot)), tempo_plot.values, color="#b794f6", alpha=0.7, edgecolor="black")
ax2.set_xticks(range(len(tempo_plot)))
ax2.set_xticklabels(tempo_plot.index, rotation=45, ha="right")
ax2.set_ylabel("Taxa de Default", fontweight="bold")
ax2.set_title("Taxa de Default por Tempo de Emprego", fontweight="bold")
ax2.axhline(y=df["default"].mean(), color="red", linestyle="--",
label=f"Média Geral ({df['default'].mean():.2%})")
ax2.grid(axis="y", alpha=0.3)
ax2.legend()
# Gráfico 3: Scatter plot Renda vs Tempo de Emprego (colorido por default)
ax3 = axes[1, 0]
sample_df = df.dropna(subset=["renda", "tempo_emprego", "default"]).sample(n=min(5000, len(df)))
colors = ["#22c55e" if x == 0 else "#ef4444" for x in sample_df["default"]]
ax3.scatter(sample_df["tempo_emprego"], sample_df["renda"],
c=colors, alpha=0.5, s=20, edgecolors="black", linewidth=0.3)
ax3.set_xlabel("Tempo de Emprego (anos)", fontweight="bold")
ax3.set_ylabel("Renda", fontweight="bold")
ax3.set_title("Renda vs Tempo de Emprego (por Default)", fontweight="bold")
ax3.grid(alpha=0.3)
ax3.legend(handles=[plt.Line2D([0], [0], marker="o", color="w", markerfacecolor="#22c55e",
markersize=10, label="Não Default"),
plt.Line2D([0], [0], marker="o", color="w", markerfacecolor="#ef4444",
markersize=10, label="Default")],
loc="upper right")
# Gráfico 4: Heatmap - Taxa de default por combinação Renda x Tempo de Emprego
ax4 = axes[1, 1]
pivot_table = df.groupby(["renda_categoria", "tempo_emprego_categoria"], observed=True)["default"].mean().unstack(fill_value=0)
pivot_table = pivot_table.dropna(how="all").dropna(axis=1, how="all")
if not pivot_table.empty:
sns.heatmap(pivot_table, annot=True, fmt=".2%", cmap="RdYlGn_r", ax=ax4,
cbar_kws={"label": "Taxa de Default"}, linewidths=0.5)
ax4.set_title("Taxa de Default: Renda vs Tempo de Emprego", fontweight="bold")
ax4.set_xlabel("Tempo de Emprego", fontweight="bold")
ax4.set_ylabel("Renda", fontweight="bold")
plt.suptitle("Análise de Renda e Tempo de Emprego vs Default", fontsize=14, fontweight="bold")
plt.tight_layout()
plt.show()
# Estatísticas descritivas
print("\n📈 ESTATÍSTICAS DESCRITIVAS:")
print("\nRenda:")
print(df["renda"].describe())
print("\nTempo de Emprego:")
print(df["tempo_emprego"].describe())
print(f"\nCorrelação Renda vs Default: {df['renda'].corr(df['default']):.4f}")
print(f"Correlação Tempo de Emprego vs Default: {df['tempo_emprego'].corr(df['default']):.4f}")
📊 ANÁLISE: RENDA E TEMPO DE EMPREGO vs DEFAULT Taxa de Default por Categoria de Renda: Muito Baixa → Total: 211,647 | Taxa: 3.50% Baixa → Total: 211,647 | Taxa: 3.26% Média → Total: 211,647 | Taxa: 3.05% Alta → Total: 211,647 | Taxa: 2.76% Muito Alta → Total: 211,647 | Taxa: 2.20% Taxa de Default por Tempo de Emprego: 0-1 ano → Total: 43,954 | Taxa: 3.05% 1-3 anos → Total: 82,887 | Taxa: 3.03% 3-5 anos → Total: 75,866 | Taxa: 2.96% 5-10 anos → Total: 165,017 | Taxa: 3.02% 10+ anos → Total: 577,443 | Taxa: 2.95%
📈 ESTATÍSTICAS DESCRITIVAS: Renda: count 1.058235e+06 mean 3.627466e+03 std 2.954931e+03 min 8.000000e+01 25% 1.985219e+03 50% 2.981264e+03 75% 4.475706e+03 max 2.686568e+05 Name: renda, dtype: float64 Tempo de Emprego: count 1.079490e+06 mean 2.400717e+01 std 2.401012e+01 min 4.361330e-06 25% 6.908580e+00 50% 1.663424e+01 75% 3.328156e+01 max 3.225305e+02 Name: tempo_emprego, dtype: float64 Correlação Renda vs Default: -0.0230 Correlação Tempo de Emprego vs Default: -0.0038
💼 Implicações de Negócio
🎯 Principais Descobertas
1. Desbalanceamento Crítico de Classes
- Apenas ~3% de defaults na base, o que é típico em risco de crédito
- Impacto: Modelos podem ter tendência a prever sempre "não default"
- Ação: Necessário balanceamento de classes (SMOTE, undersampling) e métricas adequadas (Precision-Recall, F1-Score)
2. Score de Crédito como Variável Chave
- Score 300-500 apresenta taxa de default de 14.04% (4.7× maior que a média)
- Score 700+ tem taxa de default muito baixa (<1%)
- Ação: Score de crédito será feature prioritária no modelo, com categorização estratégica
3. Problemas Críticos de Qualidade de Dados
- Valores ausentes: Renda (~12%) e Tempo de Emprego (~10%)
- Dados categóricos mal formatados: Múltiplas variações do mesmo valor (ex: "solteiro", "SOLTEIRO", "solxeiro")
- Outliers: Distribuições assimétricas em renda e tempo de emprego
- Ação: Pré-processamento robusto será essencial (imputação, padronização, tratamento de outliers)
4. Correlações Baixas com Target
- Renda vs Default: -0.023 (correlação muito fraca)
- Tempo de Emprego vs Default: -0.004 (praticamente nula)
- Implicação: Variáveis isoladas têm pouco poder preditivo; interações e features derivadas serão importantes
5. Padrões Identificados
- Faixa etária 18-25 anos: Maior taxa de default (3.4%)
- Renda mais baixa: Taxa de default ligeiramente maior (3.5% vs 2.2% na mais alta)
- Valor do empréstimo: Relação inversa com score de crédito (clientes com score baixo pedem valores menores)
- Ação: Features de interação (ex: renda × tempo_emprego) podem melhorar o modelo
📋 Próximos Passos
- Pré-processamento: Tratamento de valores ausentes, padronização de categóricas, tratamento de outliers
- Feature Engineering: Criar features derivadas e interações entre variáveis
- Modelagem: Usar técnicas de balanceamento e métricas adequadas para dados desbalanceados
- Validação: Focar em identificar corretamente os defaults (recall) sem gerar muitos falsos positivos