Spaces:
Sleeping
Sleeping
| """ | |
| =========================================================== | |
| 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 | |
| # ============================================================================= | |
| 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 | |
| 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 | |
| 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 | |
| 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() | |
| 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() |