hugohonda's picture
update
ffc2e6b
#!/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**
""")