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 --- @st.cache_resource def load_system(): return joblib.load("sistema_risco_completo.pkl") @st.cache_data 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"""
Probabilidade de Calote
Note: {msg}
Perfil: Jovens com altos níveis de dívida (média de 30% da renda), renda média, mas pagando juros altos.
Ação Recomendada:
Perfil: Jovens mais novos na fintech, com menor apetite ao risco que o Cluster 2.
Ação Recomendada:
Perfil: Receita alta. Valores absolutos de empréstimo altos, mas percentualmente controlados.
Ação Recomendada:
Perfil: Receita média, histórico de operações estável e dívidas controladas.
Ação Recomendada: