|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
""") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@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"]) |
|
|
|
|
|
|
|
|
df_clean = df.drop(["RowNumber", "CustomerId", "Surname"], axis=1) |
|
|
|
|
|
|
|
|
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"]) |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
df_clean, X, y, features, le_geography, le_gender = load_data() |
|
|
|
|
|
if df_clean is not None: |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
st.sidebar.header("Controles da Análise") |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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]):,}") |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
st.header("Modelagem de Regressão Logística") |
|
|
|
|
|
|
|
|
X_train, X_test, y_train, y_test = train_test_split( |
|
|
X, y, test_size=test_size, random_state=random_state, stratify=y |
|
|
) |
|
|
|
|
|
|
|
|
scaler = StandardScaler() |
|
|
X_train_scaled = scaler.fit_transform(X_train) |
|
|
X_test_scaled = scaler.transform(X_test) |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
model = LogisticRegression(random_state=random_state, max_iter=1000) |
|
|
model.fit(X_train_balanced, y_train_balanced) |
|
|
|
|
|
|
|
|
y_pred = model.predict(X_test_scaled) |
|
|
y_pred_proba = model.predict_proba(X_test_scaled)[:, 1] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
st.subheader("Resultados do Modelo") |
|
|
|
|
|
|
|
|
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}") |
|
|
|
|
|
|
|
|
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 |
|
|
""") |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
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}") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if show_assumptions: |
|
|
st.header("Validação dos Pressupostos") |
|
|
|
|
|
col1, col2 = st.columns(2) |
|
|
|
|
|
with col1: |
|
|
st.subheader("Testes Estatísticos") |
|
|
|
|
|
|
|
|
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") |
|
|
|
|
|
|
|
|
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") |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
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") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
st.header("Avaliação do Modelo") |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
fpr, tpr, thresholds = roc_curve(y_test, y_pred_proba) |
|
|
|
|
|
|
|
|
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}") |
|
|
|
|
|
|
|
|
fig_roc = go.Figure() |
|
|
|
|
|
|
|
|
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)", |
|
|
) |
|
|
) |
|
|
|
|
|
|
|
|
fig_roc.add_trace( |
|
|
go.Scatter( |
|
|
x=fpr, |
|
|
y=tpr, |
|
|
mode="lines", |
|
|
name="ROC Curve", |
|
|
line=dict(color="blue", width=3), |
|
|
) |
|
|
) |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
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})" |
|
|
) |
|
|
|
|
|
|
|
|
st.subheader("Análise de 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) |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
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})" |
|
|
) |
|
|
|
|
|
|
|
|
st.subheader("Relatório de Classificação") |
|
|
st.text(classification_report(y_test, y_pred)) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
st.header("Resumo dos Resultados") |
|
|
|
|
|
|
|
|
models_summary = pd.DataFrame( |
|
|
{ |
|
|
"Métrica": ["Acurácia", "Precisão", "Sensibilidade", "F1-Score", "AUC-ROC"], |
|
|
"Valor": [accuracy, precision, recall, f1, auc], |
|
|
} |
|
|
) |
|
|
|
|
|
|
|
|
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." |
|
|
) |
|
|
|
|
|
|
|
|
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())} |
|
|
""") |
|
|
|
|
|
|
|
|
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}") |
|
|
|
|
|
|
|
|
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." |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
st.markdown("---") |
|
|
st.markdown(""" |
|
|
**PPCA/UnB** | **Outubro 2025** |
|
|
""") |
|
|
|