SIEP4.2 / src /streamlit_app.py
Pegumenezes's picture
Update src/streamlit_app.py
369a9e8 verified
import streamlit as st
import pandas as pd
from collections import Counter
import numpy as np
import io
# Modelagem e Métricas
from imblearn.over_sampling import SMOTE
from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.preprocessing import StandardScaler
from sklearn.neighbors import KNeighborsClassifier
from sklearn.svm import SVC
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier, AdaBoostClassifier, GradientBoostingClassifier
from xgboost import XGBClassifier
from lightgbm import LGBMClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.feature_selection import RFE
from sklearn.metrics import roc_auc_score, roc_curve, accuracy_score, precision_score, recall_score, f1_score, \
confusion_matrix, ConfusionMatrixDisplay
import statsmodels.api as sm
# Visualização
import matplotlib.pyplot as plt
import seaborn as sns
# --- CONFIGURAÇÃO DA PÁGINA ---
st.set_page_config(
layout="wide",
page_title="Dashboard de Previsão de Reclamações",
page_icon="📊"
)
# --- FUNÇÕES DE ESTILO E CONFIGURAÇÃO ---
def style_p_value(p_val):
"""Aplica cor ao p-valor: verde se significante, vermelho caso contrário."""
is_significant = p_val < 0.05
color = 'green' if is_significant else 'red'
return f'color: {color}'
# Dicionários de modelos e seus grids de parâmetros para otimização
MODELS = {
"KNN": KNeighborsClassifier(),
"SVM": SVC(probability=True, random_state=42),
"Decision Tree": DecisionTreeClassifier(random_state=42),
"Random Forest": RandomForestClassifier(random_state=42),
"AdaBoost": AdaBoostClassifier(random_state=42),
"Gradient Boosting": GradientBoostingClassifier(random_state=42),
"XGBoost": XGBClassifier(use_label_encoder=False, eval_metric='logloss', random_state=42),
"LightGBM": LGBMClassifier(random_state=42)
}
PARAM_GRIDS = {
"KNN": {'n_neighbors': [3, 5, 7]},
"SVM": {'C': [0.1, 1], 'gamma': ['scale']},
"Decision Tree": {'max_depth': [5, 10, None], 'min_samples_leaf': [1, 2, 4]},
"Random Forest": {'n_estimators': [100, 200], 'max_depth': [10, 20]},
"AdaBoost": {'n_estimators': [50, 100], 'learning_rate': [0.1, 1.0]},
"Gradient Boosting": {'n_estimators': [100, 200], 'learning_rate': [0.05, 0.1], 'max_depth': [3, 5]},
"XGBoost": {'n_estimators': [100, 200], 'learning_rate': [0.05, 0.1], 'max_depth': [3, 5]},
"LightGBM": {'n_estimators': [100, 200], 'learning_rate': [0.05, 0.1], 'num_leaves': [31, 40]}
}
# --- FUNÇÕES CACHEADAS ---
@st.cache_data
def load_data():
"""Carrega os dados do GitHub."""
url = "https://raw.githubusercontent.com/Abdulraqib20/Customer-Personality-Analysis/refs/heads/main/marketing_campaign.csv"
return pd.read_csv(url, sep='\t')
@st.cache_data
def preprocess_data(df):
"""Realiza o pré-processamento completo dos dados."""
df_processed = df.copy()
df_processed['Dt_Customer'] = pd.to_datetime(df_processed['Dt_Customer'], dayfirst=True)
df_processed['Days_Since_Customer'] = (pd.to_datetime('today') - df_processed['Dt_Customer']).dt.days
df_processed = df_processed.drop('Dt_Customer', axis=1)
df_processed['Income'] = df_processed['Income'].fillna(df_processed['Income'].median())
df_processed = pd.get_dummies(df_processed, columns=['Education', 'Marital_Status'], drop_first=True, dtype=float)
cols_to_drop = ['ID', 'Z_CostContact', 'Z_Revenue']
df_processed = df_processed.drop(columns=[col for col in cols_to_drop if col in df_processed.columns], axis=1)
return df_processed
@st.cache_resource
def train_initial_models(X_train, y_train):
"""Treina os modelos com parâmetros padrão e retorna os modelos treinados e o scaler."""
trained_models = {}
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
for name, model in MODELS.items():
if name in ["KNN", "SVM"]:
model.fit(X_train_scaled, y_train)
else:
model.fit(X_train, y_train)
trained_models[name] = model
return trained_models, scaler
@st.cache_resource
def run_grid_search(_model, param_grid, X_train, y_train):
"""Executa o GridSearchCV para um modelo específico."""
grid = GridSearchCV(_model, param_grid, refit=True, verbose=0, cv=3, scoring='roc_auc', n_jobs=-1)
grid.fit(X_train, y_train)
return grid
@st.cache_data
def get_statsmodels_summary(X_train, y_train):
"""Gera o sumário estatístico do modelo de regressão logística."""
X_train_numeric = X_train.astype(float)
y_train_numeric = y_train.astype(float)
X_train_sm = sm.add_constant(X_train_numeric)
logit_model = sm.Logit(y_train_numeric, X_train_sm).fit(disp=0)
params = logit_model.params
p_values = logit_model.pvalues
conf_int = logit_model.conf_int()
odds_ratios = np.exp(params)
results_df = pd.DataFrame({
'Coeficiente': params,
'p-valor': p_values,
'Odds Ratio': odds_ratios,
'IC 95% Inferior': np.exp(conf_int[0]),
'IC 95% Superior': np.exp(conf_int[1])
})
return results_df
# --- TÍTULO DO DASHBOARD ---
st.title("📊 Dashboard de Previsão de Reclamações de Clientes")
st.markdown("Desenvolvido para a Tarefa 4 da disciplina de Engenharia de Produção da UnB.")
st.markdown("Este dashboard interativo permite analisar, treinar e avaliar modelos de Machine Learning para prever a probabilidade de um cliente fazer uma reclamação, com base em seus dados demográficos e de compra.")
# --- SIDEBAR DE CONTROLES ---
with st.sidebar:
st.header("⚙️ Configurações da Análise")
st.markdown("Ajuste os parâmetros e clique em 'Iniciar Análise' para processar os dados e treinar os modelos.")
use_smote = st.checkbox("Balancear dados com SMOTE", value=True, help="Aplica a técnica SMOTE para corrigir o desbalanceamento da variável 'Complain'.")
use_rfe = st.checkbox("Usar Seleção de Variáveis (RFE)", value=False, help="Aplica a Eliminação Recursiva de Features para selecionar as variáveis mais importantes.")
n_features_rfe = st.slider("Número de Variáveis (RFE)", min_value=5, max_value=30, value=15, disabled=not use_rfe)
test_size = st.slider("Tamanho do Conjunto de Teste", min_value=0.1, max_value=0.5, value=0.3, step=0.05)
st.divider()
st.header("🚀 Otimização de Modelos")
models_to_optimize = st.multiselect(
"Escolha os Modelos para Otimizar",
options=list(MODELS.keys()),
default=["Random Forest", "XGBoost"],
help="Selecione um ou mais modelos para realizar a busca por melhores hiperparâmetros (GridSearchCV)."
)
start_analysis = st.button("▶️ Iniciar Análise Preditiva", type="primary", use_container_width=True)
# --- LÓGICA PRINCIPAL ---
if 'analysis_done' not in st.session_state:
st.session_state.analysis_done = False
if start_analysis:
st.session_state.analysis_done = True
with st.spinner('Etapa 1/6: Carregando e pré-processando os dados...'):
df_original = load_data()
df_processed = preprocess_data(df_original)
y = df_processed['Complain']
X = df_processed.drop('Complain', axis=1)
if use_rfe:
with st.spinner('Etapa 2/6: Aplicando Seleção de Variáveis (RFE)...'):
estimator = LogisticRegression(max_iter=1000)
selector = RFE(estimator, n_features_to_select=n_features_rfe, step=1)
selector = selector.fit(X, y)
X = X.loc[:, selector.support_]
st.session_state.X_final_cols = X.columns
with st.spinner('Etapa 3/6: Balanceando e dividindo os dados...'):
st.session_state.y_original_dist = Counter(y)
if use_smote:
smote = SMOTE(random_state=42)
X_res, y_res = smote.fit_resample(X, y)
st.session_state.y_resampled_dist = Counter(y_res)
else:
X_res, y_res = X, y
X_train, X_test, y_train, y_test = train_test_split(X_res, y_res, test_size=test_size, random_state=42, stratify=y_res)
st.session_state.data = {'X_train': X_train, 'X_test': X_test, 'y_train': y_train, 'y_test': y_test}
with st.spinner('Etapa 4/6: Treinando modelos com parâmetros padrão...'):
initial_models, scaler = train_initial_models(X_train, y_train)
st.session_state.initial_models = initial_models
st.session_state.scaler = scaler
optimization_results = {}
if models_to_optimize:
for i, model_name in enumerate(models_to_optimize):
with st.spinner(f'Etapa 5/6: Otimizando modelo {i+1}/{len(models_to_optimize)} ({model_name})...'):
model_instance = MODELS[model_name]
param_grid = PARAM_GRIDS[model_name]
grid_result = run_grid_search(model_instance, param_grid, X_train, y_train)
optimization_results[model_name] = grid_result
st.session_state.optimization_results = optimization_results
with st.spinner('Etapa 6/6: Gerando análise estatística final...'):
stats_summary = get_statsmodels_summary(X_train, y_train)
st.session_state.stats_summary = stats_summary
# --- ABAS DE RESULTADOS ---
if st.session_state.analysis_done:
tab1, tab2, tab3, tab4, tab5 = st.tabs([
"📊 Visão Geral",
"🔍 Comparativo Inicial",
"🚀 Otimização e Estatísticas",
"📈 Desempenho Final",
"👔 Análise Gerencial"
])
with tab1:
st.header("Visão Geral e Preparação dos Dados")
st.markdown("Nesta seção, exploramos o conjunto de dados original e visualizamos o impacto das etapas de preparação, como o balanceamento com SMOTE e a seleção de variáveis com RFE.")
col1, col2, col3 = st.columns(3)
col1.metric("Total de Clientes", f"{len(load_data())}")
col2.metric("Variáveis Iniciais", f"{load_data().shape[1]}")
col3.metric("Variáveis Após Preparação", f"{len(st.session_state.X_final_cols)}")
st.subheader("Distribuição da Variável-Alvo: 'Complain'")
col1, col2 = st.columns(2)
with col1:
st.write("**Distribuição Original:**")
fig, ax = plt.subplots()
sns.countplot(x=load_data()['Complain'], ax=ax, palette="viridis")
ax.set_title("Antes do Balanceamento")
st.pyplot(fig)
if use_smote:
with col2:
st.write("**Distribuição Após SMOTE:**")
fig, ax = plt.subplots()
sns.countplot(x=st.session_state.data['y_train'], ax=ax, palette="rocket")
ax.set_title("Depois do Balanceamento")
st.pyplot(fig)
with st.expander("Ver amostra e detalhes dos dados"):
st.dataframe(load_data().head())
st.subheader("Estatísticas Descritivas")
st.dataframe(load_data().describe())
with tab2:
st.header("Comparativo de Desempenho dos Modelos (Padrão)")
st.markdown("Aqui avaliamos a performance inicial de todos os modelos treinados com seus hiperparâmetros padrão. Isso nos dá uma linha de base para entender quais arquiteturas de modelo são mais promissoras antes de qualquer otimização.")
initial_performance_data = []
X_test = st.session_state.data['X_test']
y_test = st.session_state.data['y_test']
X_test_scaled = st.session_state.scaler.transform(X_test)
for name, model in st.session_state.initial_models.items():
if name in ["KNN", "SVM"]:
y_pred = model.predict(X_test_scaled)
y_prob = model.predict_proba(X_test_scaled)[:, 1]
else:
y_pred = model.predict(X_test)
y_prob = model.predict_proba(X_test)[:, 1]
initial_performance_data.append({
"Modelo": name, "AUC": roc_auc_score(y_test, y_prob), "Acurácia": accuracy_score(y_test, y_pred),
"Precisão": precision_score(y_test, y_pred), "Recall": recall_score(y_test, y_pred), "F1-Score": f1_score(y_test, y_pred)
})
initial_perf_df = pd.DataFrame(initial_performance_data).sort_values(by="AUC", ascending=False)
st.dataframe(initial_perf_df.style.background_gradient(cmap='viridis', subset=['AUC']), use_container_width=True)
st.subheader("Curvas ROC Comparativas (Modelos Padrão)")
fig, ax = plt.subplots(figsize=(10, 8))
for _, row in initial_perf_df.iterrows():
model = st.session_state.initial_models[row['Modelo']]
if row['Modelo'] in ["KNN", "SVM"]:
y_prob = model.predict_proba(X_test_scaled)[:,1]
else:
y_prob = model.predict_proba(X_test)[:,1]
fpr, tpr, _ = roc_curve(y_test, y_prob)
ax.plot(fpr, tpr, label=f"{row['Modelo']} (AUC = {row['AUC']:.3f})")
ax.plot([0, 1], [0, 1], 'k--', label='Aleatório (AUC = 0.50)')
ax.set_xlabel('Taxa de Falsos Positivos'); ax.set_ylabel('Taxa de Verdadeiros Positivos')
ax.set_title('Curva ROC para Modelos Padrão'); ax.legend(); ax.grid(True)
st.pyplot(fig)
with tab3:
st.header("Otimização de Modelos e Análise Estatística")
st.markdown("Aqui detalhamos a busca pelos melhores hiperparâmetros para os modelos selecionados e apresentamos uma análise de regressão para entender a significância estatística de cada variável.")
if not models_to_optimize:
st.warning("Nenhum modelo foi selecionado para otimização na barra lateral.")
else:
st.subheader("Resultados da Otimização (GridSearchCV)")
for model_name, grid_result in st.session_state.optimization_results.items():
with st.expander(f"Resultados para {model_name}"):
st.metric(f"Melhor Score AUC (Validação Cruzada)", f"{grid_result.best_score_:.4f}")
st.write("**Melhores Parâmetros Encontrados:**")
params_df = pd.DataFrame([grid_result.best_params_]).T.rename(columns={0: "Valor"})
st.table(params_df)
st.divider()
st.subheader("Análise Estatística com Regressão Logística (Significância das Variáveis)")
st.info("A tabela abaixo mostra o resultado de um modelo de regressão logística. A coluna `p-valor` é crucial: valores menores que 0.05 (em verde) indicam que a variável tem um impacto estatisticamente significante na previsão de reclamações.")
stats_df_styled = st.session_state.stats_summary.style \
.applymap(style_p_value, subset=['p-valor']) \
.format({
'Coeficiente': '{:.4f}', 'p-valor': '{:.4f}', 'Odds Ratio': '{:.3f}',
'IC 95% Inferior': '{:.3f}', 'IC 95% Superior': '{:.3f}'
})
st.dataframe(stats_df_styled, use_container_width=True)
with tab4:
st.header("Comparativo e Desempenho do Modelo Final")
st.markdown("Após a otimização, comparamos o desempenho de todos os modelos no conjunto de teste para selecionar o campeão final. Em seguida, analisamos este modelo em detalhes.")
if not st.session_state.optimization_results:
st.error("Execute a otimização de pelo menos um modelo para ver esta análise.")
else:
performance_data = []
best_overall_model = None
best_overall_score = -1
best_overall_name = ""
for name, grid in st.session_state.optimization_results.items():
model = grid.best_estimator_
y_pred = model.predict(st.session_state.data['X_test'])
y_prob = model.predict_proba(st.session_state.data['X_test'])[:, 1]
auc = roc_auc_score(st.session_state.data['y_test'], y_prob)
performance_data.append({
"Modelo": f"{name} (Otimizado)", "AUC": auc, "Acurácia": accuracy_score(st.session_state.data['y_test'], y_pred),
"Precisão": precision_score(st.session_state.data['y_test'], y_pred), "Recall": recall_score(st.session_state.data['y_test'], y_pred),
"F1-Score": f1_score(st.session_state.data['y_test'], y_pred)
})
if auc > best_overall_score:
best_overall_score = auc
best_overall_model = model
best_overall_name = name
st.session_state.final_model_name = f"{best_overall_name} (Otimizado)"
st.session_state.final_model = best_overall_model
performance_df = pd.DataFrame(performance_data).sort_values(by="AUC", ascending=False)
st.subheader("Tabela Comparativa de Desempenho (Modelos Otimizados)")
st.dataframe(performance_df.style.background_gradient(cmap='viridis', subset=['AUC']), use_container_width=True)
st.subheader(f"Análise Detalhada do Melhor Modelo: {st.session_state.final_model_name}")
y_pred_final = st.session_state.final_model.predict(st.session_state.data['X_test'])
y_prob_final = st.session_state.final_model.predict_proba(st.session_state.data['X_test'])[:, 1]
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 6))
disp = ConfusionMatrixDisplay.from_estimator(st.session_state.final_model, st.session_state.data['X_test'], st.session_state.data['y_test'], cmap=plt.cm.Blues, ax=ax1)
ax1.set_title(f'Matriz de Confusão'); disp_roc = roc_curve(st.session_state.data['y_test'], y_prob_final)
ax2.plot(disp_roc[0], disp_roc[1], color='darkorange', lw=2, label=f'Curva ROC (AUC = {best_overall_score:.3f})')
ax2.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--', label='Aleatório')
ax2.set_title(f'Curva ROC'); ax2.legend(loc='lower right'); ax2.grid(True)
st.pyplot(fig)
with tab5:
st.header("Análise Gerencial e Tomada de Decisão")
if 'final_model' not in st.session_state or st.session_state.final_model is None:
st.error("A análise gerencial não pode ser gerada. Execute a análise primeiro.")
else:
st.markdown("Esta seção traduz os resultados do modelo em insights acionáveis para a estratégia de negócio, focando nos fatores que mais influenciam a insatisfação do cliente.")
st.subheader("Importância das Variáveis do Modelo Final")
st.info(f"As variáveis a seguir são as que mais influenciam a previsão de reclamação, de acordo com o modelo **{st.session_state.final_model_name}**.")
if hasattr(st.session_state.final_model, 'feature_importances_'):
importances = st.session_state.final_model.feature_importances_
importance_df = pd.DataFrame({
'Variável': st.session_state.X_final_cols,
'Importância': importances
}).sort_values(by='Importância', ascending=False)
fig, ax = plt.subplots(figsize=(10, 6))
sns.barplot(x='Importância', y='Variável', data=importance_df.head(10), ax=ax, palette="rocket")
ax.set_title(f'Top 10 Variáveis Mais Importantes'); ax.grid(axis='x')
st.pyplot(fig)
top1_feature, top2_feature, top3_feature = importance_df['Variável'].iloc[0:3]
st.divider()
st.subheader("Recomendação Estratégica para o Negócio")
recomendacao_texto = f"""
O modelo **{st.session_state.final_model_name}** nos permitiu identificar com alta precisão os principais fatores de risco para reclamações. Os três mais impactantes são:
1. **{top1_feature}**
2. **{top2_feature}**
3. **{top3_feature}**
Com base nisso, propomos um plano de ação focado em retenção proativa:
#### **Ação Sugerida:**
* **Criação de Alertas Automáticos (Triggers):**
Implementar no CRM ou sistema de BI alertas que identifiquem clientes cujo perfil se encaixe nos fatores de risco. Por exemplo, se **'{top1_feature}'** for 'Recency' (tempo desde a última compra), um alerta pode ser gerado para clientes inativos há mais de X dias que também possuam outras características de risco.
* **Segmentação para Atendimento Prioritário:**
Clientes sinalizados devem entrar em uma fila de atendimento prioritário. A equipe de Sucesso do Cliente pode contatá-los proativamente para oferecer ajuda, coletar feedback ou apresentar uma oferta especial de reengajamento, antes mesmo que a insatisfação se manifeste como uma reclamação formal.
* **Análise de Causa Raiz:**
É fundamental investigar *por que* variáveis como **'{top2_feature}'** e **'{top3_feature}'** são tão influentes. Isso pode apontar para problemas mais profundos, como falhas na usabilidade do site, gargalos logísticos ou desalinhamento entre o marketing e o produto entregue.
Ao adotar essa abordagem, a empresa transforma dados em uma ferramenta estratégica, passando de uma postura reativa para uma **gestão proativa da satisfação do cliente**, o que tende a reduzir custos de suporte e aumentar a lealdade (LTV).
"""
st.markdown(recomendacao_texto)
else:
st.warning("Não foi possível extrair a importância das variáveis para este tipo de modelo de forma direta.")
else:
st.warning("👈 Configure os parâmetros na barra lateral e clique em 'Iniciar Análise Preditiva' para gerar o dashboard.")
st.markdown("Aguardando o início da análise para exibir os resultados...")