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