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"""

{icon} {status}


{proba:.1%}

Probabilidade de Calote

Note: {msg}

""", 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("""

🔴 Cluster 2: Alto Risco (Jovens Endividados)

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:

""", unsafe_allow_html=True) # CLUSTER 3 - EM ASCENSÃO st.markdown("""

🟡 Cluster 3: Potencial (Novos Entrantes)

Perfil: Jovens mais novos na fintech, com menor apetite ao risco que o Cluster 2.

Ação Recomendada:

""", unsafe_allow_html=True) with c_clus2: # CLUSTER 1 - PREMIUM st.markdown("""

🟢 Cluster 1: Clientes Premium (Alta Renda)

Perfil: Receita alta. Valores absolutos de empréstimo altos, mas percentualmente controlados.

Ação Recomendada:

""", unsafe_allow_html=True) # CLUSTER 0 - PADRÃO st.markdown("""

🔵 Cluster 0: Cliente Padrão

Perfil: Receita média, histórico de operações estável e dívidas controladas.

Ação Recomendada:

""", unsafe_allow_html=True) st.divider() st.caption("Relatório gerado pelo Sistema Integrado de Análise de Risco (CrediFast).")