Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| import pandas as pd | |
| import joblib | |
| import matplotlib.pyplot as plt | |
| import seaborn as sns | |
| import shap | |
| import numpy as np | |
| import plotly.express as px | |
| import io | |
| from sklearn.metrics import confusion_matrix, roc_curve, auc | |
| from sklearn.decomposition import PCA | |
| from sklearn.preprocessing import StandardScaler | |
| from sklearn.cluster import KMeans | |
| # --- CONFIGURAÇÃO --- | |
| st.set_page_config(page_title="CrediFast Risk System", layout="wide") | |
| st.title("🏦 CrediFast: Sistema Integrado de Análise de Risco") | |
| # --- CARREGAMENTO OTIMIZADO --- | |
| def load_system(): | |
| return joblib.load("sistema_risco_completo.pkl") | |
| def load_data(): | |
| return pd.read_csv("dados_credito_clean.csv") | |
| # --- CARREGAMENTO DO SISTEMA --- | |
| try: | |
| # 1. Carrega dados e sistema | |
| df = load_data() | |
| sistema = load_system() | |
| # 2. Recupera o scaler de dentro do sistema (conforme sua atualização) | |
| # Se der erro aqui, é porque você não gerou o novo .pkl no notebook ainda | |
| scaler = sistema["scaler"] | |
| # 3. Recupera os modelos | |
| dict_modelos = sistema["modelos"] | |
| # 4. Recupera dados de teste para validação | |
| y_test_real = sistema["y_test_real"] | |
| feature_names = sistema["X_test_sample"].columns.tolist() | |
| except FileNotFoundError: | |
| st.error("⚠️ Arquivo 'sistema_risco_completo.pkl' não encontrado. Faça o upload dele no Hugging Face.") | |
| st.stop() | |
| except KeyError: | |
| st.error("⚠️ O arquivo .pkl é antigo e não tem o 'scaler'. Gere o arquivo novamente no Notebook.") | |
| st.stop() | |
| except Exception as e: | |
| st.error(f"Erro crítico ao carregar o sistema: {e}") | |
| st.stop() | |
| # --- TABS --- | |
| tab1, tab2, tab3, tab4 = st.tabs(["I. Diagnóstico", "II & III. Construção e Avaliação dos Modelos & Explicabilidade", "IV. Clusterização", "V. Recomendações"]) | |
| # ========================================================= | |
| # ABA I: DIAGNÓSTICO E TRATAMENTO (O que você pediu) | |
| # ========================================================= | |
| with tab1: | |
| st.header("I. Diagnóstico Inicial e Tratamento de Dados") | |
| col_kpi1, col_kpi2, col_kpi3 = st.columns(3) | |
| col_kpi1.metric("Total de Registros", f"{df.shape[0]:,}") | |
| col_kpi2.metric("Variáveis (Colunas)", df.shape[1]) | |
| col_kpi3.metric("Taxa Global de Calote", f"{df['loan_status'].mean():.1%}") | |
| st.divider() | |
| # 1. Visualização dos Dados | |
| st.subheader("1. Visualização da Base de Dados (Processada)") | |
| st.dataframe(df.head(10), use_container_width=True) | |
| with st.expander("Ver Estatísticas Descritivas (Describe)"): | |
| st.dataframe(df.describe()) | |
| # 2. Relatório de Tratamento | |
| st.subheader("2. Relatório de Tratamento de Dados") | |
| st.info(""" | |
| **Processos de Limpeza e Imputação Realizados no Código:** | |
| 1. **Tratamento de Nulos (`Missing Values`):** | |
| * `person_emp_length`: Preenchido com a **Mediana** (devido à presença de outliers que distorciam a média). | |
| * `loan_int_rate`: Preenchido com a função KNN Imputer para minimizar impacto nos parâmetros da coluna. | |
| 2. **Remoção de Outliers e Inconsistências:** | |
| * Foram removidos registros com **Idade > 100 anos**. | |
| * Foram removidos casos biologicamente impossíveis onde **Tempo de Emprego > Idade**. | |
| 3. **Engenharia de Features:** | |
| * Criação de variáveis Dummy para colunas categóricas. | |
| * Aplicação de **SMOTE** (Synthetic Minority Over-sampling Technique) nos dados de treino para corrigir o desbalanceamento da classe 'Default'. | |
| """) | |
| st.divider() | |
| # 3. Gráfico Interativo de Distribuição | |
| st.subheader("3. Análise Exploratória Interativa") | |
| st.markdown("Selecione qualquer variável para visualizar sua distribuição em relação ao Risco de Crédito.") | |
| # Seletor de Variáveis | |
| # Filtramos para mostrar primeiro as colunas mais interessantes | |
| colunas_pri = ['person_age', 'person_income', 'loan_amnt', 'loan_int_rate', 'loan_percent_income', 'loan_grade', 'person_home_ownership'] | |
| # Adiciona o resto das colunas que não estão na lista prioritária | |
| outras_cols = [c for c in df.columns if c not in colunas_pri and c != 'loan_status'] | |
| opcoes = colunas_pri + outras_cols | |
| var_selected = st.selectbox("Escolha a Variável para Análise:", opcoes) | |
| # Lógica de Plotagem Inteligente (Plotly) | |
| try: | |
| # Se for numérica com muitos valores únicos -> Histograma | |
| if pd.api.types.is_numeric_dtype(df[var_selected]) and df[var_selected].nunique() > 10: | |
| fig = px.histogram( | |
| df, | |
| x=var_selected, | |
| color="loan_status", | |
| marginal="box", # Adiciona boxplot no topo | |
| nbins=50, | |
| title=f"Distribuição de '{var_selected}' por Status do Empréstimo", | |
| labels={"loan_status": "Calote (0=Não, 1=Sim)"}, | |
| color_discrete_sequence=["#1f77b4", "#d62728"], # Azul (Bom), Vermelho (Ruim) | |
| opacity=0.7, | |
| barmode="overlay" | |
| ) | |
| # Se for categórica ou numérica discreta (ex: Grade) -> Gráfico de Barras | |
| else: | |
| # Conta a frequência | |
| df_count = df.groupby([var_selected, 'loan_status']).size().reset_index(name='Contagem') | |
| fig = px.bar( | |
| df_count, | |
| x=var_selected, | |
| y='Contagem', | |
| color='loan_status', | |
| barmode='group', | |
| title=f"Frequência de '{var_selected}' por Status", | |
| color_discrete_sequence=["#1f77b4", "#d62728"] | |
| ) | |
| st.plotly_chart(fig, use_container_width=True) | |
| except Exception as e: | |
| st.warning(f"Não foi possível gerar o gráfico para esta variável. Erro: {e}") | |
| # ========================================================= | |
| # ABA II: AVALIAÇÃO + SIMULADOR (CORRIGIDA E UNIFICADA) | |
| # ========================================================= | |
| with tab2: | |
| st.header("II & III. Construção e Avaliação dos Modelos & Explicabilidade") | |
| # --- FUNÇÃO CALLBACK --- | |
| def reset_simulacao(): | |
| st.session_state['simulacao_resultado'] = None | |
| # --- 1. SELEÇÃO DO MODELO --- | |
| col_sel1, col_sel2 = st.columns([1, 3]) | |
| with col_sel1: | |
| st.markdown("##### Configuração") | |
| # Filtros de Categoria para facilitar a busca | |
| cats = ["Todos", "Boosting", "Árvores", "Outros"] | |
| cat_filter = st.radio( | |
| "Filtrar Família:", | |
| cats, | |
| horizontal=False, | |
| on_change=reset_simulacao # <--- Limpa ao mudar de categoria | |
| ) | |
| with col_sel2: | |
| model_names = list(sistema["modelos"].keys()) | |
| if cat_filter == "Boosting": | |
| model_names = [m for m in model_names if any(x in m for x in ['Light', 'XGB', 'Gradient', 'Ada'])] | |
| elif cat_filter == "Árvores": | |
| model_names = [m for m in model_names if 'Tree' in m or 'Forest' in m] | |
| elif cat_filter == "Outros": | |
| model_names = [m for m in model_names if any(x in m for x in ['KNN', 'SVM', 'MLP', 'Rede'])] | |
| selected_model_name = st.selectbox( | |
| "Selecione o Modelo para Análise:", | |
| model_names, | |
| on_change=reset_simulacao | |
| ) | |
| # Recuperação dos dados do modelo escolhido | |
| dados_modelo = sistema["modelos"][selected_model_name] | |
| metrics = dados_modelo["metrics"] | |
| y_pred_saved = dados_modelo["y_pred"] | |
| y_proba_saved = dados_modelo["y_proba"] | |
| current_model_obj = dados_modelo["modelo"] | |
| st.divider() | |
| # --- 2. DASHBOARD DE PERFORMANCE (Estático) --- | |
| c1, c2, c3 = st.columns([1, 1, 1.5]) | |
| with c1: | |
| st.subheader("Matriz de Confusão") | |
| cm = confusion_matrix(sistema["y_test_real"], y_pred_saved) | |
| fig_cm = plt.figure(figsize=(4, 3)) | |
| sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', cbar=False, | |
| xticklabels=['Bom', 'Ruim'], yticklabels=['Bom', 'Ruim']) | |
| plt.xlabel('Predito') | |
| plt.ylabel('Real') | |
| st.pyplot(fig_cm) | |
| with c2: | |
| st.subheader("Curva ROC") | |
| if len(np.unique(sistema["y_test_real"])) > 1: | |
| fpr, tpr, _ = roc_curve(sistema["y_test_real"], y_proba_saved) | |
| fig_roc = plt.figure(figsize=(4, 3)) | |
| plt.plot(fpr, tpr, color='#ff7f0e', lw=2, label=f'AUC = {metrics["AUC"]:.3f}') | |
| plt.plot([0, 1], [0, 1], color='navy', linestyle='--') | |
| plt.legend(loc="lower right") | |
| plt.grid(alpha=0.3) | |
| st.pyplot(fig_roc) | |
| else: | |
| st.info("ROC indisponível.") | |
| with c3: | |
| st.subheader("Métricas Gerais") | |
| k1, k2 = st.columns(2) | |
| k1.metric("Recall (Sensibilidade)", f"{metrics['Recall']:.1%}", help="Capacidade de detectar maus pagadores.") | |
| k2.metric("AUC Global", f"{metrics['AUC']:.3f}") | |
| k3, k4 = st.columns(2) | |
| k3.metric("Acurácia", f"{metrics['Acurácia']:.1%}") | |
| k4.metric("F1-Score", f"{metrics['F1-Score']:.1%}") | |
| st.divider() | |
| # --- BLOCO COLAPSÁVEL: INTERPRETABILIDADE GLOBAL --- | |
| # O 'expanded=False' faz ele começar fechado. Mude para True se quiser aberto. | |
| with st.expander(f"🧩 Ver Impacto das Variáveis (Global) - {selected_model_name}", expanded=False): | |
| # 1. Recupera o dicionário de Imagens | |
| shap_imgs = sistema.get("shap_images_dict", {}) | |
| # 2. Verifica se existe imagem para o modelo selecionado | |
| if selected_model_name in shap_imgs: | |
| st.markdown(f"**Visão Macro:** O que o modelo **{selected_model_name}** considera mais arriscado?") | |
| # Exibe a imagem estática (Instantâneo) | |
| st.image(shap_imgs[selected_model_name], use_container_width=True) | |
| # Legenda compacta dentro de uma caixinha informativa | |
| st.info(""" | |
| **Como ler este gráfico:** | |
| * ⬆️ **Topo:** Variáveis mais importantes. | |
| * 🔴 **Vermelho:** Valor Alto (Ex: Renda Alta) | 🔵 **Azul:** Valor Baixo. | |
| * ➡️ **Eixo X (Direita):** Empurra o risco para cima (Calote). | |
| * ⬅️ **Eixo X (Esquerda):** Empurra o risco para baixo (Pagamento). | |
| """) | |
| else: | |
| st.warning(f"⚠️ Gráfico de impacto não disponível para '{selected_model_name}' (Disponível apenas para modelos baseados em árvore).") | |
| st.divider() | |
| # --- 3. SIMULADOR DE RISCO (COM ST.FORM E SESSION STATE) --- | |
| st.subheader(f"🔮 Simulador de Crédito ({selected_model_name})") | |
| col_input, col_res = st.columns([1, 1.2]) | |
| # Inicializa estado se não existir | |
| if 'simulacao_resultado' not in st.session_state: | |
| st.session_state['simulacao_resultado'] = None | |
| # --- LADO ESQUERDO: FORMULÁRIO --- | |
| with col_input: | |
| with st.form("form_simulador"): | |
| st.markdown("**Perfil do Cliente**") | |
| c_in1, c_in2 = st.columns(2) | |
| with c_in1: | |
| income = st.number_input("Renda Anual (R$)", 4000, 5000000, 65000, step=1000) | |
| loan_amnt = st.number_input("Valor Solicitado (R$)", 1000, 100000, 15000, step=500) | |
| age = st.number_input("Idade", 18, 100, 25) | |
| emp_length = st.number_input("Anos de Emprego", 0.0, 70.0, 2.0) | |
| with c_in2: | |
| int_rate = st.slider("Taxa de Juros (%)", 4.0, 25.0, 10.0, step=0.1) | |
| grade = st.selectbox("Classificação (Grade)", ["A", "B", "C", "D", "E", "F", "G"]) | |
| home = st.selectbox("Tipo de Moradia", ["ALUGUEL", "PRÓPRIA", "FINANCIADA", "OUTROS"]) | |
| intent = st.selectbox("Motivo", ["PESSOAL", "EDUCAÇÃO", "MÉDICO", "VENTURE", "REFORMA", "DÍVIDA"]) | |
| default_hist = st.selectbox("Já teve Calote (Histórico)?", ["Não", "Sim"]) | |
| # Botão de Envio (DENTRO do form) | |
| submit_button = st.form_submit_button("Calcular Risco 🚀", type="primary") | |
| # --- LÓGICA DE CÁLCULO (Executa apenas ao clicar) --- | |
| if submit_button: | |
| try: | |
| with st.spinner("Calculando risco e gerando explicação..."): # Feedback visual para o usuário | |
| # 1. Feature Engineering | |
| percent_income = loan_amnt / income if income > 0 else 0 | |
| cred_hist_len = max(2, int(age - 20)) | |
| # 2. Cria DataFrame base | |
| df_input = pd.DataFrame(0, index=[0], columns=feature_names) | |
| # 3. Preenchimento Numérico | |
| cols_numericas = ['person_age', 'person_income', 'person_emp_length', | |
| 'loan_amnt', 'loan_int_rate', 'loan_percent_income', | |
| 'cb_person_cred_hist_length'] | |
| df_input['person_age'] = age | |
| df_input['person_income'] = income | |
| df_input['person_emp_length'] = emp_length | |
| df_input['loan_amnt'] = loan_amnt | |
| df_input['loan_int_rate'] = int_rate | |
| df_input['loan_percent_income'] = percent_income | |
| df_input['cb_person_cred_hist_length'] = cred_hist_len | |
| # 4. Preenchimento Categórico | |
| map_home = {'ALUGUEL': 'RENT', 'PRÓPRIA': 'OWN', 'FINANCIADA': 'MORTGAGE', 'OUTROS': 'OTHER'} | |
| col_home = f"person_home_ownership_{map_home.get(home.split()[0], 'OTHER')}" | |
| if col_home in df_input.columns: df_input[col_home] = 1 | |
| map_intent = {"PESSOAL": "PERSONAL", "EDUCAÇÃO": "EDUCATION", "MÉDICO": "MEDICAL", | |
| "VENTURE": "VENTURE", "REFORMA": "HOMEIMPROVEMENT", "DÍVIDA": "DEBTCONSOLIDATION"} | |
| col_intent = f"loan_intent_{map_intent.get(intent, 'PERSONAL')}" | |
| if col_intent in df_input.columns: df_input[col_intent] = 1 | |
| col_grade = f"loan_grade_{grade}" | |
| if col_grade in df_input.columns: df_input[col_grade] = 1 | |
| if default_hist == 'Sim' and 'cb_person_default_on_file_Y' in df_input.columns: | |
| df_input['cb_person_default_on_file_Y'] = 1 | |
| # 5. Scaling | |
| df_input = df_input.reindex(columns=feature_names, fill_value=0) | |
| # Guarda input bruto para o SHAP (antes do scaler) | |
| df_input_shap = df_input.copy() | |
| try: | |
| df_input[cols_numericas] = scaler.transform(df_input[cols_numericas]) | |
| except ValueError: | |
| df_input = scaler.transform(df_input) | |
| # 6. Predição | |
| try: | |
| proba = current_model_obj.predict_proba(df_input)[0][1] | |
| except: | |
| pred = current_model_obj.predict(df_input)[0] | |
| proba = 1.0 if pred == 1 else 0.0 | |
| # 7. GERA O GRÁFICO SHAP E CONVERTE PARA IMAGEM ESTÁTICA | |
| shap_image_buffer = None # Variável para guardar a imagem | |
| modelos_arvore = ['LightGBM', 'XGBoost', 'Random Forest', 'Decision Tree', 'Gradient'] | |
| if any(m in selected_model_name for m in modelos_arvore): | |
| try: | |
| explainer = shap.TreeExplainer(current_model_obj) | |
| shap_values = explainer(df_input_shap) | |
| if len(shap_values.shape) == 3: | |
| shap_val_plot = shap_values[:, :, 1] | |
| else: | |
| shap_val_plot = shap_values | |
| # Cria a figura | |
| fig = plt.figure(figsize=(8, 4)) | |
| shap.plots.waterfall(shap_val_plot[0], show=False, max_display=7) | |
| # --- O TRUQUE ANTI-VIBRAÇÃO --- | |
| # Salva o plot em um buffer de memória (como se fosse um arquivo PNG invisível) | |
| buf = io.BytesIO() | |
| fig.savefig(buf, format="png", bbox_inches='tight', dpi=150) | |
| buf.seek(0) # Volta para o início do arquivo | |
| shap_image_buffer = buf # Guarda os dados da imagem | |
| plt.close(fig) # Limpa a memória do Matplotlib imediatamente | |
| except Exception as e: | |
| print(f"Erro ao gerar SHAP: {e}") | |
| # 8. Salva TUDO no Session State | |
| st.session_state['simulacao_resultado'] = { | |
| 'proba': proba, | |
| 'model_name': selected_model_name, | |
| 'shap_image': shap_image_buffer # <--- Agora salvamos a IMAGEM, não a figura | |
| } | |
| except Exception as e: | |
| st.error(f"Erro no cálculo: {e}") | |
| st.error(f"Erro no cálculo: {e}") | |
| # --- LADO DIREITO: EXIBIÇÃO DO RESULTADO --- | |
| with col_res: | |
| if st.session_state['simulacao_resultado']: | |
| res = st.session_state['simulacao_resultado'] | |
| proba = res['proba'] | |
| # Definição de Cores e Textos | |
| if proba < 0.20: | |
| status, icon, bg_color, text_color, border_color = "APROVADO AUTOMATICAMENTE", "✅", "#d4edda", "#155724", "#c3e6cb" | |
| msg = "Cliente apresenta baixíssimo risco." | |
| elif proba < 0.60: | |
| status, icon, bg_color, text_color, border_color = "ANÁLISE MANUAL RECOMENDADA", "⚠️", "#fff3cd", "#856404", "#ffeeba" | |
| msg = "Cliente na zona cinzenta. Verificar documentos." | |
| else: | |
| status, icon, bg_color, text_color, border_color = "REPROVADO / ALTO RISCO", "❌", "#f8d7da", "#721c24", "#f5c6cb" | |
| msg = "Alta probabilidade de inadimplência detectada." | |
| # Card HTML | |
| st.markdown(f""" | |
| <div style="background-color: {bg_color}; color: {text_color}; padding: 20px; border-radius: 10px; border: 1px solid {border_color}; box-shadow: 0 4px 6px rgba(0,0,0,0.1); margin-bottom: 20px;"> | |
| <h3 style="color: {text_color}; margin:0; font-size: 20px;">{icon} {status}</h3> | |
| <hr style="border-top: 1px solid {border_color}; margin: 10px 0;"> | |
| <h1 style="margin:0; font-size: 50px; font-weight: bold;">{proba:.1%}</h1> | |
| <p style="margin-top: 5px; opacity: 0.8;">Probabilidade de Calote</p> | |
| <p style="font-style: italic; margin-top: 10px;">Note: {msg}</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # EXIBIÇÃO DO SHAP (Instantânea com st.image) | |
| # Verifica se existe uma imagem salva no dicionário | |
| if res.get('shap_image'): | |
| with st.expander("🔎 Entender Motivos (SHAP)"): | |
| st.caption("Fatores que mais impactaram esta decisão:") | |
| # st.image não processa nada, só exibe. Zero vibração. | |
| st.image(res['shap_image'], use_container_width=True) | |
| else: | |
| # Estado inicial (sem simulação feita) | |
| st.info("👈 Preencha os dados ao lado e clique em 'Calcular Risco' para ver o resultado.") | |
| # ========================================================= | |
| # ABA III: CLUSTERIZAÇÃO COM PCA (Visualização Avançada) | |
| # ========================================================= | |
| with tab3: | |
| st.header("IV. Segmentação de Clientes (Clusterização)") | |
| st.markdown(""" | |
| Abaixo, utilizamos **K-Means** para agrupar clientes semelhantes e **PCA (Análise de Componentes Principais)** para reduzir todas as dimensões (Renda, Idade, Juros, etc.) em um mapa 2D. | |
| """) | |
| # 1. Definição das Colunas Numéricas para Clusterização | |
| # (Removendo colunas categóricas e alvo) | |
| cols_cluster = ['person_age', 'person_income', 'person_emp_length', | |
| 'loan_amnt', 'loan_int_rate', 'loan_percent_income', | |
| 'cb_person_cred_hist_length'] | |
| # 2. Verifica/Gera Clusters (Caso o CSV não tenha a coluna 'Cluster') | |
| if 'Cluster' not in df.columns: | |
| with st.spinner("Identificando grupos de clientes (Clusterização)..."): | |
| # Prepara dados (Inputa médidas se houver nulos para não quebrar) | |
| X_clus = df[cols_cluster].fillna(df[cols_cluster].mean()) | |
| # Escala específica para o Cluster (importante ser fresco) | |
| scaler_clus = StandardScaler() | |
| X_clus_scaled = scaler_clus.fit_transform(X_clus) | |
| # Aplica K-Means (Ex: 4 grupos) | |
| kmeans = KMeans(n_clusters=4, random_state=42, n_init=10) | |
| df['Cluster'] = kmeans.fit_predict(X_clus_scaled) | |
| # Garante que Cluster seja tratado como texto (Categoria) para cores discretas | |
| df['Cluster'] = df['Cluster'].astype(str) | |
| # 3. Aplicação do PCA para Visualização | |
| try: | |
| # Prepara dados para PCA | |
| X_pca_input = df[cols_cluster].fillna(df[cols_cluster].mean()) | |
| scaler_pca = StandardScaler() | |
| X_scaled = scaler_pca.fit_transform(X_pca_input) | |
| # Calcula PCA (Reduz para 2 componentes) | |
| pca = PCA(n_components=2) | |
| components = pca.fit_transform(X_scaled) | |
| # Cria DataFrame temporário para o gráfico | |
| df_pca = pd.DataFrame(data=components, columns=['PC1', 'PC2']) | |
| df_pca['Cluster'] = df['Cluster'].values | |
| # Adiciona dados originais para o Tooltip (Hover) | |
| df_pca['Renda'] = df['person_income'].values | |
| df_pca['Empréstimo'] = df['loan_amnt'].values | |
| df_pca['Risco'] = df['loan_status'].apply(lambda x: 'Calote' if x==1 else 'Bom Pagador').values | |
| # 4. Gráfico Interativo | |
| col_graph, col_stats = st.columns([2, 1]) | |
| with col_graph: | |
| var_explicada = pca.explained_variance_ratio_.sum() | |
| st.caption(f"Visualização PCA (Explica {var_explicada:.1%} da variação dos dados)") | |
| fig_pca = px.scatter( | |
| df_pca, | |
| x='PC1', | |
| y='PC2', | |
| color='Cluster', | |
| symbol='Risco', # Diferencia caloteiros por formato (opcional) | |
| hover_data=['Renda', 'Empréstimo', 'Risco'], | |
| title="Mapa de Clusters (PCA)", | |
| color_discrete_sequence=px.colors.qualitative.Bold, | |
| height=500 | |
| ) | |
| fig_pca.update_traces(marker=dict(size=8, opacity=0.7), selector=dict(mode='markers')) | |
| st.plotly_chart(fig_pca, use_container_width=True) | |
| # 5. Estatísticas dos Perfis | |
| with col_stats: | |
| st.subheader("Perfil dos Grupos") | |
| # Agrupa e calcula médias | |
| resumo = df.groupby('Cluster')[['person_income', 'loan_amnt', 'person_age', 'loan_status']].mean() | |
| # Formatação bonita | |
| st.dataframe( | |
| resumo.style.format({ | |
| 'person_income': 'R$ {:,.0f}', | |
| 'loan_amnt': 'R$ {:,.0f}', | |
| 'person_age': '{:.0f} anos', | |
| 'loan_status': '{:.1%}' | |
| }).background_gradient(cmap='Blues', subset=['loan_status']), | |
| use_container_width=True | |
| ) | |
| except Exception as e: | |
| st.error(f"Erro ao gerar PCA: {e}") | |
| # ========================================================= | |
| # ABA V: RECOMENDAÇÕES E CONCLUSÃO | |
| # ========================================================= | |
| with tab4: | |
| st.header("V. Recomendações Estratégicas e Conclusões") | |
| # --- 1. INSIGHTS DE MODELAGEM --- | |
| with st.container(): | |
| st.subheader("🔍 1. Insights de Explicabilidade do Modelo") | |
| # Cria duas colunas para comparar Baixo vs Alto risco lado a lado | |
| col_insight1, col_insight2 = st.columns(2) | |
| with col_insight1: | |
| st.info("**Caso de Baixo Risco (Aprovado)**") | |
| st.markdown(""" | |
| Ao analisarmos clientes seguros, identificamos padrões além do óbvio: | |
| * **Fatores Ocultos:** A posse de imóvel (*Home Ownership*) atua como um forte redutor de risco ("colchão de segurança"). | |
| * **Variáveis Dummy:** O modelo identificou implicitamente que os melhores pagadores pertencem à **Classe A** (Loan Grade A), mesmo com essa variável omitida no treino para evitar colinearidade. | |
| """) | |
| with col_insight2: | |
| st.warning("**Caso de Alto Risco (Reprovado)**") | |
| st.markdown(""" | |
| Nas observações de risco de inadimplência, o modelo valida a lógica financeira tradicional: | |
| * **Tríade do Risco:** A decisão foi severamente impactada pela combinação de **Baixa Receita**, alta **Relação Empréstimo/Receita** (alavancagem excessiva) e a **Nota (Grade)** atribuída pelo sistema. | |
| * **Conclusão:** O modelo penaliza agressivamente o comprometimento de renda. Quando a nota do sistema é baixa e a dívida é alta proporcionalmente à renda, a reprovação é quase imediata. | |
| """) | |
| # --- 2. POLÍTICAS DE CRÉDITO SUGERIDAS --- | |
| st.subheader("🛡️ 2. Políticas de Mitigação de Risco") | |
| col_pol1, col_pol2, col_pol3 = st.columns(3) | |
| with col_pol1: | |
| st.info("**Evolução de Crédito (Ramp-up)**") | |
| st.markdown(""" | |
| Criar uma trava de comprometimento de renda progressiva: | |
| * **Novos Clientes:** Máximo de **20%** da renda comprometida. | |
| * **Clientes Recorrentes:** Aumento gradual conforme histórico de pagamentos e uso de outros serviços da Fintech. | |
| * *Objetivo:* Evitar inadimplência por superendividamento inicial. | |
| """) | |
| with col_pol2: | |
| st.warning("**Retenção de Perfil Intermediário**") | |
| st.markdown(""" | |
| Para clientes de risco médio (zona cinzenta): | |
| * **Ação:** Ofertar redução de taxas ou prazos mais longos. | |
| * **Objetivo:** Diluir o valor da parcela no tempo, facilitando o pagamento e mantendo o cliente no ecossistema, evitando que ele busque crédito predatório fora. | |
| """) | |
| with col_pol3: | |
| st.success("**Esteira de Aprovação Inteligente**") | |
| st.markdown(""" | |
| Implementar triagem automática baseada na probabilidade do modelo: | |
| * **Baixo Risco (Prob < 20%):** Fast-track (aprovação automática) e ofertas agressivas de aumento de limite. | |
| * **Objetivo:** Reduzir CAC (Custo de Aquisição) e melhorar a experiência do usuário (UX) para os melhores clientes. | |
| """) | |
| st.divider() | |
| # --- 3. ESTRATÉGIA POR CLUSTER (Grupos Focais) --- | |
| st.subheader("🎯 3. Ações Táticas por Segmento (Clusters)") | |
| # Organizando os clusters em 2 colunas para melhor leitura | |
| c_clus1, c_clus2 = st.columns(2) | |
| with c_clus1: | |
| # CLUSTER 2 - ALTO RISCO | |
| st.markdown(""" | |
| <div style="border: 1px solid #ffcccc; padding: 15px; border-radius: 10px; margin-bottom: 20px;"> | |
| <h4 style="color: #cc0000;">🔴 Cluster 2: Alto Risco (Jovens Endividados)</h4> | |
| <p><strong>Perfil:</strong> Jovens com altos níveis de dívida (média de 30% da renda), renda média, mas pagando juros altos.</p> | |
| <p><strong>Ação Recomendada:</strong></p> | |
| <ul> | |
| <li>Análise manual rigorosa obrigatória.</li> | |
| <li>Exigência de garantias reais.</li> | |
| <li>Restrição de novos créditos até a regularização do <i>loan_percent_income</i>.</li> | |
| </ul> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # CLUSTER 3 - EM ASCENSÃO | |
| st.markdown(""" | |
| <div style="border: 1px solid #fff3cd; padding: 15px; border-radius: 10px;"> | |
| <h4 style="color: #856404;">🟡 Cluster 3: Potencial (Novos Entrantes)</h4> | |
| <p><strong>Perfil:</strong> Jovens mais novos na fintech, com menor apetite ao risco que o Cluster 2.</p> | |
| <p><strong>Ação Recomendada:</strong></p> | |
| <ul> | |
| <li>Foco em fidelização e educação financeira.</li> | |
| <li>Aumento gradual de limites (Política de Ramp-up).</li> | |
| <li>Candidatos a se tornarem "Premium" no futuro.</li> | |
| </ul> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| with c_clus2: | |
| # CLUSTER 1 - PREMIUM | |
| st.markdown(""" | |
| <div style="border: 1px solid #d4edda; padding: 15px; border-radius: 10px; margin-bottom: 20px;"> | |
| <h4 style="color: #155724;">🟢 Cluster 1: Clientes Premium (Alta Renda)</h4> | |
| <p><strong>Perfil:</strong> Receita alta. Valores absolutos de empréstimo altos, mas percentualmente controlados.</p> | |
| <p><strong>Ação Recomendada:</strong></p> | |
| <ul> | |
| <li>Oferta de taxas de juros preferenciais (menores).</li> | |
| <li>Aumento proativo de limite.</li> | |
| <li>Atendimento prioritário para aumentar o <i>Share of Wallet</i>.</li> | |
| </ul> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # CLUSTER 0 - PADRÃO | |
| st.markdown(""" | |
| <div style="border: 1px solid #e2e3e5; padding: 15px; border-radius: 10px;"> | |
| <h4 style="color: #383d41;">🔵 Cluster 0: Cliente Padrão</h4> | |
| <p><strong>Perfil:</strong> Receita média, histórico de operações estável e dívidas controladas.</p> | |
| <p><strong>Ação Recomendada:</strong></p> | |
| <ul> | |
| <li>Manutenção das políticas padrão de crédito.</li> | |
| <li>Monitoramento automatizado de rotina.</li> | |
| <li>Ofertas padrão de mercado.</li> | |
| </ul> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.divider() | |
| st.caption("Relatório gerado pelo Sistema Integrado de Análise de Risco (CrediFast).") |