HugoNeres's picture
Update app.py
44500e6 verified
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"""
<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).")