dcga's picture
Update app.py
bcd2046 verified
"""
===========================================================
PROVA FINAL - ANÁLISE DE RISCO DE CRÉDITO - CREDIFAST
==========================================================
Aluno: Daniel Coser Gonçalves de Araujo
Matrícula: 200033638
Disciplina: Sistemas de Informação em Engenharia de Produção (SIEP)
Professor: João Gabriel de Moraes Souza
Universidade de Brasília - UnB
==========================================================
"""
import streamlit as st
import pandas as pd
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
plt.ioff() # Desabilitar modo interativo
import seaborn as sns
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import warnings
warnings.filterwarnings('ignore')
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.preprocessing import StandardScaler, LabelEncoder
from sklearn.metrics import (accuracy_score, precision_score, recall_score,
f1_score, roc_auc_score, roc_curve,
confusion_matrix, classification_report)
from sklearn.neighbors import KNeighborsClassifier
from sklearn.svm import SVC
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import (RandomForestClassifier, AdaBoostClassifier,
GradientBoostingClassifier)
from sklearn.neural_network import MLPClassifier
from xgboost import XGBClassifier
from lightgbm import LGBMClassifier
from imblearn.over_sampling import SMOTE
from sklearn.cluster import KMeans, DBSCAN
from sklearn.decomposition import PCA
import shap
st.set_page_config(
page_title="CrediFast - Análise de Risco de Crédito",
page_icon="💳",
layout="wide",
initial_sidebar_state="expanded"
)
st.markdown("""
<style>
.main-header {
font-size: 2.5rem;
font-weight: bold;
color: #1E3A8A;
text-align: center;
margin-bottom: 0.5rem;
}
.sub-header {
font-size: 1.2rem;
color: #6B7280;
text-align: center;
margin-bottom: 2rem;
}
.metric-card {
background-color: #F3F4F6;
border-radius: 10px;
padding: 20px;
text-align: center;
}
.section-header {
font-size: 1.8rem;
font-weight: bold;
color: #1E3A8A;
border-bottom: 3px solid #3B82F6;
padding-bottom: 10px;
margin-top: 2rem;
}
.info-box {
background-color: #1E3A5F;
border-left: 4px solid #3B82F6;
padding: 15px;
margin: 10px 0;
border-radius: 0 8px 8px 0;
color: #FFFFFF;
}
.info-box h4 {
color: #93C5FD;
}
.info-box p, .info-box li {
color: #E5E7EB;
}
.info-box code {
background-color: #374151;
color: #FCD34D;
padding: 2px 6px;
border-radius: 4px;
}
.warning-box {
background-color: #78350F;
border-left: 4px solid #F59E0B;
padding: 15px;
margin: 10px 0;
border-radius: 0 8px 8px 0;
color: #FFFFFF;
}
.warning-box h4 {
color: #FCD34D;
}
.warning-box p, .warning-box li {
color: #E5E7EB;
}
.success-box {
background-color: #064E3B;
border-left: 4px solid #10B981;
padding: 15px;
margin: 10px 0;
border-radius: 0 8px 8px 0;
color: #FFFFFF;
}
.success-box h4 {
color: #6EE7B7;
}
.success-box p, .success-box li {
color: #E5E7EB;
}
</style>
""", unsafe_allow_html=True)
# =============================================================================
# FUNÇÕES DE CARREGAMENTO E PROCESSAMENTO DE DADOS
# =============================================================================
@st.cache_data
def load_data():
"""Carrega e prepara os dados do dataset de risco de crédito."""
url = "https://raw.githubusercontent.com/danielcoservalor/credit_data/refs/heads/main/credit_risk_dataset.csv"
df = pd.read_csv(url)
return df
@st.cache_data
def preprocess_data(df):
"""Preprocessa os dados para modelagem."""
df_processed = df.copy()
# Tratamento de valores ausentes
# Preencher valores numéricos com a mediana
numeric_cols = df_processed.select_dtypes(include=[np.number]).columns
for col in numeric_cols:
if df_processed[col].isnull().sum() > 0:
df_processed[col].fillna(df_processed[col].median(), inplace=True)
# Tratamento de outliers extremos em person_age (valores > 100)
df_processed = df_processed[df_processed['person_age'] <= 100]
# Tratamento de outliers em person_emp_length (valores > 60)
df_processed = df_processed[df_processed['person_emp_length'] <= 60]
return df_processed
@st.cache_data
def encode_features(df):
"""Codifica variáveis categóricas."""
df_encoded = df.copy()
# Label encoding para variáveis categóricas
categorical_cols = ['person_home_ownership', 'loan_intent', 'loan_grade', 'cb_person_default_on_file']
label_encoders = {}
for col in categorical_cols:
le = LabelEncoder()
df_encoded[col] = le.fit_transform(df_encoded[col].astype(str))
label_encoders[col] = le
return df_encoded, label_encoders
@st.cache_data
def prepare_model_data(df_encoded):
"""Prepara dados para modelagem."""
# Separar features e target
X = df_encoded.drop('loan_status', axis=1)
y = df_encoded['loan_status']
# Split treino/teste
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
# Escalonamento
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
return X_train, X_test, y_train, y_test, X_train_scaled, X_test_scaled, scaler, X.columns.tolist()
@st.cache_data
def apply_smote(X_train_scaled, y_train):
"""Aplica SMOTE para balanceamento."""
smote = SMOTE(random_state=42)
X_train_balanced, y_train_balanced = smote.fit_resample(X_train_scaled, y_train)
return X_train_balanced, y_train_balanced
# =============================================================================
# FUNÇÕES DE MODELAGEM
# =============================================================================
def train_models(X_train, y_train, X_test, y_test, feature_names):
"""Treina todos os modelos solicitados."""
models = {
'KNN': KNeighborsClassifier(n_neighbors=5),
'SVM': SVC(probability=True, random_state=42, kernel='rbf', C=1.0),
'Decision Tree': DecisionTreeClassifier(random_state=42, max_depth=10),
'Random Forest': RandomForestClassifier(n_estimators=100, random_state=42, n_jobs=-1),
'AdaBoost': AdaBoostClassifier(n_estimators=100, random_state=42),
'Gradient Boosting': GradientBoostingClassifier(n_estimators=100, random_state=42),
'XGBoost': XGBClassifier(n_estimators=100, random_state=42, use_label_encoder=False,
eval_metric='logloss', verbosity=0),
'LightGBM': LGBMClassifier(n_estimators=100, random_state=42, verbose=-1),
'MLP': MLPClassifier(hidden_layer_sizes=(100, 50), max_iter=500, random_state=42)
}
results = {}
trained_models = {}
progress_bar = st.progress(0)
status_text = st.empty()
for i, (name, model) in enumerate(models.items()):
status_text.text(f"Treinando {name}...")
# Treinar modelo
model.fit(X_train, y_train)
trained_models[name] = model
# Predições
y_pred = model.predict(X_test)
y_prob = model.predict_proba(X_test)[:, 1] if hasattr(model, 'predict_proba') else None
# Métricas
results[name] = {
'accuracy': accuracy_score(y_test, y_pred),
'precision': precision_score(y_test, y_pred),
'recall': recall_score(y_test, y_pred),
'f1': f1_score(y_test, y_pred),
'auc': roc_auc_score(y_test, y_prob) if y_prob is not None else None,
'y_pred': y_pred,
'y_prob': y_prob,
'confusion_matrix': confusion_matrix(y_test, y_pred)
}
progress_bar.progress((i + 1) / len(models))
status_text.text("Treinamento concluído!")
return results, trained_models
def get_best_model(results):
"""Identifica o melhor modelo baseado no AUC."""
best_name = max(results, key=lambda x: results[x]['auc'] if results[x]['auc'] else 0)
return best_name
# =============================================================================
# FUNÇÕES DE VISUALIZAÇÃO
# =============================================================================
def plot_class_distribution(y, title="Distribuição das Classes"):
"""Plota distribuição das classes."""
fig = px.pie(
values=y.value_counts().values,
names=['Good (0)', 'Bad (1)'],
title=title,
color_discrete_sequence=['#10B981', '#EF4444'],
hole=0.4
)
fig.update_traces(textposition='inside', textinfo='percent+label+value')
return fig
def plot_class_comparison(y_original, y_balanced):
"""Compara distribuição antes e depois do SMOTE."""
fig = make_subplots(rows=1, cols=2, specs=[[{'type':'pie'}, {'type':'pie'}]],
subplot_titles=['Antes do SMOTE', 'Após SMOTE'])
# Antes
fig.add_trace(go.Pie(
labels=['Good (0)', 'Bad (1)'],
values=y_original.value_counts().sort_index().values,
marker_colors=['#10B981', '#EF4444'],
hole=0.4,
name='Original'
), row=1, col=1)
# Depois
unique, counts = np.unique(y_balanced, return_counts=True)
fig.add_trace(go.Pie(
labels=['Good (0)', 'Bad (1)'],
values=counts,
marker_colors=['#10B981', '#EF4444'],
hole=0.4,
name='SMOTE'
), row=1, col=2)
fig.update_layout(title_text="Impacto do SMOTE no Balanceamento das Classes")
return fig
def plot_metrics_comparison(results):
"""Plota comparação de métricas entre modelos."""
df_results = pd.DataFrame({
'Modelo': list(results.keys()),
'Accuracy': [r['accuracy'] for r in results.values()],
'Precision': [r['precision'] for r in results.values()],
'Recall': [r['recall'] for r in results.values()],
'F1-Score': [r['f1'] for r in results.values()],
'AUC': [r['auc'] if r['auc'] else 0 for r in results.values()]
})
df_melted = df_results.melt(id_vars='Modelo', var_name='Métrica', value_name='Valor')
fig = px.bar(df_melted, x='Modelo', y='Valor', color='Métrica',
barmode='group', title='Comparação de Métricas por Modelo',
color_discrete_sequence=px.colors.qualitative.Set2)
fig.update_layout(xaxis_tickangle=-45)
return fig
def plot_roc_curves(results, y_test):
"""Plota curvas ROC de todos os modelos."""
fig = go.Figure()
colors = px.colors.qualitative.Set1
for i, (name, res) in enumerate(results.items()):
if res['y_prob'] is not None:
fpr, tpr, _ = roc_curve(y_test, res['y_prob'])
fig.add_trace(go.Scatter(
x=fpr, y=tpr,
mode='lines',
name=f"{name} (AUC={res['auc']:.3f})",
line=dict(color=colors[i % len(colors)])
))
# Linha diagonal
fig.add_trace(go.Scatter(
x=[0, 1], y=[0, 1],
mode='lines',
name='Random',
line=dict(color='gray', dash='dash')
))
fig.update_layout(
title='Curvas ROC - Comparação de Modelos',
xaxis_title='Taxa de Falsos Positivos (FPR)',
yaxis_title='Taxa de Verdadeiros Positivos (TPR)',
legend=dict(x=1.02, y=0.5)
)
return fig
def plot_confusion_matrix(cm, model_name):
"""Plota matriz de confusão."""
fig = px.imshow(
cm,
labels=dict(x="Predito", y="Real", color="Contagem"),
x=['Good (0)', 'Bad (1)'],
y=['Good (0)', 'Bad (1)'],
text_auto=True,
color_continuous_scale='Blues',
title=f'Matriz de Confusão - {model_name}'
)
return fig
def plot_feature_importance(model, feature_names, model_name):
"""Plota importância das features."""
if hasattr(model, 'feature_importances_'):
importances = model.feature_importances_
elif hasattr(model, 'coef_'):
importances = np.abs(model.coef_[0])
else:
return None
df_imp = pd.DataFrame({
'Feature': feature_names,
'Importance': importances
}).sort_values('Importance', ascending=True)
fig = px.bar(df_imp, x='Importance', y='Feature', orientation='h',
title=f'Importância das Features - {model_name}',
color='Importance', color_continuous_scale='Blues')
return fig
# =============================================================================
# FUNÇÕES DE CLUSTERIZAÇÃO
# ============================================================================
def perform_clustering(X_scaled, n_clusters=4):
"""Realiza clustering com KMeans."""
kmeans = KMeans(n_clusters=n_clusters, random_state=42, n_init=10)
clusters = kmeans.fit_predict(X_scaled)
return clusters, kmeans
def perform_dbscan(X_scaled, eps=0.5, min_samples=5):
"""Realiza DBSCAN para detecção de outliers."""
dbscan = DBSCAN(eps=eps, min_samples=min_samples)
labels = dbscan.fit_predict(X_scaled)
return labels, dbscan
def perform_pca(X_scaled, n_components=2):
"""Reduz dimensionalidade com PCA."""
pca = PCA(n_components=n_components)
X_pca = pca.fit_transform(X_scaled)
return X_pca, pca
def plot_clusters_pca(X_pca, clusters, title="Clusters Visualizados com PCA"):
"""Visualiza clusters em 2D usando PCA."""
df_pca = pd.DataFrame({
'PC1': X_pca[:, 0],
'PC2': X_pca[:, 1],
'Cluster': clusters.astype(str)
})
fig = px.scatter(df_pca, x='PC1', y='PC2', color='Cluster',
title=title,
color_discrete_sequence=px.colors.qualitative.Set1)
return fig
def plot_dbscan_outliers(X_pca, labels, title="Outliers Detectados pelo DBSCAN"):
"""Visualiza outliers detectados pelo DBSCAN."""
df_pca = pd.DataFrame({
'PC1': X_pca[:, 0],
'PC2': X_pca[:, 1],
'Tipo': ['Outlier' if l == -1 else 'Normal' for l in labels]
})
fig = px.scatter(df_pca, x='PC1', y='PC2', color='Tipo',
title=title,
color_discrete_map={'Outlier': '#EF4444', 'Normal': '#3B82F6'})
return fig
# =========================================================================
# FUNÇÕES SHAP
# =========================================================================
def compute_shap_values(model, X_test, feature_names, model_name):
"""Computa SHAP values para o modelo."""
try:
if model_name in ['Random Forest', 'XGBoost', 'LightGBM', 'Decision Tree',
'AdaBoost', 'Gradient Boosting']:
explainer = shap.TreeExplainer(model)
else:
# Para outros modelos, usar KernelExplainer com amostra
background = shap.sample(X_test, min(100, len(X_test)))
explainer = shap.KernelExplainer(model.predict_proba, background)
# Limitar amostras para performance
X_sample = X_test[:min(500, len(X_test))]
shap_values = explainer.shap_values(X_sample)
return explainer, shap_values, X_sample
except Exception as e:
st.warning(f"Não foi possível calcular SHAP values: {str(e)}")
return None, None, None
# ============================================================================
# INTERFACE PRINCIPAL
# ============================================================================
def main():
# Header
st.markdown('<h1 class="main-header">💳 CrediFast - Sistema de Análise de Risco de Crédito</h1>',
unsafe_allow_html=True)
st.markdown('''<p class="sub-header">
Dashboard Interativo para Predição de Inadimplência |
Prova Final | Daniel Coser Gonçalves de Araujo | 200033638
</p>''', unsafe_allow_html=True)
# Sidebar
st.sidebar.image("https://upload.wikimedia.org/wikipedia/commons/thumb/c/c3/Webysther_20160322_-_Logo_UnB_%28sem_texto%29.svg/1200px-Webysther_20160322_-_Logo_UnB_%28sem_texto%29.svg.png", width=100)
st.sidebar.markdown("### 📊 Navegação")
page = st.sidebar.radio(
"Selecione a seção:",
["🏠 Visão Geral",
"📊 I. Diagnóstico Inicial",
"🤖 II. Modelagem Supervisionada",
"🔍 III. Explicabilidade (SHAP)",
"📋 IV. Recomendações Gerenciais",
"🎯 V. Clusterização e Outliers",
"⚡ VI. Classificador Interativo"]
)
# Carregar dados
with st.spinner("Carregando dados..."):
df_raw = load_data()
df = preprocess_data(df_raw)
df_encoded, label_encoders = encode_features(df)
# Preparar dados para modelagem
(X_train, X_test, y_train, y_test,
X_train_scaled, X_test_scaled, scaler, feature_names) = prepare_model_data(df_encoded)
# Aplicar SMOTE
X_train_balanced, y_train_balanced = apply_smote(X_train_scaled, y_train)
# ============================================================================
# PÁGINA: VISÃO GERAL
# ============================================================================
if page == "🏠 Visão Geral":
st.markdown('<h2 class="section-header">Visão Geral do Projeto</h2>', unsafe_allow_html=True)
st.markdown("""
<div class="info-box">
<h4>📋 Contexto do Negócio</h4>
<p>A <strong>CrediFast</strong> é uma fintech especializada em empréstimos pessoais no modelo P2P (Peer-to-Peer),
conectando investidores a tomadores de crédito de maneira totalmente digital. Como a empresa não opera com
capital próprio, sua sobrevivência depende da capacidade de prever corretamente o risco de inadimplência.</p>
</div>
""", unsafe_allow_html=True)
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric("Total de Registros", f"{len(df):,}")
with col2:
st.metric("Features", f"{len(df.columns) - 1}")
with col3:
bad_rate = (df['loan_status'].sum() / len(df)) * 100
st.metric("Taxa de Inadimplência", f"{bad_rate:.1f}%")
with col4:
st.metric("Período de Análise", "2024-2025")
st.markdown("### 📁 Amostra dos Dados")
st.dataframe(df.head(10), use_container_width=True)
st.markdown("### 📊 Estatísticas Descritivas")
st.dataframe(df.describe(), use_container_width=True)
st.markdown("### 📋 Dicionário de Variáveis")
var_dict = pd.DataFrame({
'Variável': ['person_age', 'person_income', 'person_home_ownership', 'person_emp_length',
'loan_intent', 'loan_grade', 'loan_amnt', 'loan_int_rate',
'loan_status', 'loan_percent_income', 'cb_person_default_on_file',
'cb_person_cred_hist_length'],
'Descrição': [
'Idade do solicitante',
'Renda anual do solicitante',
'Tipo de residência (RENT, OWN, MORTGAGE, OTHER)',
'Tempo de emprego em anos',
'Finalidade do empréstimo',
'Classificação de risco do empréstimo (A-G)',
'Valor do empréstimo solicitado',
'Taxa de juros do empréstimo',
'Status do empréstimo (0=Bom, 1=Inadimplente) - TARGET',
'Percentual do empréstimo em relação à renda',
'Histórico de inadimplência (Y/N)',
'Tempo de histórico de crédito em anos'
],
'Tipo': ['Numérica', 'Numérica', 'Categórica', 'Numérica',
'Categórica', 'Categórica', 'Numérica', 'Numérica',
'Target (Binária)', 'Numérica', 'Categórica', 'Numérica']
})
st.dataframe(var_dict, use_container_width=True)
# ==========================================================================
# PÁGINA: DIAGNÓSTICO INICIAL
# ==========================================================================
elif page == "📊 I. Diagnóstico Inicial":
st.markdown('<h2 class="section-header">I. Diagnóstico Inicial e Variável-Alvo</h2>',
unsafe_allow_html=True)
st.markdown("""
<div class="info-box">
<h4>🎯 Declaração da Variável-Alvo</h4>
<p>A coluna <code>loan_status</code> é declarada como variável-alvo (target/class), onde:
<ul>
<li><strong>0 = Good</strong>: Cliente pagou o empréstimo integralmente (Fully Paid)</li>
<li><strong>1 = Bad</strong>: Cliente inadimplente (Default ou Charge Off)</li>
</ul>
</p>
</div>
""", unsafe_allow_html=True)
# Análise de proporção
st.markdown("### 📊 Proporção das Classes")
col1, col2 = st.columns(2)
with col1:
good_count = (df['loan_status'] == 0).sum()
bad_count = (df['loan_status'] == 1).sum()
total = len(df)
st.metric("Clientes Good (0)", f"{good_count:,} ({good_count/total*100:.1f}%)")
st.metric("Clientes Bad (1)", f"{bad_count:,} ({bad_count/total*100:.1f}%)")
ratio = good_count / bad_count
st.metric("Razão Good/Bad", f"{ratio:.2f}:1")
with col2:
fig = plot_class_distribution(df['loan_status'], "Distribuição Original das Classes")
st.plotly_chart(fig, use_container_width=True)
# Discussão sobre desbalanceamento
st.markdown("### ⚠️ Análise do Desbalanceamento")
st.markdown("""
<div class="warning-box">
<h4>Por que o Desbalanceamento é Problemático?</h4>
<p>O desbalanceamento entre as classes pode prejudicar significativamente os modelos de classificação,
especialmente em contextos de risco de crédito:</p>
<p><strong>🔴 Falsos Negativos (FN) - Maior Custo:</strong><br>
Classificar um cliente bad como good significa aprovar um empréstimo que provavelmente não será pago.
Para uma fintech P2P como a CrediFast, isso representa:</p>
<ul>
<li>Perda direta do capital emprestado</li>
<li>Perda de confiança dos investidores</li>
<li>Impacto na liquidez da plataforma</li>
<li>Custos de cobrança e recuperação</li>
</ul>
<p><strong>🟡 Falsos Positivos (FP) - Custo Moderado:</strong><br>
Negar crédito a um bom pagador representa:</p>
<ul>
<li>Perda de receita potencial</li>
<li>Redução da base de clientes</li>
<li>Oportunidade perdida de fidelização</li>
</ul>
<p><strong>⚡ Conclusão:</strong> Em risco de crédito, prioriza-se o <strong>Recall</strong> (capturar
o máximo de inadimplentes) mesmo que isso aumente falsos positivos, pois o custo do FN é muito maior.</p>
</div>
""", unsafe_allow_html=True)
# SMOTE
st.markdown("### 🔄 Aplicação do SMOTE (Synthetic Minority Over-sampling Technique)")
st.markdown("""
<div class="success-box">
<h4>Técnica de Balanceamento Escolhida: SMOTE</h4>
<p>O SMOTE foi selecionado por:</p>
<ul>
<li><strong>Criação de amostras sintéticas:</strong> Gera novos exemplos da classe minoritária
através de interpolação entre exemplos existentes</li>
<li><strong>Preservação da distribuição:</strong> Mantém as características estatísticas da classe minoritária</li>
<li><strong>Redução de overfitting:</strong> Diferente do oversampling simples, não replica exemplos idênticos</li>
<li><strong>Aplicação apenas no treino:</strong> Evita data leakage ao não modificar o conjunto de teste</li>
</ul>
</div>
""", unsafe_allow_html=True)
col1, col2 = st.columns(2)
with col1:
st.markdown("**Antes do SMOTE:**")
st.write(f"- Good: {(y_train == 0).sum():,}")
st.write(f"- Bad: {(y_train == 1).sum():,}")
with col2:
unique, counts = np.unique(y_train_balanced, return_counts=True)
st.markdown("**Após SMOTE:**")
st.write(f"- Good: {counts[0]:,}")
st.write(f"- Bad: {counts[1]:,}")
fig = plot_class_comparison(y_train, y_train_balanced)
st.plotly_chart(fig, use_container_width=True)
# Análise exploratória adicional
st.markdown("### 📈 Análise Exploratória das Variáveis")
tab1, tab2, tab3 = st.tabs(["Distribuições Numéricas", "Variáveis Categóricas", "Correlações"])
with tab1:
numeric_cols = ['person_age', 'person_income', 'loan_amnt', 'loan_int_rate',
'loan_percent_income', 'cb_person_cred_hist_length']
selected_var = st.selectbox("Selecione a variável:", numeric_cols)
fig = px.histogram(df, x=selected_var, color='loan_status',
barmode='overlay',
title=f'Distribuição de {selected_var} por Status',
color_discrete_map={0: '#10B981', 1: '#EF4444'},
labels={'loan_status': 'Status'})
st.plotly_chart(fig, use_container_width=True)
with tab2:
cat_cols = ['person_home_ownership', 'loan_intent', 'loan_grade', 'cb_person_default_on_file']
selected_cat = st.selectbox("Selecione a variável categórica:", cat_cols)
cross_tab = pd.crosstab(df[selected_cat], df['loan_status'], normalize='index') * 100
cross_tab.columns = ['Good (%)', 'Bad (%)']
fig = px.bar(cross_tab.reset_index(), x=selected_cat, y=['Good (%)', 'Bad (%)'],
barmode='group', title=f'Taxa de Inadimplência por {selected_cat}',
color_discrete_sequence=['#10B981', '#EF4444'])
st.plotly_chart(fig, use_container_width=True)
with tab3:
numeric_df = df.select_dtypes(include=[np.number])
corr_matrix = numeric_df.corr()
fig = px.imshow(corr_matrix,
labels=dict(color="Correlação"),
x=corr_matrix.columns,
y=corr_matrix.columns,
color_continuous_scale='RdBu_r',
title='Matriz de Correlação')
st.plotly_chart(fig, use_container_width=True)
# ==========================================================================
# PÁGINA: MODELAGEM SUPERVISIONADA
# ==========================================================================
elif page == "🤖 II. Modelagem Supervisionada":
st.markdown('<h2 class="section-header">II. Construção e Avaliação dos Modelos Supervisionados</h2>',
unsafe_allow_html=True)
st.markdown("""
<div class="info-box">
<h4>🤖 Modelos Treinados</h4>
<p>Os seguintes algoritmos serão implementados:</p>
<ul>
<li><strong>Modelos baseados em distância:</strong> KNN e SVM</li>
<li><strong>Modelos de árvores e bagging:</strong> Decision Tree e Random Forest</li>
<li><strong>Métodos de boosting:</strong> AdaBoost, Gradient Boosting, XGBoost e LightGBM</li>
<li><strong>Modelo neural:</strong> MLPClassifier</li>
</ul>
</div>
""", unsafe_allow_html=True)
# Treinar modelos
if st.button("🚀 Treinar Todos os Modelos", type="primary"):
with st.spinner("Treinando os modelos... Pode demorar alguns minutos, mas tá funcionando, professor!"):
results, trained_models = train_models(
X_train_balanced, y_train_balanced,
X_test_scaled, y_test, feature_names
)
# Salvar em session state
st.session_state['results'] = results
st.session_state['trained_models'] = trained_models
st.session_state['X_test_scaled'] = X_test_scaled
st.session_state['y_test'] = y_test
st.session_state['feature_names'] = feature_names
st.success("✅ Todos os modelos foram treinados com sucesso!")
# Verificar se já temos resultados
if 'results' in st.session_state:
results = st.session_state['results']
trained_models = st.session_state['trained_models']
# Tabela de resultados
st.markdown("### 📊 Comparação de Métricas")
df_results = pd.DataFrame({
'Modelo': list(results.keys()),
'Accuracy': [f"{r['accuracy']:.4f}" for r in results.values()],
'Precision': [f"{r['precision']:.4f}" for r in results.values()],
'Recall': [f"{r['recall']:.4f}" for r in results.values()],
'F1-Score': [f"{r['f1']:.4f}" for r in results.values()],
'AUC': [f"{r['auc']:.4f}" if r['auc'] else "N/A" for r in results.values()]
})
st.dataframe(df_results, use_container_width=True)
# Gráfico de comparação
fig = plot_metrics_comparison(results)
st.plotly_chart(fig, use_container_width=True)
# Curvas ROC
st.markdown("### 📈 Curvas ROC")
fig_roc = plot_roc_curves(results, y_test)
st.plotly_chart(fig_roc, use_container_width=True)
# Matrizes de confusão
st.markdown("### Matrizes de Confusão")
selected_model = st.selectbox("Selecione o modelo:", list(results.keys()))
col1, col2 = st.columns(2)
with col1:
fig_cm = plot_confusion_matrix(results[selected_model]['confusion_matrix'], selected_model)
st.plotly_chart(fig_cm, use_container_width=True)
with col2:
cm = results[selected_model]['confusion_matrix']
tn, fp, fn, tp = cm.ravel()
st.markdown(f"""
**Interpretação da Matriz de Confusão - {selected_model}:**
- **Verdadeiros Negativos (TN):** {tn:,} - Clientes bons corretamente identificados
- **Falsos Positivos (FP):** {fp:,} - Clientes bons incorretamente classificados como ruins
- **Falsos Negativos (FN):** {fn:,} - Clientes ruins incorretamente classificados como bons ⚠️
- **Verdadeiros Positivos (TP):** {tp:,} - Clientes ruins corretamente identificados
**Análise de Custos:**
- FN ({fn}) representa o maior risco financeiro: empréstimos aprovados que resultarão em inadimplência
- FP ({fp}) representa perda de receita potencial: bons clientes que foram rejeitados
""")
# Melhor modelo
best_model = get_best_model(results)
st.markdown(f"""
<div class="success-box">
<h4>🏆 Modelo de Melhor Desempenho: {best_model}</h4>
<p><strong>Justificativa Técnica:</strong></p>
<ul>
<li><strong>AUC = {results[best_model]['auc']:.4f}:</strong> Maior capacidade discriminativa entre classes</li>
<li><strong>Recall = {results[best_model]['recall']:.4f}:</strong> Alta taxa de detecção de inadimplentes</li>
<li><strong>F1-Score = {results[best_model]['f1']:.4f}:</strong> Bom equilíbrio entre precisão e recall</li>
</ul>
<p>Para o contexto da CrediFast, o {best_model} é recomendado por maximizar a detecção de
clientes de risco (recall) mantendo um bom equilíbrio com a precisão, minimizando assim
os custosos falsos negativos.</p>
</div>
""", unsafe_allow_html=True)
# Interpretação das métricas
st.markdown("### 📚 Interpretação das Métricas para o Negócio")
st.markdown("""
| Métrica | Significado no Contexto de Crédito | Importância para CrediFast |
|---------|-----------------------------------|---------------------------|
| **AUC** | Capacidade geral do modelo de distinguir bons e maus pagadores | Métrica principal para comparação de modelos |
| **Recall** | % de inadimplentes corretamente identificados | Crítico - alto recall = menos fraudes aprovadas |
| **Precision** | % de previsões de inadimplência que estão corretas | Importante - evita rejeitar bons clientes |
| **F1-Score** | Média harmônica entre precision e recall | Equilíbrio geral do modelo |
| **Accuracy** | % de previsões corretas totais | Menos relevante em dados desbalanceados |
""")
else:
st.info("👆 Clique no botão acima para treinar os modelos e visualizar os resultados.")
# ==========================================================================
# PÁGINA: EXPLICABILIDADE (SHAP)
# ==========================================================================
elif page == "🔍 III. Explicabilidade (SHAP)":
st.markdown('<h2 class="section-header">III. Explicabilidade com SHAP</h2>',
unsafe_allow_html=True)
if 'trained_models' not in st.session_state:
st.warning("⚠️ Por favor, treine os modelos primeiro na seção 'II. Modelagem Supervisionada'")
else:
results = st.session_state['results']
trained_models = st.session_state['trained_models']
best_model_name = get_best_model(results)
st.markdown(f"""
<div class="info-box">
<h4>🔍 Análise de Explicabilidade do Modelo: {best_model_name}</h4>
<p>SHAP (SHapley Additive exPlanations) permite entender como cada variável contribui
para as predições do modelo, tanto de forma global quanto individual.</p>
</div>
""", unsafe_allow_html=True)
# Selecionar modelo para análise SHAP
model_for_shap = st.selectbox(
"Selecione o modelo para análise SHAP:",
['LightGBM', 'XGBoost', 'Random Forest', 'Gradient Boosting'],
index=0,
key='shap_model_selector'
)
if st.button("🔬 Calcular SHAP Values", type="primary", key='calc_shap_btn'):
with st.spinner("Calculando SHAP values... Isso pode levar alguns minutos."):
model = trained_models[model_for_shap]
# Usar TreeExplainer para modelos de árvore
try:
explainer = shap.TreeExplainer(model)
X_sample = X_test_scaled[:500]
shap_values = explainer.shap_values(X_sample)
# Para modelos de classificação binária
if isinstance(shap_values, list):
shap_values = shap_values[1] # Classe positiva (bad)
# Obter base_value corretamente
expected_val = explainer.expected_value
if isinstance(expected_val, (list, np.ndarray)):
if len(expected_val) > 1:
base_val = float(expected_val[1])
else:
base_val = float(expected_val[0])
else:
base_val = float(expected_val)
st.session_state['shap_explainer'] = explainer
st.session_state['shap_values'] = shap_values
st.session_state['X_sample_shap'] = X_sample
st.session_state['shap_model'] = model_for_shap
st.session_state['shap_base_val'] = base_val
st.success("✅ SHAP values calculados com sucesso!")
except Exception as e:
st.error(f"Erro ao calcular SHAP values: {str(e)}")
if 'shap_values' in st.session_state:
shap_values = st.session_state['shap_values']
X_sample = st.session_state['X_sample_shap']
base_val = st.session_state.get('shap_base_val', 0)
st.markdown("### 📊 Summary Plot - Visão Global")
st.markdown("""
<div class="info-box">
<p>O <strong>Summary Plot</strong> mostra a importância global de cada variável e como
seus valores afetam as predições:</p>
<ul>
<li>Features ordenadas por importância (de cima para baixo)</li>
<li>Cores indicam valores das features (vermelho = alto, azul = baixo)</li>
<li>Posição horizontal indica impacto na predição (direita = aumenta risco)</li>
</ul>
</div>
""", unsafe_allow_html=True)
# Summary plot com matplotlib - usando container para estabilizar
summary_container = st.container()
with summary_container:
fig_summary = plt.figure(figsize=(10, 8))
shap.summary_plot(shap_values, X_sample, feature_names=feature_names,
plot_type="dot", show=False)
st.pyplot(fig_summary, clear_figure=True)
plt.close(fig_summary)
# Análise das principais variáveis
st.markdown("### 📈 Análise das Variáveis Mais Importantes")
# Calcular importância média
shap_importance = np.abs(shap_values).mean(0)
importance_df = pd.DataFrame({
'Feature': feature_names,
'Importância SHAP': shap_importance
}).sort_values('Importância SHAP', ascending=False)
col1, col2 = st.columns([1, 2])
with col1:
st.dataframe(importance_df, use_container_width=True, hide_index=False)
with col2:
fig_bar = px.bar(importance_df.head(10), x='Importância SHAP', y='Feature',
orientation='h', title='Top 10 Variáveis Mais Importantes',
color='Importância SHAP', color_continuous_scale='Blues')
fig_bar.update_layout(yaxis={'categoryorder': 'total ascending'})
st.plotly_chart(fig_bar, use_container_width=True, key='shap_importance_chart')
# Interpretação detalhada
st.markdown("""
<div class="success-box">
<h4>🔎 Interpretação das Principais Variáveis</h4>
<p><strong>1. loan_percent_income (% do empréstimo em relação à renda):</strong><br>
Valores ALTOS (vermelho à direita) → AUMENTAM o risco de inadimplência.<br>
<em>Interpretação:</em> Clientes que comprometem grande parte da renda com o empréstimo
têm maior probabilidade de default.</p>
<p><strong>2. loan_int_rate (Taxa de juros):</strong><br>
Valores ALTOS → AUMENTAM significativamente o risco.<br>
<em>Interpretação:</em> Taxas elevadas geralmente são atribuídas a clientes de maior risco,
criando um ciclo de dificuldade de pagamento.</p>
<p><strong>3. loan_grade (Classificação do empréstimo):</strong><br>
Valores ALTOS (grades piores: E, F, G) → AUMENTAM o risco.<br>
<em>Interpretação:</em> A classificação prévia do empréstimo é um forte preditor de inadimplência.</p>
<p><strong>4. person_income (Renda):</strong><br>
Valores BAIXOS (azul à direita) → AUMENTAM o risco.<br>
<em>Interpretação:</em> Menor renda implica menor capacidade de pagamento.</p>
<p><strong>5. cb_person_default_on_file (Histórico de inadimplência):</strong><br>
Valor = 1 (Sim) → AUMENTA significativamente o risco.<br>
<em>Interpretação:</em> Histórico negativo é forte preditor de comportamento futuro.</p>
</div>
""", unsafe_allow_html=True)
# Análise individual (Force/Waterfall plots)
st.markdown("### 🎯 Análise Individual - Force Plots")
# Encontrar exemplos good e bad
y_test_array = np.array(y_test)
# Encontrar índices de exemplos good e bad na amostra
good_indices = np.where(y_test_array[:500] == 0)[0]
bad_indices = np.where(y_test_array[:500] == 1)[0]
if len(good_indices) > 0 and len(bad_indices) > 0:
tab1, tab2 = st.tabs(["Cliente GOOD (Bom Pagador)", "Cliente BAD (Inadimplente)"])
with tab1:
st.markdown("#### Análise de um Cliente Classificado como GOOD")
idx_good = good_indices[0]
# Waterfall plot usando container estável
wf_container1 = st.container()
with wf_container1:
fig_wf = plt.figure(figsize=(10, 6))
shap.waterfall_plot(shap.Explanation(
values=shap_values[idx_good],
base_values=base_val,
data=X_sample[idx_good],
feature_names=feature_names
), show=False)
st.pyplot(fig_wf, clear_figure=True)
plt.close(fig_wf)
st.markdown("""
**Interpretação:** Este cliente foi classificado como bom pagador porque:
- Variáveis que REDUZEM o risco (barras azuis apontando para esquerda) dominam
- Baixo comprometimento de renda com o empréstimo
- Boa classificação de crédito (loan_grade baixo)
- Sem histórico de inadimplência
""")
with tab2:
st.markdown("#### Análise de um Cliente Classificado como BAD")
idx_bad = bad_indices[0]
# Waterfall plot usando container estável
wf_container2 = st.container()
with wf_container2:
fig_wf2 = plt.figure(figsize=(10, 6))
shap.waterfall_plot(shap.Explanation(
values=shap_values[idx_bad],
base_values=base_val,
data=X_sample[idx_bad],
feature_names=feature_names
), show=False)
st.pyplot(fig_wf2, clear_figure=True)
plt.close(fig_wf2)
st.markdown("""
**Interpretação:** Este cliente foi classificado como inadimplente porque:
- Variáveis que AUMENTAM o risco (barras vermelhas apontando para direita) dominam
- Alto comprometimento da renda (loan_percent_income elevado)
- Taxa de juros alta (indicando risco prévio identificado)
- Possível histórico de inadimplência anterior
""")
# ==========================================================================
# PÁGINA: RECOMENDAÇÕES GERENCIAIS
# ==========================================================================
elif page == "📋 IV. Recomendações Gerenciais":
st.markdown('<h2 class="section-header">IV. Recomendações Gerenciais Baseadas nos Resultados</h2>',
unsafe_allow_html=True)
st.markdown("""
<div class="info-box">
<h4>📋 Síntese das Descobertas para a Diretoria da CrediFast</h4>
<p>Com base nas análises de modelagem supervisionada e explicabilidade SHAP,
apresentamos as seguintes recomendações estratégicas para redução da inadimplência
e melhoria da eficiência operacional.</p>
</div>
""", unsafe_allow_html=True)
# Recomendação 1
st.markdown("### 🎯 1. Revisão de Limites de Crédito")
col1, col2 = st.columns([2, 1])
with col1:
st.markdown("""
**Evidência:** A variável `loan_percent_income` (% do empréstimo em relação à renda)
é o principal preditor de inadimplência.
**Recomendação:**
- Implementar limite máximo de comprometimento de renda de **35%** para novos empréstimos
- Para clientes com histórico positivo, permitir até **45%** com aprovação especial
- Criar alertas automáticos quando solicitações excedem **30%** da renda
**Impacto Esperado:** Redução de 15-20% na taxa de inadimplência em novos empréstimos.
""")
with col2:
fig = go.Figure(go.Indicator(
mode="gauge+number",
value=35,
title={'text': "Limite Recomendado (%)"},
gauge={'axis': {'range': [0, 100]},
'bar': {'color': "#3B82F6"},
'steps': [
{'range': [0, 35], 'color': "#D1FAE5"},
{'range': [35, 50], 'color': "#FEF3C7"},
{'range': [50, 100], 'color': "#FEE2E2"}
]}
))
st.plotly_chart(fig, use_container_width=True)
# Recomendação 2
st.markdown("### 📊 2. Criação de Categorias de Risco Refinadas")
st.markdown("""
**Evidência:** As variáveis `loan_grade`, `loan_int_rate` e `cb_person_default_on_file`
apresentam forte poder preditivo.
**Nova Matriz de Risco Proposta:**
""")
risk_matrix = pd.DataFrame({
'Categoria': ['Ultra Baixo', 'Baixo', 'Moderado', 'Alto', 'Muito Alto', 'Crítico'],
'Score': ['0-10', '11-25', '26-45', '46-65', '66-85', '86-100'],
'Características': [
'Grade A, sem histórico negativo, income > 100k',
'Grade A-B, loan_percent_income < 20%',
'Grade B-C, sem histórico negativo',
'Grade C-D ou histórico negativo anterior',
'Grade D-E, alto comprometimento de renda',
'Grade F-G, múltiplos fatores de risco'
],
'Taxa Sugerida': ['Base', 'Base + 1%', 'Base + 3%', 'Base + 5%', 'Base + 8%', 'Análise especial'],
'Ação': ['Aprovação automática', 'Aprovação rápida', 'Análise padrão',
'Verificação adicional', 'Comitê de crédito', 'Possível recusa']
})
st.dataframe(risk_matrix, use_container_width=True)
# Recomendação 3
st.markdown("### 🔍 3. Verificações Complementares por Perfil")
col1, col2 = st.columns(2)
with col1:
st.markdown("""
**Perfis que Exigem Verificação Adicional:**
1. **Clientes com histórico de inadimplência (cb_person_default_on_file = Y)**
- Exigir comprovante de quitação de dívidas anteriores
- Solicitar fiador ou garantia adicional
- Limite inicial reduzido em 50%
2. **Empréstimos > 40% da renda**
- Análise detalhada de despesas fixas
- Verificação de outras dívidas ativas
- Aprovação por comitê
3. **Clientes jovens (< 25 anos) com pouco histórico**
- Score de crédito alternativo (redes sociais, utilities)
- Limite progressivo baseado em comportamento
""")
with col2:
st.markdown("""
**Perfis com Aprovação Facilitada:**
1. **Funcionários estáveis (emp_length > 5 anos)**
- Processo simplificado
- Taxas preferenciais
2. **Proprietários de imóvel (home_ownership = OWN/MORTGAGE)**
- Menor risco comprovado nos dados
- Limites maiores disponíveis
3. **Histórico de crédito longo (> 5 anos) sem ocorrências**
- Pré-aprovação automática
- Programa de fidelidade
""")
# Recomendação 4
st.markdown("### 📈 4. Monitoramento e Acompanhamento")
st.markdown("""
**Sistema de Early Warning (Alerta Antecipado):**
Com base nos SHAP values, implementar monitoramento contínuo de:
| Indicador | Threshold de Alerta | Ação |
|-----------|---------------------|------|
| Atraso no pagamento | > 5 dias | SMS/Email automático |
| Score de risco aumentou | > 15 pontos | Contato proativo |
| Múltiplas consultas de crédito | > 3/mês | Análise de comportamento |
| Solicitação de aumento de limite | Em período de risco | Bloqueio temporário |
""")
# Recomendação 5
st.markdown("### 📚 5. Políticas de Educação Financeira")
st.markdown("""
<div class="success-box">
<h4>Programa "CrediFast Consciente"</h4>
<p><strong>Público-alvo:</strong> Clientes nas categorias de risco "Alto" e "Muito Alto"</p>
<p><strong>Componentes:</strong></p>
<ul>
<li>Curso online obrigatório antes da liberação do empréstimo (2 horas)</li>
<li>Calculadora de capacidade de pagamento integrada ao app</li>
<li>Alertas personalizados sobre comprometimento de renda</li>
<li>Desconto na taxa de juros para quem completar o programa (+0.5%)</li>
</ul>
<p><strong>Impacto esperado:</strong> Redução de 10% na inadimplência do grupo de alto risco</p>
</div>
""", unsafe_allow_html=True)
# Síntese Final
st.markdown("### 🎯 Síntese: Impacto Esperado das Recomendações")
impact_data = pd.DataFrame({
'Iniciativa': ['Limites de crédito', 'Categorias de risco', 'Verificações complementares',
'Monitoramento proativo', 'Educação financeira'],
'Redução Inadimplência (%)': [18, 12, 15, 8, 10],
'Custo Implementação': ['Baixo', 'Médio', 'Médio', 'Alto', 'Baixo'],
'Prazo (meses)': [1, 3, 2, 6, 4]
})
fig = px.bar(impact_data, x='Iniciativa', y='Redução Inadimplência (%)',
color='Custo Implementação',
title='Impacto Esperado por Iniciativa',
color_discrete_map={'Baixo': '#10B981', 'Médio': '#F59E0B', 'Alto': '#EF4444'})
st.plotly_chart(fig, use_container_width=True)
st.markdown("""
<div class="info-box">
<h4>📌 Conclusão Executiva</h4>
<p>A implementação conjunta das recomendações acima pode resultar em uma <strong>redução
de até 40% na taxa de inadimplência</strong> da CrediFast em 12 meses, mantendo o
crescimento saudável da base de clientes através de políticas de crédito mais inteligentes
e baseadas em dados.</p>
<p>O modelo de machine learning desenvolvido (LightGBM/XGBoost) deve ser integrado ao
sistema de decisão de crédito para scoring automático, com revisão trimestral dos
parâmetros baseada no desempenho real da carteira.</p>
</div>
""", unsafe_allow_html=True)
# ==========================================================================
# PÁGINA: Clusterizacao e Outliers
# ==========================================================================
elif page == "🎯 V. Clusterização e Outliers":
st.markdown('<h2 class="section-header">V. Clusterização e Outliers</h2>',
unsafe_allow_html=True)
st.markdown("""
<div class="info-box">
<h4>🎯 Objetivo da Análise</h4>
<p>Segmentar clientes em grupos homogêneos (sem usar a variável-alvo) e detectar
outliers que podem representar riscos adicionais ou oportunidades especiais.</p>
</div>
""", unsafe_allow_html=True)
# Preparar dados para clustering (sem a variável alvo)
X_cluster = X_test_scaled
# PCA para visualização
X_pca, pca = perform_pca(X_cluster)
st.markdown(f"""
**Variância explicada pelo PCA:**
- PC1: {pca.explained_variance_ratio_[0]*100:.1f}%
- PC2: {pca.explained_variance_ratio_[1]*100:.1f}%
- Total: {sum(pca.explained_variance_ratio_)*100:.1f}%
""")
# KMeans
st.markdown("### 🔵 Segmentação com KMeans")
n_clusters = st.slider("Número de clusters:", 2, 8, 4)
clusters, kmeans = perform_clustering(X_cluster, n_clusters)
col1, col2 = st.columns(2)
with col1:
fig_clusters = plot_clusters_pca(X_pca, clusters, f"Clusters KMeans (k={n_clusters})")
st.plotly_chart(fig_clusters, use_container_width=True)
with col2:
# Análise de clusters vs inadimplência
cluster_analysis = pd.DataFrame({
'Cluster': clusters,
'loan_status': y_test.values
})
cluster_stats = cluster_analysis.groupby('Cluster').agg({
'loan_status': ['count', 'sum', 'mean']
}).round(3)
cluster_stats.columns = ['Total', 'Inadimplentes', 'Taxa Inadimplência']
cluster_stats['Taxa Inadimplência'] = (cluster_stats['Taxa Inadimplência'] * 100).round(1).astype(str) + '%'
st.markdown("**Análise de Inadimplência por Cluster:**")
st.dataframe(cluster_stats, use_container_width=True)
# Características dos clusters
st.markdown("### 📊 Características dos Clusters")
# Adicionar cluster aos dados originais para análise
X_test_df = pd.DataFrame(X_test_scaled, columns=feature_names)
X_test_df['Cluster'] = clusters
# Estatísticas por cluster
cluster_profiles = X_test_df.groupby('Cluster').mean()
fig_heatmap = px.imshow(cluster_profiles.T,
labels=dict(x="Cluster", y="Feature", color="Valor Médio (Normalizado)"),
title="Perfil Médio dos Clusters",
color_continuous_scale='RdBu_r',
aspect='auto')
st.plotly_chart(fig_heatmap, use_container_width=True)
# Interpretação dos clusters
st.markdown("""
<div class="success-box">
<h4>🔍 Interpretação dos Clusters</h4>
<p>Com base no perfil médio, podemos caracterizar os clusters:</p>
<ul>
<li><strong>Cluster com menor taxa de inadimplência:</strong> Geralmente apresenta menor
comprometimento de renda, renda mais alta e melhor grade de crédito</li>
<li><strong>Cluster com maior taxa de inadimplência:</strong> Caracterizado por alto
comprometimento de renda, taxas de juros elevadas e possível histórico negativo</li>
</ul>
<p>Estes clusters podem ser usados para estratégias de marketing e políticas de crédito diferenciadas.</p>
</div>
""", unsafe_allow_html=True)
# DBSCAN para outliers
st.markdown("### 🔴Detecção de Outliers com DBSCAN")
col1, col2 = st.columns(2)
with col1:
eps = st.slider("Parâmetro eps:", 0.1, 2.0, 0.8, 0.1)
with col2:
min_samples = st.slider("Min samples:", 3, 20, 10)
labels_dbscan, dbscan = perform_dbscan(X_cluster, eps, min_samples)
n_outliers = (labels_dbscan == -1).sum()
n_normal = (labels_dbscan != -1).sum()
col1, col2, col3 = st.columns(3)
with col1:
st.metric("Total de Outliers", f"{n_outliers:,}")
with col2:
st.metric("Pontos Normais", f"{n_normal:,}")
with col3:
st.metric("% Outliers", f"{n_outliers/len(labels_dbscan)*100:.1f}%")
# Visualização dos outliers
fig_outliers = plot_dbscan_outliers(X_pca, labels_dbscan)
st.plotly_chart(fig_outliers, use_container_width=True)
# Análise dos outliers vs inadimplência
st.markdown("### 📈 Outliers e Risco de Inadimplência")
outlier_analysis = pd.DataFrame({
'Tipo': ['Outlier' if l == -1 else 'Normal' for l in labels_dbscan],
'loan_status': y_test.values
})
outlier_stats = outlier_analysis.groupby('Tipo').agg({
'loan_status': ['count', 'sum', 'mean']
})
outlier_stats.columns = ['Total', 'Inadimplentes', 'Taxa Inadimplência']
col1, col2 = st.columns(2)
with col1:
st.dataframe(outlier_stats, use_container_width=True)
with col2:
fig_outlier_bar = px.bar(
outlier_stats.reset_index(),
x='Tipo',
y='Taxa Inadimplência',
title='Taxa de Inadimplência: Outliers vs Normais',
color='Tipo',
color_discrete_map={'Outlier': '#EF4444', 'Normal': '#3B82F6'}
)
fig_outlier_bar.update_yaxes(tickformat='.1%')
st.plotly_chart(fig_outlier_bar, use_container_width=True)
# Conclusões
outlier_bad_rate = outlier_stats.loc['Outlier', 'Taxa Inadimplência'] if 'Outlier' in outlier_stats.index else 0
normal_bad_rate = outlier_stats.loc['Normal', 'Taxa Inadimplência'] if 'Normal' in outlier_stats.index else 0
if outlier_bad_rate > normal_bad_rate:
st.markdown(f"""
<div class="warning-box">
<h4>⚠️ Outliers Apresentam Maior Risco</h4>
<p>Os clientes identificados como outliers apresentam taxa de inadimplência de
<strong>{outlier_bad_rate*100:.1f}%</strong>, versus <strong>{normal_bad_rate*100:.1f}%</strong>
dos clientes normais.</p>
<p><strong>Recomendações:</strong></p>
<ul>
<li>Implementar análise manual obrigatória para perfis atípicos</li>
<li>Criar flag automática no sistema para outliers detectados</li>
<li>Considerar limites de crédito reduzidos para estes perfis</li>
<li>Monitoramento mais frequente após aprovação</li>
</ul>
</div>
""", unsafe_allow_html=True)
else:
st.markdown("""
<div class="info-box">
<h4>ℹ️ Outliers não representam risco adicional significativo</h4>
<p>Nesta análise, os outliers não apresentaram taxa de inadimplência significativamente
maior que os clientes normais. No entanto, recomenda-se manter monitoramento especial
para perfis atípicos.</p>
</div>
""", unsafe_allow_html=True)
# ==========================================================================
# PÁGINA: CLASSIFICADOR INTERATIVO
# ==========================================================================
elif page == "⚡ VI. Classificador Interativo":
st.markdown('<h2 class="section-header">VI. Classificador Interativo de Risco</h2>',
unsafe_allow_html=True)
st.markdown("""
<div class="info-box">
<h4>⚡ Simulação de Análise de Crédito</h4>
<p>Utilize esta ferramenta para simular a análise de risco de um novo cliente.
Preencha os dados abaixo ou faça upload de um arquivo CSV.</p>
</div>
""", unsafe_allow_html=True)
if 'trained_models' not in st.session_state:
st.warning("⚠️ Por favor, treine os modelos primeiro na seção 'II. Modelagem Supervisionada'")
else:
trained_models = st.session_state['trained_models']
tab1, tab2 = st.tabs(["📝 Entrada Manual", "📁 Upload de Dados"])
with tab1:
st.markdown("### Dados do Solicitante")
col1, col2, col3 = st.columns(3)
with col1:
age = st.number_input("Idade", min_value=18, max_value=100, value=30)
income = st.number_input("Renda Anual (R$)", min_value=0, value=60000)
home = st.selectbox("Tipo de Residência",
['RENT', 'OWN', 'MORTGAGE', 'OTHER'])
emp_length = st.number_input("Tempo de Emprego (anos)", min_value=0, max_value=50, value=5)
with col2:
intent = st.selectbox("Finalidade do Empréstimo",
['PERSONAL', 'EDUCATION', 'MEDICAL', 'VENTURE',
'HOMEIMPROVEMENT', 'DEBTCONSOLIDATION'])
grade = st.selectbox("Grade de Crédito", ['A', 'B', 'C', 'D', 'E', 'F', 'G'])
loan_amount = st.number_input("Valor do Empréstimo (R$)", min_value=500, value=10000)
int_rate = st.number_input("Taxa de Juros (%)", min_value=5.0, max_value=25.0, value=12.0)
with col3:
percent_income = loan_amount / income if income > 0 else 0
st.metric("% Comprometimento Renda", f"{percent_income*100:.1f}%")
default_history = st.selectbox("Histórico de Inadimplência", ['N', 'Y'])
cred_hist_length = st.number_input("Histórico de Crédito (anos)", min_value=0, max_value=30, value=5)
if st.button("🔮 Analisar Risco", type="primary"):
# Preparar dados
new_data = pd.DataFrame({
'person_age': [age],
'person_income': [income],
'person_home_ownership': [home],
'person_emp_length': [emp_length],
'loan_intent': [intent],
'loan_grade': [grade],
'loan_amnt': [loan_amount],
'loan_int_rate': [int_rate],
'loan_percent_income': [percent_income],
'cb_person_default_on_file': [default_history],
'cb_person_cred_hist_length': [cred_hist_length]
})
# Codificar
for col, le in label_encoders.items():
if col in new_data.columns:
try:
new_data[col] = le.transform(new_data[col])
except:
# Se o valor não existe no encoder, usar o mais comum
new_data[col] = 0
# Escalar
new_data_scaled = scaler.transform(new_data)
# Predizer com múltiplos modelos
st.markdown("### 📊 Resultado da Análise")
results_pred = {}
for name, model in trained_models.items():
pred = model.predict(new_data_scaled)[0]
prob = model.predict_proba(new_data_scaled)[0] if hasattr(model, 'predict_proba') else [0.5, 0.5]
results_pred[name] = {'pred': pred, 'prob_bad': prob[1]}
avg_prob = np.mean([r['prob_bad'] for r in results_pred.values()])
col1, col2 = st.columns(2)
with col1:
# risco
fig_gauge = go.Figure(go.Indicator(
mode="gauge+number",
value=avg_prob * 100,
title={'text': "Probabilidade de Inadimplência"},
gauge={
'axis': {'range': [0, 100]},
'bar': {'color': "#3B82F6"},
'steps': [
{'range': [0, 30], 'color': "#D1FAE5"},
{'range': [30, 60], 'color': "#FEF3C7"},
{'range': [60, 100], 'color': "#FEE2E2"}
],
'threshold': {
'line': {'color': "red", 'width': 4},
'thickness': 0.75,
'value': 50
}
}
))
st.plotly_chart(fig_gauge, use_container_width=True)
with col2:
# Decisão
if avg_prob < 0.3:
st.success("✅ APROVADO - Baixo Risco")
st.markdown(f"""
**Recomendação:** Aprovar empréstimo
- Risco estimado: {avg_prob*100:.1f}%
- Categoria: Baixo Risco
- Ação: Aprovação automática
""")
elif avg_prob < 0.6:
st.warning("⚠️ ANÁLISE ADICIONAL - Risco Moderado")
st.markdown(f"""
**Recomendação:** Verificação adicional
- Risco estimado: {avg_prob*100:.1f}%
- Categoria: Risco Moderado
- Ação: Solicitar documentação complementar
""")
else:
st.error("❌ NEGADO - Alto Risco")
st.markdown(f"""
**Recomendação:** Não aprovar
- Risco estimado: {avg_prob*100:.1f}%
- Categoria: Alto Risco
- Ação: Encaminhar para análise especial ou recusar
""")
# Detalhes por modelo
st.markdown("### 📋 Detalhes por Modelo")
df_pred = pd.DataFrame({
'Modelo': list(results_pred.keys()),
'Predição': ['Bad (Inadimplente)' if r['pred'] == 1 else 'Good (Bom Pagador)'
for r in results_pred.values()],
'Prob. Inadimplência': [f"{r['prob_bad']*100:.1f}%" for r in results_pred.values()]
})
st.dataframe(df_pred, use_container_width=True)
with tab2:
st.markdown("### Upload de Arquivo CSV")
uploaded_file = st.file_uploader("Selecione um arquivo CSV", type=['csv'])
if uploaded_file is not None:
try:
df_upload = pd.read_csv(uploaded_file)
st.markdown("**Preview dos dados:**")
st.dataframe(df_upload.head(), use_container_width=True)
if st.button("🔮 Analisar Todos", type="primary"):
# Processamento similar ao anterior
st.info("Funcionalidade de processamento em lote disponível na versão completa.")
except Exception as e:
st.error(f"Erro ao carregar arquivo: {str(e)}")
# Footer
st.markdown("---")
st.markdown("""
<div style="text-align: center; color: #6B7280; font-size: 0.9rem;">
<p>Prova Final - SIEP</p>
<p>Daniel Coser Gonçalves de Araujo| Matrícula: 200033638</p>
<p>Prof: João Gabriel de Moraes Souza</p>
</div>
""", unsafe_allow_html=True)
if __name__ == "__main__":
main()