#!/usr/bin/env python # coding: utf-8 import warnings import numpy as np import pandas as pd import plotly.express as px import plotly.graph_objects as go import streamlit as st from imblearn.over_sampling import SMOTE from sklearn.linear_model import LogisticRegression from sklearn.metrics import ( accuracy_score, classification_report, confusion_matrix, f1_score, precision_score, recall_score, roc_auc_score, roc_curve, ) from sklearn.model_selection import train_test_split from sklearn.preprocessing import LabelEncoder, StandardScaler from statsmodels.stats.outliers_influence import variance_inflation_factor warnings.filterwarnings("ignore") st.set_page_config( page_title="Análise de Regressão Logística - Churn Bancário", page_icon="🏦", layout="wide", ) st.markdown(""" # Tarefa 5 de AEDI - Análise de Regressão Logística de Churn Bancário **Churn Modelling Dataset** - [Kaggle](https://www.kaggle.com/datasets/shrutimechlearn/churn-modelling) - **Autor:** Hugo Honda - **Matrícula:** 252106924 - **Disciplina:** AEDI - PPCA/UnB - **Data:** Outubro de 2025 """) # ============================= # Carregamento e preparação dos dados # ============================= @st.cache_data def load_data(): """Carrega e prepara os dados de churn bancário""" try: df = pd.read_csv("Churn_Modelling.csv", na_values=["NA", "", "NaN"]) # Remover colunas não relevantes df_clean = df.drop(["RowNumber", "CustomerId", "Surname"], axis=1) # Codificar variáveis categóricas le_geography = LabelEncoder() le_gender = LabelEncoder() df_clean["Geography_encoded"] = le_geography.fit_transform( df_clean["Geography"] ) df_clean["Gender_encoded"] = le_gender.fit_transform(df_clean["Gender"]) # Selecionar variáveis para o modelo features = [ "CreditScore", "Age", "Tenure", "Balance", "NumOfProducts", "HasCrCard", "IsActiveMember", "EstimatedSalary", "Geography_encoded", "Gender_encoded", ] X = df_clean[features] y = df_clean["Exited"] return df_clean, X, y, features, le_geography, le_gender except FileNotFoundError as e: st.error(f""" **Erro ao carregar dados:** {str(e)} **Possíveis soluções:** 1. Verifique se o arquivo Churn_Modelling.csv está presente 2. Certifique-se de que o Dockerfile está copiando o arquivo corretamente 3. Verifique as permissões do arquivo no container """) return None, None, None, None, None, None except Exception as e: st.error(f"Erro inesperado ao carregar dados: {str(e)}") return None, None, None, None, None, None # Carregar dados df_clean, X, y, features, le_geography, le_gender = load_data() if df_clean is not None: # ============================= # Sidebar - Controles # ============================= st.sidebar.header("Controles da Análise") # Parâmetros do modelo st.sidebar.subheader("Parâmetros do Modelo") test_size = st.sidebar.slider("Tamanho do conjunto de teste:", 0.1, 0.4, 0.2, 0.05) random_state = st.sidebar.number_input("Random State:", 1, 1000, 42) # Opções de análise st.sidebar.subheader("Opções de Análise") show_assumptions = st.sidebar.checkbox("Mostrar validação de pressupostos", True) use_balancing = st.sidebar.checkbox("Usar balanceamento (SMOTE)", False) # ============================= # Análise Exploratória # ============================= st.header("Análise Exploratória dos Dados") col1, col2, col3, col4 = st.columns(4) with col1: st.metric("Total de Observações", len(df_clean)) with col2: st.metric("Taxa de Churn", f"{y.mean():.1%}") with col3: st.metric("Clientes que Não Churnaram", f"{len(y[y == 0]):,}") with col4: st.metric("Clientes que Churnaram", f"{len(y[y == 1]):,}") # Distribuição do churn fig_hist = px.pie( values=y.value_counts().values, names=["Não Churn", "Churn"], title="Distribuição de Churn", ) st.plotly_chart(fig_hist, use_container_width=True) # Matriz de correlação corr_data = X.corr() fig_corr = px.imshow( corr_data, text_auto=True, aspect="auto", title="Matriz de Correlação" ) st.plotly_chart(fig_corr, use_container_width=True) # ============================= # Modelagem # ============================= st.header("Modelagem de Regressão Logística") # Divisão treino-teste X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=test_size, random_state=random_state, stratify=y ) # Padronização scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test) # Balanceamento (se solicitado) if use_balancing: st.info("Aplicando SMOTE para balanceamento...") smote = SMOTE(random_state=random_state) X_train_balanced, y_train_balanced = smote.fit_resample(X_train_scaled, y_train) st.write( f"Após balanceamento: {pd.Series(y_train_balanced).value_counts().to_dict()}" ) else: X_train_balanced, y_train_balanced = X_train_scaled, y_train # Modelo de regressão logística model = LogisticRegression(random_state=random_state, max_iter=1000) model.fit(X_train_balanced, y_train_balanced) # Predições y_pred = model.predict(X_test_scaled) y_pred_proba = model.predict_proba(X_test_scaled)[:, 1] # ============================= # Resultados do Modelo # ============================= st.subheader("Resultados do Modelo") # Métricas col1, col2, col3 = st.columns(3) with col1: accuracy = accuracy_score(y_test, y_pred) precision = precision_score(y_test, y_pred) st.metric("Acurácia", f"{accuracy:.4f}") st.metric("Precisão", f"{precision:.4f}") with col2: recall = recall_score(y_test, y_pred) f1 = f1_score(y_test, y_pred) st.metric("Sensibilidade (Recall)", f"{recall:.4f}") st.metric("F1-Score", f"{f1:.4f}") with col3: auc = roc_auc_score(y_test, y_pred_proba) st.metric("AUC-ROC", f"{auc:.4f}") # Interpretação st.info(f""" **Interpretação das Métricas:** - **AUC-ROC = {auc:.4f}**: {"Excelente" if auc > 0.8 else "Bom" if auc > 0.7 else "Moderado"} poder discriminativo - **Acurácia = {accuracy:.4f}**: {accuracy * 100:.1f}% de predições corretas - **F1-Score = {f1:.4f}**: Balanceamento entre precisão e sensibilidade """) # Coeficientes das variáveis coefficients = model.coef_[0] odds_ratios = np.exp(coefficients) coef_df = pd.DataFrame( { "Variável": features, "Coeficiente": coefficients, "Odds_Ratio": odds_ratios, "Abs_Coefficient": np.abs(coefficients), } ).sort_values("Abs_Coefficient", ascending=False) st.subheader("Variáveis Mais Importantes") fig_coef = px.bar( coef_df.head(10), x="Coeficiente", y="Variável", orientation="h", title="Coeficientes das Variáveis Mais Importantes", color="Abs_Coefficient", color_continuous_scale="RdYlBu_r", ) fig_coef.update_layout(height=400) st.plotly_chart(fig_coef, use_container_width=True) # Interpretação dos coeficientes st.subheader("Interpretação dos Odds Ratios") for _, row in coef_df.head(5).iterrows(): if row["Odds_Ratio"] > 1: effect = f"aumenta a probabilidade de churn em {(row['Odds_Ratio'] - 1) * 100:.1f}%" else: effect = f"diminui a probabilidade de churn em {(1 - row['Odds_Ratio']) * 100:.1f}%" st.write(f"**{row['Variável']}**: {effect}") # ============================= # Validação de Pressupostos # ============================= if show_assumptions: st.header("Validação dos Pressupostos") col1, col2 = st.columns(2) with col1: st.subheader("Testes Estatísticos") # Multicolinearidade (VIF) vif_data = pd.DataFrame() vif_data["Variável"] = X.columns vif_data["VIF"] = [ variance_inflation_factor(X.values, i) for i in range(X.shape[1]) ] high_vif_count = len(vif_data[vif_data["VIF"] > 10]) st.write(f"**Multicolinearidade (VIF > 10):** {high_vif_count} variáveis") # Balanceamento das classes class_counts = y.value_counts() imbalance_ratio = class_counts[0] / class_counts[1] st.write(f"**Balanceamento das Classes:** {imbalance_ratio:.2f}:1") with col2: st.subheader("Visualizações") # Distribuição das classes fig_balance = px.bar( x=["Não Churn", "Churn"], y=class_counts.values, title="Distribuição das Classes", ) st.plotly_chart(fig_balance, use_container_width=True) # Avaliação dos pressupostos multicoll_ok = high_vif_count == 0 balance_ok = imbalance_ratio < 2 st.subheader("Avaliação dos Pressupostos") col1, col2 = st.columns(2) with col1: st.metric("Multicolinearidade", "OK" if multicoll_ok else "Alta") with col2: st.metric("Balanceamento", "OK" if balance_ok else "Desbalanceado") # ============================= # Avaliação do Modelo # ============================= st.header("Avaliação do Modelo") # Matriz de confusão cm = confusion_matrix(y_test, y_pred) fig_cm = px.imshow( cm, text_auto=True, aspect="auto", title="Matriz de Confusão", labels=dict(x="Predito", y="Real", color="Contagem"), ) st.plotly_chart(fig_cm, use_container_width=True) # Análise da Curva ROC e AUC fpr, tpr, thresholds = roc_curve(y_test, y_pred_proba) # Interpretação do AUC if auc >= 0.9: interpretation = "Excelente" elif auc >= 0.8: interpretation = "Muito Bom" elif auc >= 0.7: interpretation = "Bom" elif auc >= 0.6: interpretation = "Razoável" else: interpretation = "Ruim" st.info(f"**AUC-ROC: {auc:.4f}** - Interpretação: {interpretation}") # Curva ROC com área preenchida fig_roc = go.Figure() # Área sob a curva fig_roc.add_trace( go.Scatter( x=fpr, y=tpr, fill="tonexty", mode="none", name=f"AUC = {auc:.3f}", fillcolor="rgba(0,100,80,0.2)", ) ) # Curva ROC fig_roc.add_trace( go.Scatter( x=fpr, y=tpr, mode="lines", name="ROC Curve", line=dict(color="blue", width=3), ) ) # Linha de referência fig_roc.add_trace( go.Scatter( x=[0, 1], y=[0, 1], mode="lines", name="Random Classifier", line=dict(color="red", dash="dash"), ) ) fig_roc.update_layout( title=f"Curva ROC (AUC = {auc:.4f})", xaxis_title="Taxa de Falsos Positivos", yaxis_title="Taxa de Verdadeiros Positivos", height=500, ) st.plotly_chart(fig_roc, use_container_width=True) # Análise de threshold ótimo youden_j = tpr - fpr optimal_idx = np.argmax(youden_j) optimal_threshold = thresholds[optimal_idx] st.write( f"**Threshold ótimo:** {optimal_threshold:.3f} (Youden's J: {youden_j[optimal_idx]:.3f})" ) # Análise de Thresholds st.subheader("Análise de Thresholds") # Calcular métricas para diferentes thresholds thresholds_analysis = [] thresholds_to_test = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9] for threshold in thresholds_to_test: y_pred_thresh = (y_pred_proba >= threshold).astype(int) acc = accuracy_score(y_test, y_pred_thresh) prec = precision_score(y_test, y_pred_thresh) rec = recall_score(y_test, y_pred_thresh) f1 = f1_score(y_test, y_pred_thresh) thresholds_analysis.append( { "Threshold": threshold, "Accuracy": acc, "Precision": prec, "Recall": rec, "F1-Score": f1, } ) thresholds_df = pd.DataFrame(thresholds_analysis) # Visualização das métricas por threshold fig_thresh = go.Figure() fig_thresh.add_trace( go.Scatter( x=thresholds_df["Threshold"], y=thresholds_df["Accuracy"], mode="lines+markers", name="Acurácia", line=dict(color="blue"), ) ) fig_thresh.add_trace( go.Scatter( x=thresholds_df["Threshold"], y=thresholds_df["Precision"], mode="lines+markers", name="Precisão", line=dict(color="green"), ) ) fig_thresh.add_trace( go.Scatter( x=thresholds_df["Threshold"], y=thresholds_df["Recall"], mode="lines+markers", name="Recall", line=dict(color="red"), ) ) fig_thresh.add_trace( go.Scatter( x=thresholds_df["Threshold"], y=thresholds_df["F1-Score"], mode="lines+markers", name="F1-Score", line=dict(color="orange"), ) ) fig_thresh.update_layout( title="Métricas por Threshold", xaxis_title="Threshold", yaxis_title="Valor", height=400, ) st.plotly_chart(fig_thresh, use_container_width=True) # Melhor threshold para F1-Score best_f1_idx = thresholds_df["F1-Score"].idxmax() best_f1_threshold = thresholds_df.loc[best_f1_idx, "Threshold"] best_f1_score = thresholds_df.loc[best_f1_idx, "F1-Score"] st.write( f"**Melhor F1-Score:** {best_f1_score:.4f} (threshold: {best_f1_threshold:.1f})" ) # Relatório de classificação st.subheader("Relatório de Classificação") st.text(classification_report(y_test, y_pred)) # ============================= # Resumo dos Resultados # ============================= st.header("Resumo dos Resultados") # Calcular métricas do modelo models_summary = pd.DataFrame( { "Métrica": ["Acurácia", "Precisão", "Sensibilidade", "F1-Score", "AUC-ROC"], "Valor": [accuracy, precision, recall, f1, auc], } ) # Exibir resumo col1, col2 = st.columns(2) with col1: st.subheader("Performance do Modelo") st.dataframe(models_summary, use_container_width=True) with col2: st.subheader("Status dos Pressupostos") if show_assumptions: assumptions_status = pd.DataFrame( { "Pressuposto": ["Multicolinearidade", "Balanceamento"], "Status": [ "OK" if multicoll_ok else "Alta", "OK" if balance_ok else "Desbalanceado", ], "Valor": [ f"{high_vif_count} variáveis VIF > 10", f"{imbalance_ratio:.2f}:1", ], } ) st.dataframe(assumptions_status, use_container_width=True) else: st.info( "Ative 'Mostrar validação de pressupostos' para ver o status detalhado." ) # Conclusões principais st.subheader("Principais Conclusões") assumptions_ok = show_assumptions and multicoll_ok and balance_ok st.success(f""" **Modelo de regressão logística implementado com sucesso** **Interpretação:** - O modelo tem {interpretation.lower()} poder discriminativo (AUC = {auc:.4f}) - Acurácia de {accuracy * 100:.1f}% nas predições - F1-Score de {f1:.4f} (balanceamento entre precisão e sensibilidade) - {"Todos os pressupostos foram atendidos" if assumptions_ok else "Alguns pressupostos podem ter sido violados"} - Variáveis mais importantes: {", ".join(coef_df.head(3)["Variável"].tolist())} """) # Resumo das variáveis mais importantes st.subheader("Variáveis Mais Importantes para Prever Churn") top_vars = coef_df.head(5) for _, row in top_vars.iterrows(): st.write(f"**{row['Variável']}**: Odds Ratio = {row['Odds_Ratio']:.3f}") # Performance final st.subheader("Performance Final do Modelo") col1, col2, col3 = st.columns(3) with col1: st.metric("AUC-ROC", f"{auc:.4f}") with col2: st.metric("Acurácia", f"{accuracy:.4f}") with col3: st.metric("F1-Score", f"{f1:.4f}") else: st.error( "Erro ao carregar os dados. Verifique se o arquivo " "Churn_Modelling.csv está presente no diretório." ) # ============================= # Footer # ============================= st.markdown("---") st.markdown(""" **PPCA/UnB** | **Outubro 2025** """)