aedi-provafinal / dashboard.py
Vinícius dos Santos Moreira
Revertendo últimas alterações, sem efeito.
a3b3b36
import streamlit as st
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import kagglehub
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler, LabelEncoder
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.cluster import KMeans, DBSCAN
from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score, roc_curve, accuracy_score, precision_score, recall_score, f1_score
import xgboost as xgb
import shap
# Configuração da Página
st.set_page_config(page_title="Dashboard de Risco de Crédito", layout="wide")
# --- FUNÇÕES DE CARREGAMENTO E PREPARAÇÃO DE DADOS ---
@st.cache_data
def load_data():
# Baixa o dataset do Kaggle (cacheado pelo Streamlit para não baixar sempre)
try:
path = "dataset"
file_path = f"{path}/credit_customers.csv"
df = pd.read_csv(file_path)
except Exception as e:
st.error(f"Erro ao baixar dados: {e}. Certifique-se de ter acesso à internet.")
return None
# Padronização dos nomes das colunas
df.columns = df.columns.str.lower().str.replace(" ", "_")
return df
@st.cache_data
def preprocess_data(df):
# Cópia para não alterar o original
df_proc = df.copy()
# Codificar a target 'class' (good=0, bad=1 para focar na inadimplência como classe positiva)
# Nota: No notebook original, a análise pode variar, mas aqui 'bad' é o evento de interesse (1)
target_map = {'good': 0, 'bad': 1}
df_proc['target'] = df_proc['class'].map(target_map)
df_proc = df_proc.drop(columns=['class'])
# Tratamento de variáveis categóricas (One-Hot Encoding)
# Ajuste de nomes de colunas problemáticos para modelos como XGBoost
df_encoded = pd.get_dummies(df_proc, drop_first=True, dtype=int)
# Limpeza de caracteres especiais nos nomes das colunas (para evitar erros no XGBoost/LightGBM)
df_encoded.columns = df_encoded.columns.str.replace("<", "lt_", regex=False)
df_encoded.columns = df_encoded.columns.str.replace(">", "gt_", regex=False)
df_encoded.columns = df_encoded.columns.str.replace("=", "eq_", regex=False)
df_encoded.columns = df_encoded.columns.str.replace("[", "", regex=False)
df_encoded.columns = df_encoded.columns.str.replace("]", "", regex=False)
return df_encoded
# --- CARREGAR DADOS ---
df_raw = load_data()
# Título
st.markdown("""
![Logo UnB](https://www.unb.br/templates/unb/assets/img/logo/as_comp_COR.png "Universidade de Brasília - UnB")\\
Programa de Pós-Graduação em Computação Aplicada - PPCA\\
Disciplina: Análise Estatística de Dados e Informações\\
Ano/Período: 2025/2\\
Prof. Dr. João Gabriel\\
Discente: 252107091 - Vinícius dos Santos Moreira
""")
st.title("Classificador de Risco de Crédito para Instituições Financeiras")
if df_raw is not None:
df_model = preprocess_data(df_raw)
# Separação X e y
X = df_model.drop(columns=['target'])
y = df_model['target']
# Split de treino e teste
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)
# --- BARRA LATERAL DE NAVEGAÇÃO ---
st.sidebar.title("Navegação")
page = st.sidebar.radio("Selecione a Seção:",
["a) Discussão do Problema",
"b) Análise Descritiva",
"c) Modelagem Preditiva",
"d) Explicabilidade (SHAP)",
"e) Análise Não Supervisionada",
"f) Tomada de Decisão"])
# --- PÁGINA A: DISCUSSÃO ---
if page == "a) Discussão do Problema":
st.title("📉 Contextualização do Risco de Crédito")
st.markdown("""
### O Problema de Negócio
O **risco de crédito** é a possibilidade de uma perda financeira resultante do falha de um tomador de empréstimo em pagar sua dívida total ou parcialmente. No setor bancário, gerenciar esse risco é o coração do negócio.
### Importância Econômica
* **Sustentabilidade Bancária:** A inadimplência elevada corrói o capital dos bancos, limitando sua capacidade de conceder novos empréstimos.
* **Custo do Crédito:** Quando o risco é mal mensurado, os bancos tendem a aumentar as taxas de juros (spread) para todos os clientes para cobrir as perdas, prejudicando bons pagadores e a economia real.
* **Estabilidade Sistêmica:** Uma gestão de risco ineficiente em larga escala pode levar a crises financeiras.
### Por que prever a inadimplência?
Prever se um cliente será um "mau pagador" (`bad`) ou "bom pagador" (`good`) permite:
1. **Reduzir Perdas:** Negar crédito ou ajustar limites para perfis de alto risco.
2. **Melhorar a Gestão:** Alocar capital de reserva (provisão) de forma mais eficiente.
3. **Estratégias Personalizadas:** Oferecer taxas melhores para bons clientes e exigir garantias para os mais arriscados.
""")
# --- PÁGINA B: ANÁLISE DESCRITIVA ---
elif page == "b) Análise Descritiva":
st.title("📊 Análise Exploratória de Dados (EDA)")
st.subheader("Visão Geral do Dataset")
st.dataframe(df_raw.head())
col1, col2 = st.columns(2)
with col1:
st.markdown("**Distribuição da Variável Alvo (Class)**")
fig, ax = plt.subplots()
sns.countplot(data=df_raw, x='class', palette='viridis', ax=ax)
st.pyplot(fig)
st.info("Observa-se o desbalanceamento das classes. A classe 'bad' (inadimplente) é geralmente a minoritária.")
with col2:
st.markdown("**Estatísticas Descritivas (Numéricas)**")
st.dataframe(df_raw.describe())
st.markdown("---")
st.subheader("Relação das Variáveis com a Classe")
# Seleção de variável para plotar
num_cols = df_raw.select_dtypes(include=np.number).columns.tolist()
cat_cols = df_raw.select_dtypes(include='object').columns.tolist()
cat_cols.remove('class') # Remover target da lista
plot_type = st.selectbox("Escolha o tipo de visualização:", ["Numérica (Boxplot)", "Categórica (Barras)"])
if plot_type == "Numérica (Boxplot)":
var_num = st.selectbox("Selecione a variável numérica:", num_cols)
fig, ax = plt.subplots(figsize=(10, 5))
sns.boxplot(data=df_raw, x='class', y=var_num, palette='Set2', ax=ax)
st.pyplot(fig, use_container_width=False)
else:
var_cat = st.selectbox("Selecione a variável categórica:", cat_cols)
fig, ax = plt.subplots(figsize=(10, 5))
sns.countplot(data=df_raw, x=var_cat, hue='class', palette='Set1', ax=ax)
plt.xticks(rotation=45)
st.pyplot(fig, use_container_width=False)
st.markdown("---")
st.subheader("Matriz de Correlação (Numéricas)")
fig, ax = plt.subplots(figsize=(10, 8))
sns.heatmap(df_raw[num_cols].corr(), annot=True, fmt=".2f", cmap='coolwarm', ax=ax)
st.pyplot(fig, use_container_width=False)
# --- PÁGINA C: MODELAGEM ---
elif page == "c) Modelagem Preditiva":
st.title("🤖 Definição e Seleção dos Modelos")
st.sidebar.header("Configuração do Modelo")
model_choice = st.sidebar.selectbox("Escolha o Algoritmo:",
["Random Forest", "XGBoost", "Regressão Logística", "Árvore de Decisão"])
# Instanciação dos modelos
if model_choice == "Random Forest":
model = RandomForestClassifier(n_estimators=100, random_state=42)
elif model_choice == "XGBoost":
model = xgb.XGBClassifier(use_label_encoder=False, eval_metric='logloss', random_state=42)
elif model_choice == "Regressão Logística":
model = LogisticRegression(max_iter=1000, random_state=42)
elif model_choice == "Árvore de Decisão":
model = DecisionTreeClassifier(random_state=42)
# Treinamento
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
y_prob = model.predict_proba(X_test)[:, 1]
# Métricas
acc = accuracy_score(y_test, y_pred)
prec = precision_score(y_test, y_pred)
rec = recall_score(y_test, y_pred)
f1 = f1_score(y_test, y_pred)
auc = roc_auc_score(y_test, y_prob)
st.subheader(f"Performance do Modelo: {model_choice}")
# Cards de Métricas
c1, c2, c3, c4, c5 = st.columns(5)
c1.metric("Acurácia", f"{acc:.2%}")
c2.metric("Precisão (Bad)", f"{prec:.2%}")
c3.metric("Recall (Bad)", f"{rec:.2%}")
c4.metric("F1-Score", f"{f1:.2f}")
c5.metric("AUC-ROC", f"{auc:.2f}")
col1, col2 = st.columns(2)
with col1:
st.markdown("### Matriz de Confusão")
cm = confusion_matrix(y_test, y_pred)
fig, ax = plt.subplots()
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=['Good', 'Bad'], yticklabels=['Good', 'Bad'])
plt.xlabel('Predito')
plt.ylabel('Real')
st.pyplot(fig)
with col2:
st.markdown("### Curva ROC")
fpr, tpr, _ = roc_curve(y_test, y_prob)
fig, ax = plt.subplots()
plt.plot(fpr, tpr, label=f"AUC = {auc:.2f}")
plt.plot([0, 1], [0, 1], 'k--')
plt.xlabel('Taxa de Falso Positivo (FPR)')
plt.ylabel('Taxa de Verdadeiro Positivo (TPR)')
plt.legend()
st.pyplot(fig)
st.markdown("""
**Justificativa da Escolha:**
* **Random Forest e XGBoost** geralmente performam melhor em dados tabulares complexos devido à capacidade de lidar com não-linearidades e interações entre variáveis.
* No contexto de crédito, o **Recall** (capturar os maus pagadores) é crucial, pois o custo de um calote é geralmente maior que o custo de oportunidade de negar um bom cliente.
""")
# --- PÁGINA D: SHAP ---
elif page == "d) Explicabilidade (SHAP)":
st.title("🔍 Explicabilidade com SHAP Values")
st.info("Calculando valores SHAP para o modelo XGBoost (padrão para esta análise)...")
# Treinar um modelo fixo para análise SHAP estável
model_shap = xgb.XGBClassifier(use_label_encoder=False, eval_metric='logloss', random_state=42)
model_shap.fit(X_train, y_train)
# Criar Explainer
explainer = shap.TreeExplainer(model_shap)
shap_values = explainer.shap_values(X_test)
st.subheader("Importância Global das Variáveis (Summary Plot)")
st.markdown("Este gráfico mostra quais variáveis mais impactam na decisão do modelo. Pontos à direita aumentam a probabilidade de ser 'Bad' (Inadimplente).")
fig, ax = plt.subplots()
shap.summary_plot(shap_values, X_test, show=False)
st.pyplot(fig, use_container_width=False)
st.markdown("""
**Interpretação:**
* **checking_status (Saldo da Conta):** Geralmente a variável mais forte. Saldos negativos ou baixos tendem a aumentar o risco (SHAP positivo).
* **duration (Duração):** Empréstimos mais longos costumam ter maior risco acumulado.
* **credit_amount:** Valores muito altos podem indicar maior dificuldade de pagamento.
""")
# --- PÁGINA E: CLUSTERIZAÇÃO ---
elif page == "e) Análise Não Supervisionada":
st.title("🧩 Segmentação de Clientes (Clustering)")
# Preparação dos dados para clusterização (apenas numéricos e normalizados)
features_cluster = df_raw.select_dtypes(include=np.number).columns.tolist()
# Remover target se estiver lá
features_cluster = [f for f in features_cluster if f != 'class']
df_cluster = df_raw[features_cluster].dropna()
scaler = StandardScaler()
X_scaled = scaler.fit_transform(df_cluster)
tab1, tab2 = st.tabs(["K-Means", "DBSCAN (Outliers)"])
with tab1:
st.subheader("K-Means Clustering")
k = st.slider("Número de Clusters (K):", 2, 8, 3)
kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
clusters = kmeans.fit_predict(X_scaled)
df_cluster['Cluster_KMeans'] = clusters
# Plot 2D simples usando duas variáveis principais
x_axis = st.selectbox("Eixo X para plot:", features_cluster, index=0)
y_axis = st.selectbox("Eixo Y para plot:", features_cluster, index=1)
fig, ax = plt.subplots(figsize=(10, 6))
sns.scatterplot(data=df_cluster, x=x_axis, y=y_axis, hue='Cluster_KMeans', palette='viridis', s=100, ax=ax)
plt.title(f"Segmentação K-Means (k={k})")
st.pyplot(fig, use_container_width=False)
st.markdown("**Interpretação dos Perfis:**")
st.dataframe(df_cluster.groupby('Cluster_KMeans').mean())
with tab2:
st.subheader("DBSCAN - Detecção de Outliers")
eps = st.slider("Epsilon (eps):", 0.1, 5.0, 2.0, 0.1)
min_samples = st.slider("Min Samples:", 2, 20, 5)
dbscan = DBSCAN(eps=eps, min_samples=min_samples)
clusters_db = dbscan.fit_predict(X_scaled)
df_cluster['Cluster_DBSCAN'] = clusters_db
# O DBSCAN rotula outliers como -1
outliers_count = (df_cluster['Cluster_DBSCAN'] == -1).sum()
st.metric("Número de Outliers Detectados", outliers_count)
fig, ax = plt.subplots(figsize=(10, 6))
# Plotar normais
sns.scatterplot(data=df_cluster[df_cluster['Cluster_DBSCAN'] != -1], x='credit_amount', y='age', label='Normal', color='gray', alpha=0.5, ax=ax)
# Plotar outliers
if outliers_count > 0:
sns.scatterplot(data=df_cluster[df_cluster['Cluster_DBSCAN'] == -1], x='credit_amount', y='age', label='Outlier', color='red', s=100, ax=ax)
plt.title("Detecção de Perfis Atípicos (DBSCAN)")
st.pyplot(fig, use_container_width=False)
st.markdown("Os pontos em **vermelho** (-1) são perfis atípicos que podem indicar risco elevado ou fraude, pois não se agrupam com o comportamento padrão dos clientes.")
# --- PÁGINA F: DECISÃO ESTRATÉGICA ---
elif page == "f) Tomada de Decisão":
st.title("🚀 Tomada de Decisão Estratégica")
st.markdown("""
Com base nos resultados obtidos pelos modelos supervisionados e na análise de clusters, sugerem-se as seguintes ações para a instituição financeira:
### 1. Políticas de Concessão Diferenciadas
* **Segmento de Alto Risco (Predito 'Bad'):** Implementar políticas mais rígidas. Exigir garantias (fiador, bens) ou limitar o valor do crédito (`credit_amount`) e o prazo (`duration`).
* **Checking Status:** Clientes com saldo de conta corrente negativo ou baixo demonstraram alto risco (via SHAP). Para estes, a aprovação automática deve ser suspensa.
### 2. Segmentação de Clientes (K-Means)
* Utilizar os clusters para criar produtos específicos.
* *Exemplo:* Se um cluster é formado por jovens com baixo crédito, criar um produto de "primeiro crédito" com limites baixos e foco em educação financeira.
* *Exemplo:* Clientes mais velhos com alto montante podem receber ofertas de crédito consignado ou imobiliário.
### 3. Prevenção e Monitoramento (DBSCAN)
* Os **outliers** detectados pelo DBSCAN devem passar por uma mesa de crédito manual (mesa de análise). Esses casos fogem do padrão e podem representar tanto fraudes quanto oportunidades de nicho não exploradas.
### 4. Concessão Responsável
* A análise de dados orienta a não endividar clientes que têm alta probabilidade de default, protegendo a saúde financeira tanto do banco quanto do consumidor.
""")