Update src/streamlit_app.py
Browse files- src/streamlit_app.py +75 -46
src/streamlit_app.py
CHANGED
|
@@ -10,7 +10,8 @@ from sklearn.tree import DecisionTreeClassifier
|
|
| 10 |
from sklearn.ensemble import RandomForestClassifier, AdaBoostClassifier, GradientBoostingClassifier
|
| 11 |
from xgboost import XGBClassifier
|
| 12 |
from lightgbm import LGBMClassifier
|
| 13 |
-
from sklearn.metrics import roc_auc_score, roc_curve, accuracy_score, precision_score, recall_score, f1_score,
|
|
|
|
| 14 |
import matplotlib.pyplot as plt
|
| 15 |
import seaborn as sns
|
| 16 |
import numpy as np
|
|
@@ -18,12 +19,13 @@ import io
|
|
| 18 |
from sklearn.feature_selection import RFE
|
| 19 |
from sklearn.linear_model import LogisticRegression
|
| 20 |
|
| 21 |
-
|
| 22 |
# Configuração da página do Streamlit
|
| 23 |
st.set_page_config(layout="wide", page_title="Previsão de Reclamações de Clientes")
|
| 24 |
|
| 25 |
st.title("📊 Previsão de Reclamações de Clientes com Modelos Supervisionados")
|
| 26 |
-
st.markdown(
|
|
|
|
|
|
|
| 27 |
|
| 28 |
# --- Carregamento e Pré-processamento dos Dados ---
|
| 29 |
@st.cache_data
|
|
@@ -36,6 +38,7 @@ def load_data():
|
|
| 36 |
st.stop()
|
| 37 |
return df
|
| 38 |
|
|
|
|
| 39 |
@st.cache_data
|
| 40 |
def preprocess_data(df):
|
| 41 |
df_processed = df.copy()
|
|
@@ -44,7 +47,7 @@ def preprocess_data(df):
|
|
| 44 |
df_processed['Dt_Customer'] = pd.to_datetime(df_processed['Dt_Customer'], format='%d-%m-%Y')
|
| 45 |
reference_date = df_processed['Dt_Customer'].min()
|
| 46 |
df_processed['Days_Since_Customer'] = (df_processed['Dt_Customer'] - reference_date).dt.days
|
| 47 |
-
df_processed = df_processed.drop('Dt_Customer', axis=1)
|
| 48 |
|
| 49 |
# Coerção explícita para numérico para colunas que podem vir como 'object'
|
| 50 |
cols_to_coerce_numeric = [
|
|
@@ -57,21 +60,23 @@ def preprocess_data(df):
|
|
| 57 |
for col in cols_to_coerce_numeric:
|
| 58 |
if col in df_processed.columns:
|
| 59 |
df_processed[col] = pd.to_numeric(df_processed[col], errors='coerce')
|
| 60 |
-
df_processed[col] = df_processed[col].fillna(0)
|
| 61 |
|
| 62 |
# Convertendo variáveis categóricas em numéricas (one-hot encoding)
|
| 63 |
df_processed = pd.get_dummies(df_processed, columns=['Education', 'Marital_Status'], drop_first=True)
|
| 64 |
|
| 65 |
# Excluir colunas irrelevantes e com variância zero
|
| 66 |
cols_to_drop = ['ID', 'Z_CostContact', 'Z_Revenue']
|
| 67 |
-
df_processed = df_processed.drop(columns=[col for col in cols_to_drop if col in df_processed.columns], axis=1,
|
| 68 |
-
|
|
|
|
| 69 |
# Remover colunas com variância zero (constantes) ou com muitos nulos após o pré-processamento
|
| 70 |
-
df_processed = df_processed.loc[:, df_processed.nunique() > 1]
|
| 71 |
-
df_processed = df_processed.dropna(axis=1, how='all')
|
| 72 |
|
| 73 |
return df_processed
|
| 74 |
|
|
|
|
| 75 |
# Função para treinar e avaliar modelos
|
| 76 |
@st.cache_data(show_spinner=False)
|
| 77 |
def train_and_evaluate_models(X_train_raw, X_test_raw, y_train, y_test, _scaler, model_selected=None):
|
|
@@ -95,26 +100,28 @@ def train_and_evaluate_models(X_train_raw, X_test_raw, y_train, y_test, _scaler,
|
|
| 95 |
|
| 96 |
# Check if y_train has at least two classes before attempting to train (real training calls)
|
| 97 |
if len(np.unique(y_train)) < 2:
|
| 98 |
-
st.error(
|
|
|
|
| 99 |
return {}
|
| 100 |
|
| 101 |
# Check if X_train_raw has enough samples (real training calls)
|
| 102 |
if X_train_raw.shape[0] == 0:
|
| 103 |
st.error("Erro: Dados de treino com 0 amostras. Não é possível treinar modelos.")
|
| 104 |
return {}
|
| 105 |
-
|
| 106 |
# Verificar se os dtypes são numéricos antes de treinar (real training calls)
|
| 107 |
for col in X_train_raw.columns:
|
| 108 |
if not pd.api.types.is_numeric_dtype(X_train_raw[col]):
|
| 109 |
-
st.error(
|
|
|
|
| 110 |
return {}
|
| 111 |
|
| 112 |
for col in X_test_raw.columns:
|
| 113 |
if not pd.api.types.is_numeric_dtype(X_test_raw[col]):
|
| 114 |
-
st.error(
|
|
|
|
| 115 |
return {}
|
| 116 |
|
| 117 |
-
|
| 118 |
for name, model in models.items():
|
| 119 |
if model_selected and name != model_selected:
|
| 120 |
continue
|
|
@@ -123,14 +130,14 @@ def train_and_evaluate_models(X_train_raw, X_test_raw, y_train, y_test, _scaler,
|
|
| 123 |
if name in ["K-Nearest Neighbors", "Support Vector Machine"]:
|
| 124 |
X_train_processed = _scaler.fit_transform(X_train_raw)
|
| 125 |
X_test_processed = _scaler.transform(X_test_raw)
|
| 126 |
-
else:
|
| 127 |
X_train_processed = X_train_raw
|
| 128 |
X_test_processed = X_test_raw
|
| 129 |
|
| 130 |
try:
|
| 131 |
model.fit(X_train_processed, y_train)
|
| 132 |
y_pred = model.predict(X_test_processed)
|
| 133 |
-
|
| 134 |
# Correção para IndexError no predict_proba
|
| 135 |
if hasattr(model, 'predict_proba'):
|
| 136 |
probas = model.predict_proba(X_test_processed)
|
|
@@ -139,7 +146,7 @@ def train_and_evaluate_models(X_train_raw, X_test_raw, y_train, y_test, _scaler,
|
|
| 139 |
else:
|
| 140 |
y_prob = probas[:, 0]
|
| 141 |
else:
|
| 142 |
-
y_prob = y_pred
|
| 143 |
|
| 144 |
# Calcular ROC AUC apenas se y_prob não for totalmente binário (0 ou 1)
|
| 145 |
if len(np.unique(y_prob)) > 1:
|
|
@@ -168,11 +175,13 @@ def train_and_evaluate_models(X_train_raw, X_test_raw, y_train, y_test, _scaler,
|
|
| 168 |
# O warning será exibido APENAS se o treinamento real for solicitado (nos botões "Treinar" ou "Analisar")
|
| 169 |
results[name] = {
|
| 170 |
"Model": None, "Accuracy": 0, "Precision": 0, "Recall": 0, "F1-score": 0,
|
| 171 |
-
"AUC": 0.5, "Confusion Matrix": np.array([[0,0],[0,0]]), "FPR": [0,1], "TPR": [0,1],
|
|
|
|
| 172 |
}
|
| 173 |
continue
|
| 174 |
return results
|
| 175 |
|
|
|
|
| 176 |
# --- Carregar e Pré-processar os dados ---
|
| 177 |
df = load_data()
|
| 178 |
df_processed = preprocess_data(df)
|
|
@@ -186,17 +195,20 @@ st.sidebar.header("⚙️ Configurações do Modelo")
|
|
| 186 |
# Balanceamento da Base
|
| 187 |
st.sidebar.subheader("Balanceamento de Dados (SMOTE)")
|
| 188 |
balance_data = st.sidebar.checkbox("Aplicar SMOTE", value=True)
|
| 189 |
-
st.sidebar.info(
|
|
|
|
| 190 |
|
| 191 |
# Seleção de Variáveis
|
| 192 |
st.sidebar.subheader("Seleção de Variáveis")
|
| 193 |
use_rfe = st.sidebar.checkbox("Usar Seleção de Variáveis (RFE)", value=False)
|
| 194 |
if use_rfe:
|
| 195 |
max_features_rfe = X.shape[1] if X.shape[1] > 5 else 5
|
| 196 |
-
n_features_rfe = st.sidebar.slider("Número de Variáveis a Selecionar (RFE)", 5, max_features_rfe,
|
| 197 |
-
|
|
|
|
|
|
|
| 198 |
estimator_rfe = LogisticRegression(max_iter=1000, random_state=42)
|
| 199 |
-
|
| 200 |
if X.shape[0] > 0 and X.shape[1] >= n_features_rfe:
|
| 201 |
try:
|
| 202 |
selector_rfe = RFE(estimator_rfe, n_features_to_select=n_features_rfe, step=1)
|
|
@@ -208,7 +220,8 @@ if use_rfe:
|
|
| 208 |
st.sidebar.error(f"Erro ao aplicar RFE: {e}. RFE desabilitado.")
|
| 209 |
use_rfe = False
|
| 210 |
else:
|
| 211 |
-
st.sidebar.warning(
|
|
|
|
| 212 |
use_rfe = False
|
| 213 |
|
| 214 |
# Escolha do Modelo
|
|
@@ -217,11 +230,12 @@ st.sidebar.subheader("Seleção de Modelo para Treinamento")
|
|
| 217 |
# === CORREÇÃO: Usar st.session_state para sinalizar a chamada inicial ===
|
| 218 |
st.session_state['is_initial_call'] = True
|
| 219 |
# Criar dados dummy com 1 linha de zeros e todas as colunas de X para ter o shape correto
|
| 220 |
-
dummy_X_for_keys = pd.DataFrame(np.zeros((
|
| 221 |
# y_dummy deve ter pelo menos 2 classes para a função não reclamar
|
| 222 |
-
dummy_y_for_keys = pd.Series([0, 1])
|
| 223 |
-
model_keys = train_and_evaluate_models(dummy_X_for_keys, dummy_X_for_keys, dummy_y_for_keys, dummy_y_for_keys,
|
| 224 |
-
|
|
|
|
| 225 |
|
| 226 |
model_choice = st.sidebar.selectbox(
|
| 227 |
"Escolha o Modelo Principal para Análise Detalhada:",
|
|
@@ -266,7 +280,8 @@ with tab1:
|
|
| 266 |
|
| 267 |
with tab2:
|
| 268 |
st.header("2. Balanceamento de Dados com SMOTE")
|
| 269 |
-
st.write(
|
|
|
|
| 270 |
|
| 271 |
X_display = X.copy()
|
| 272 |
y_display = y.copy()
|
|
@@ -290,7 +305,8 @@ with tab2:
|
|
| 290 |
ax.set_ylabel("Contagem")
|
| 291 |
st.pyplot(fig)
|
| 292 |
except Exception as e:
|
| 293 |
-
st.error(
|
|
|
|
| 294 |
X_res, y_res = X_display, y_display
|
| 295 |
else:
|
| 296 |
st.info("SMOTE desabilitado. O balanceamento não será aplicado.")
|
|
@@ -303,9 +319,11 @@ with tab2:
|
|
| 303 |
st.subheader("Divisão dos Dados (Treino/Teste)")
|
| 304 |
test_size = st.slider("Tamanho do Conjunto de Teste", 0.1, 0.5, 0.3, 0.05)
|
| 305 |
if len(np.unique(y_res)) > 1:
|
| 306 |
-
X_train, X_test, y_train, y_test = train_test_split(X_res, y_res, test_size=test_size, random_state=42,
|
|
|
|
| 307 |
else:
|
| 308 |
-
st.warning(
|
|
|
|
| 309 |
X_train, X_test, y_train, y_test = train_test_split(X_res, y_res, test_size=test_size, random_state=42)
|
| 310 |
|
| 311 |
st.write(f"**Shape dos dados de treino:** {X_train.shape}")
|
|
@@ -327,8 +345,8 @@ with tab2:
|
|
| 327 |
st.stop()
|
| 328 |
|
| 329 |
st.subheader("Escalonamento de Dados")
|
| 330 |
-
st.write(
|
| 331 |
-
|
| 332 |
|
| 333 |
with tab3:
|
| 334 |
st.header("3. Comparação de Modelos Supervisionados")
|
|
@@ -370,7 +388,8 @@ with tab3:
|
|
| 370 |
st.markdown("""
|
| 371 |
Para problemas de previsão de reclamações, o **Recall** é frequentemente crucial, pois minimiza Falsos Negativos (clientes que reclamam mas não são previstos). No entanto, um bom **AUC** (Área sob a Curva ROC) indica a capacidade geral do modelo de distinguir entre as classes, e o **F1-score** oferece um equilíbrio entre Precisão e Recall.
|
| 372 |
""")
|
| 373 |
-
st.success(
|
|
|
|
| 374 |
|
| 375 |
with tab4:
|
| 376 |
st.header("4. Análise Detalhada do Modelo Selecionado")
|
|
@@ -378,10 +397,12 @@ with tab4:
|
|
| 378 |
|
| 379 |
if st.button(f"Analisar {model_choice}"):
|
| 380 |
with st.spinner(f"Analisando {model_choice}..."):
|
| 381 |
-
selected_model_results = train_and_evaluate_models(X_train, X_test, y_train, y_test, StandardScaler(),
|
| 382 |
-
|
|
|
|
| 383 |
if model_choice not in selected_model_results or selected_model_results[model_choice]['Model'] is None:
|
| 384 |
-
st.error(
|
|
|
|
| 385 |
else:
|
| 386 |
metrics = selected_model_results[model_choice]
|
| 387 |
|
|
@@ -394,7 +415,8 @@ with tab4:
|
|
| 394 |
|
| 395 |
st.subheader(f"Matriz de Confusão para {model_choice}")
|
| 396 |
fig_cm, ax_cm = plt.subplots(figsize=(7, 6))
|
| 397 |
-
disp = ConfusionMatrixDisplay(confusion_matrix=metrics['Confusion Matrix'],
|
|
|
|
| 398 |
disp.plot(cmap=plt.cm.Blues, ax=ax_cm)
|
| 399 |
ax_cm.set_title(f'Matriz de Confusão para {model_choice}')
|
| 400 |
st.pyplot(fig_cm)
|
|
@@ -409,7 +431,8 @@ with tab4:
|
|
| 409 |
|
| 410 |
st.subheader(f"Curva ROC para {model_choice}")
|
| 411 |
fig_roc_single, ax_roc_single = plt.subplots(figsize=(8, 6))
|
| 412 |
-
ax_roc_single.plot(metrics['FPR'], metrics['TPR'], color='darkorange', lw=2,
|
|
|
|
| 413 |
ax_roc_single.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--', label='Classificador Aleatório')
|
| 414 |
ax_roc_single.set_xlabel('Taxa de Falsos Positivos (FPR)')
|
| 415 |
ax_roc_single.set_ylabel('Taxa de Verdadeiros Positivos (TPR)')
|
|
@@ -417,7 +440,8 @@ with tab4:
|
|
| 417 |
ax_roc_single.legend(loc='lower right')
|
| 418 |
ax_roc_single.grid(True)
|
| 419 |
st.pyplot(fig_roc_single)
|
| 420 |
-
st.write(
|
|
|
|
| 421 |
|
| 422 |
st.subheader("Sensibilidade aos Hiperparâmetros")
|
| 423 |
if model_choice == "K-Nearest Neighbors":
|
|
@@ -437,7 +461,8 @@ with tab4:
|
|
| 437 |
Modelos de Boosting como XGBoost e LightGBM são influenciados por `n_estimators` (número de estimadores), `learning_rate` (taxa de aprendizado) e `max_depth`. Uma `learning_rate` menor com mais estimadores pode melhorar o desempenho, mas requer mais tempo de treinamento. `Max_depth` controla a complexidade de cada árvore.
|
| 438 |
""")
|
| 439 |
else:
|
| 440 |
-
st.markdown(
|
|
|
|
| 441 |
|
| 442 |
with tab5:
|
| 443 |
st.header("5. Tomada de Decisão e Aplicação Gerencial")
|
|
@@ -445,10 +470,12 @@ with tab5:
|
|
| 445 |
|
| 446 |
if st.button("Gerar Análise Gerencial"):
|
| 447 |
with st.spinner("Gerando insights gerenciais..."):
|
| 448 |
-
selected_model_results = train_and_evaluate_models(X_train, X_test, y_train, y_test, StandardScaler(),
|
| 449 |
-
|
|
|
|
| 450 |
if model_choice not in selected_model_results or selected_model_results[model_choice]['Model'] is None:
|
| 451 |
-
st.error(
|
|
|
|
| 452 |
else:
|
| 453 |
model_instance = selected_model_results[model_choice]["Model"]
|
| 454 |
|
|
@@ -457,7 +484,8 @@ with tab5:
|
|
| 457 |
if hasattr(model_instance, 'feature_importances_'):
|
| 458 |
feature_importances = model_instance.feature_importances_
|
| 459 |
feature_names = X.columns.tolist()
|
| 460 |
-
importance_df = pd.DataFrame(
|
|
|
|
| 461 |
importance_df = importance_df.sort_values(by='Importância Relativa', ascending=False)
|
| 462 |
st.dataframe(importance_df.head(10).set_index('Variável'))
|
| 463 |
|
|
@@ -469,7 +497,8 @@ with tab5:
|
|
| 469 |
elif hasattr(model_instance, 'coef_'):
|
| 470 |
st.info("Para modelos lineares, os coeficientes podem ser interpretados como importância.")
|
| 471 |
else:
|
| 472 |
-
st.info(
|
|
|
|
| 473 |
|
| 474 |
st.subheader("Análise e Recomendações Gerenciais")
|
| 475 |
|
|
|
|
| 10 |
from sklearn.ensemble import RandomForestClassifier, AdaBoostClassifier, GradientBoostingClassifier
|
| 11 |
from xgboost import XGBClassifier
|
| 12 |
from lightgbm import LGBMClassifier
|
| 13 |
+
from sklearn.metrics import roc_auc_score, roc_curve, accuracy_score, precision_score, recall_score, f1_score, \
|
| 14 |
+
confusion_matrix, ConfusionMatrixDisplay
|
| 15 |
import matplotlib.pyplot as plt
|
| 16 |
import seaborn as sns
|
| 17 |
import numpy as np
|
|
|
|
| 19 |
from sklearn.feature_selection import RFE
|
| 20 |
from sklearn.linear_model import LogisticRegression
|
| 21 |
|
|
|
|
| 22 |
# Configuração da página do Streamlit
|
| 23 |
st.set_page_config(layout="wide", page_title="Previsão de Reclamações de Clientes")
|
| 24 |
|
| 25 |
st.title("📊 Previsão de Reclamações de Clientes com Modelos Supervisionados")
|
| 26 |
+
st.markdown(
|
| 27 |
+
"Este dashboard tem como objetivo identificar clientes com maior probabilidade de terem feito uma reclamação nos últimos 2 anos, utilizando modelos de Machine Learning.")
|
| 28 |
+
|
| 29 |
|
| 30 |
# --- Carregamento e Pré-processamento dos Dados ---
|
| 31 |
@st.cache_data
|
|
|
|
| 38 |
st.stop()
|
| 39 |
return df
|
| 40 |
|
| 41 |
+
|
| 42 |
@st.cache_data
|
| 43 |
def preprocess_data(df):
|
| 44 |
df_processed = df.copy()
|
|
|
|
| 47 |
df_processed['Dt_Customer'] = pd.to_datetime(df_processed['Dt_Customer'], format='%d-%m-%Y')
|
| 48 |
reference_date = df_processed['Dt_Customer'].min()
|
| 49 |
df_processed['Days_Since_Customer'] = (df_processed['Dt_Customer'] - reference_date).dt.days
|
| 50 |
+
df_processed = df_processed.drop('Dt_Customer', axis=1) # Remove coluna original de data
|
| 51 |
|
| 52 |
# Coerção explícita para numérico para colunas que podem vir como 'object'
|
| 53 |
cols_to_coerce_numeric = [
|
|
|
|
| 60 |
for col in cols_to_coerce_numeric:
|
| 61 |
if col in df_processed.columns:
|
| 62 |
df_processed[col] = pd.to_numeric(df_processed[col], errors='coerce')
|
| 63 |
+
df_processed[col] = df_processed[col].fillna(0) # Preenche NaN com 0 após coerção, se houver
|
| 64 |
|
| 65 |
# Convertendo variáveis categóricas em numéricas (one-hot encoding)
|
| 66 |
df_processed = pd.get_dummies(df_processed, columns=['Education', 'Marital_Status'], drop_first=True)
|
| 67 |
|
| 68 |
# Excluir colunas irrelevantes e com variância zero
|
| 69 |
cols_to_drop = ['ID', 'Z_CostContact', 'Z_Revenue']
|
| 70 |
+
df_processed = df_processed.drop(columns=[col for col in cols_to_drop if col in df_processed.columns], axis=1,
|
| 71 |
+
errors='ignore')
|
| 72 |
+
|
| 73 |
# Remover colunas com variância zero (constantes) ou com muitos nulos após o pré-processamento
|
| 74 |
+
df_processed = df_processed.loc[:, df_processed.nunique() > 1] # Remove colunas com apenas 1 valor único
|
| 75 |
+
df_processed = df_processed.dropna(axis=1, how='all') # Remove colunas totalmente nulas
|
| 76 |
|
| 77 |
return df_processed
|
| 78 |
|
| 79 |
+
|
| 80 |
# Função para treinar e avaliar modelos
|
| 81 |
@st.cache_data(show_spinner=False)
|
| 82 |
def train_and_evaluate_models(X_train_raw, X_test_raw, y_train, y_test, _scaler, model_selected=None):
|
|
|
|
| 100 |
|
| 101 |
# Check if y_train has at least two classes before attempting to train (real training calls)
|
| 102 |
if len(np.unique(y_train)) < 2:
|
| 103 |
+
st.error(
|
| 104 |
+
"Erro: O conjunto de treino contém apenas uma classe na variável alvo. Verifique o balanceamento ou a divisão dos dados.")
|
| 105 |
return {}
|
| 106 |
|
| 107 |
# Check if X_train_raw has enough samples (real training calls)
|
| 108 |
if X_train_raw.shape[0] == 0:
|
| 109 |
st.error("Erro: Dados de treino com 0 amostras. Não é possível treinar modelos.")
|
| 110 |
return {}
|
| 111 |
+
|
| 112 |
# Verificar se os dtypes são numéricos antes de treinar (real training calls)
|
| 113 |
for col in X_train_raw.columns:
|
| 114 |
if not pd.api.types.is_numeric_dtype(X_train_raw[col]):
|
| 115 |
+
st.error(
|
| 116 |
+
f"Erro: Coluna '{col}' no X_train_raw não é numérica. Tipo: {X_train_raw[col].dtype}. Verifique o pré-processamento.")
|
| 117 |
return {}
|
| 118 |
|
| 119 |
for col in X_test_raw.columns:
|
| 120 |
if not pd.api.types.is_numeric_dtype(X_test_raw[col]):
|
| 121 |
+
st.error(
|
| 122 |
+
f"Erro: Coluna '{col}' no X_test_raw não é numérica. Tipo: {X_test_raw[col].dtype}. Verifique o pré-processamento.")
|
| 123 |
return {}
|
| 124 |
|
|
|
|
| 125 |
for name, model in models.items():
|
| 126 |
if model_selected and name != model_selected:
|
| 127 |
continue
|
|
|
|
| 130 |
if name in ["K-Nearest Neighbors", "Support Vector Machine"]:
|
| 131 |
X_train_processed = _scaler.fit_transform(X_train_raw)
|
| 132 |
X_test_processed = _scaler.transform(X_test_raw)
|
| 133 |
+
else: # Para outros modelos, usamos os dados crus (não escalados)
|
| 134 |
X_train_processed = X_train_raw
|
| 135 |
X_test_processed = X_test_raw
|
| 136 |
|
| 137 |
try:
|
| 138 |
model.fit(X_train_processed, y_train)
|
| 139 |
y_pred = model.predict(X_test_processed)
|
| 140 |
+
|
| 141 |
# Correção para IndexError no predict_proba
|
| 142 |
if hasattr(model, 'predict_proba'):
|
| 143 |
probas = model.predict_proba(X_test_processed)
|
|
|
|
| 146 |
else:
|
| 147 |
y_prob = probas[:, 0]
|
| 148 |
else:
|
| 149 |
+
y_prob = y_pred # fallback, não ideal para AUC
|
| 150 |
|
| 151 |
# Calcular ROC AUC apenas se y_prob não for totalmente binário (0 ou 1)
|
| 152 |
if len(np.unique(y_prob)) > 1:
|
|
|
|
| 175 |
# O warning será exibido APENAS se o treinamento real for solicitado (nos botões "Treinar" ou "Analisar")
|
| 176 |
results[name] = {
|
| 177 |
"Model": None, "Accuracy": 0, "Precision": 0, "Recall": 0, "F1-score": 0,
|
| 178 |
+
"AUC": 0.5, "Confusion Matrix": np.array([[0, 0], [0, 0]]), "FPR": [0, 1], "TPR": [0, 1],
|
| 179 |
+
"y_prob": np.zeros(len(y_test)), "Error": str(e)
|
| 180 |
}
|
| 181 |
continue
|
| 182 |
return results
|
| 183 |
|
| 184 |
+
|
| 185 |
# --- Carregar e Pré-processar os dados ---
|
| 186 |
df = load_data()
|
| 187 |
df_processed = preprocess_data(df)
|
|
|
|
| 195 |
# Balanceamento da Base
|
| 196 |
st.sidebar.subheader("Balanceamento de Dados (SMOTE)")
|
| 197 |
balance_data = st.sidebar.checkbox("Aplicar SMOTE", value=True)
|
| 198 |
+
st.sidebar.info(
|
| 199 |
+
"SMOTE cria amostras sintéticas da classe minoritária para balancear os dados, melhorando o desempenho em datasets desbalanceados.")
|
| 200 |
|
| 201 |
# Seleção de Variáveis
|
| 202 |
st.sidebar.subheader("Seleção de Variáveis")
|
| 203 |
use_rfe = st.sidebar.checkbox("Usar Seleção de Variáveis (RFE)", value=False)
|
| 204 |
if use_rfe:
|
| 205 |
max_features_rfe = X.shape[1] if X.shape[1] > 5 else 5
|
| 206 |
+
n_features_rfe = st.sidebar.slider("Número de Variáveis a Selecionar (RFE)", 5, max_features_rfe,
|
| 207 |
+
min(10, max_features_rfe))
|
| 208 |
+
st.sidebar.info(
|
| 209 |
+
f"O RFE (Recursive Feature Elimination) seleciona as {n_features_rfe} melhores variáveis de forma iterativa.")
|
| 210 |
estimator_rfe = LogisticRegression(max_iter=1000, random_state=42)
|
| 211 |
+
|
| 212 |
if X.shape[0] > 0 and X.shape[1] >= n_features_rfe:
|
| 213 |
try:
|
| 214 |
selector_rfe = RFE(estimator_rfe, n_features_to_select=n_features_rfe, step=1)
|
|
|
|
| 220 |
st.sidebar.error(f"Erro ao aplicar RFE: {e}. RFE desabilitado.")
|
| 221 |
use_rfe = False
|
| 222 |
else:
|
| 223 |
+
st.sidebar.warning(
|
| 224 |
+
f"Não há dados suficientes ({X.shape[0]} amostras ou {X.shape[1]} colunas) para aplicar RFE com {n_features_rfe} features. RFE desabilitado.")
|
| 225 |
use_rfe = False
|
| 226 |
|
| 227 |
# Escolha do Modelo
|
|
|
|
| 230 |
# === CORREÇÃO: Usar st.session_state para sinalizar a chamada inicial ===
|
| 231 |
st.session_state['is_initial_call'] = True
|
| 232 |
# Criar dados dummy com 1 linha de zeros e todas as colunas de X para ter o shape correto
|
| 233 |
+
dummy_X_for_keys = pd.DataFrame(np.zeros((2, X.shape[1])), columns=X.columns)
|
| 234 |
# y_dummy deve ter pelo menos 2 classes para a função não reclamar
|
| 235 |
+
dummy_y_for_keys = pd.Series([0, 1])
|
| 236 |
+
model_keys = train_and_evaluate_models(dummy_X_for_keys, dummy_X_for_keys, dummy_y_for_keys, dummy_y_for_keys,
|
| 237 |
+
StandardScaler()).keys()
|
| 238 |
+
st.session_state['is_initial_call'] = False # Reseta a flag após a chamada inicial
|
| 239 |
|
| 240 |
model_choice = st.sidebar.selectbox(
|
| 241 |
"Escolha o Modelo Principal para Análise Detalhada:",
|
|
|
|
| 280 |
|
| 281 |
with tab2:
|
| 282 |
st.header("2. Balanceamento de Dados com SMOTE")
|
| 283 |
+
st.write(
|
| 284 |
+
"A seguir, demonstramos o efeito do balanceamento da variável alvo 'Complain' utilizando a técnica **SMOTE**.")
|
| 285 |
|
| 286 |
X_display = X.copy()
|
| 287 |
y_display = y.copy()
|
|
|
|
| 305 |
ax.set_ylabel("Contagem")
|
| 306 |
st.pyplot(fig)
|
| 307 |
except Exception as e:
|
| 308 |
+
st.error(
|
| 309 |
+
f"Erro ao aplicar SMOTE: {e}. Isso pode acontecer se houver poucas amostras na classe minoritária ou muitas features.")
|
| 310 |
X_res, y_res = X_display, y_display
|
| 311 |
else:
|
| 312 |
st.info("SMOTE desabilitado. O balanceamento não será aplicado.")
|
|
|
|
| 319 |
st.subheader("Divisão dos Dados (Treino/Teste)")
|
| 320 |
test_size = st.slider("Tamanho do Conjunto de Teste", 0.1, 0.5, 0.3, 0.05)
|
| 321 |
if len(np.unique(y_res)) > 1:
|
| 322 |
+
X_train, X_test, y_train, y_test = train_test_split(X_res, y_res, test_size=test_size, random_state=42,
|
| 323 |
+
stratify=y_res)
|
| 324 |
else:
|
| 325 |
+
st.warning(
|
| 326 |
+
"Não foi possível usar `stratify` no `train_test_split` pois o alvo tem apenas uma classe após o processamento. Dividindo sem estratificação.")
|
| 327 |
X_train, X_test, y_train, y_test = train_test_split(X_res, y_res, test_size=test_size, random_state=42)
|
| 328 |
|
| 329 |
st.write(f"**Shape dos dados de treino:** {X_train.shape}")
|
|
|
|
| 345 |
st.stop()
|
| 346 |
|
| 347 |
st.subheader("Escalonamento de Dados")
|
| 348 |
+
st.write(
|
| 349 |
+
"Para modelos sensíveis à escala (como KNN e SVM), os dados serão automaticamente escalonados (`StandardScaler`) antes do treinamento e da previsão.")
|
| 350 |
|
| 351 |
with tab3:
|
| 352 |
st.header("3. Comparação de Modelos Supervisionados")
|
|
|
|
| 388 |
st.markdown("""
|
| 389 |
Para problemas de previsão de reclamações, o **Recall** é frequentemente crucial, pois minimiza Falsos Negativos (clientes que reclamam mas não são previstos). No entanto, um bom **AUC** (Área sob a Curva ROC) indica a capacidade geral do modelo de distinguir entre as classes, e o **F1-score** oferece um equilíbrio entre Precisão e Recall.
|
| 390 |
""")
|
| 391 |
+
st.success(
|
| 392 |
+
f"**Recomendação:** O modelo com o maior **AUC** é geralmente um bom ponto de partida, pois indica a melhor capacidade discriminatória geral. Para este exemplo, o modelo principal para análise detalhada será o selecionado na sidebar: **{model_choice}**.")
|
| 393 |
|
| 394 |
with tab4:
|
| 395 |
st.header("4. Análise Detalhada do Modelo Selecionado")
|
|
|
|
| 397 |
|
| 398 |
if st.button(f"Analisar {model_choice}"):
|
| 399 |
with st.spinner(f"Analisando {model_choice}..."):
|
| 400 |
+
selected_model_results = train_and_evaluate_models(X_train, X_test, y_train, y_test, StandardScaler(),
|
| 401 |
+
model_selected=model_choice)
|
| 402 |
+
|
| 403 |
if model_choice not in selected_model_results or selected_model_results[model_choice]['Model'] is None:
|
| 404 |
+
st.error(
|
| 405 |
+
f"Não foi possível analisar o modelo {model_choice}. Ele pode ter falhado no treinamento. Erro: {selected_model_results.get(model_choice, {}).get('Error', 'Desconhecido')}")
|
| 406 |
else:
|
| 407 |
metrics = selected_model_results[model_choice]
|
| 408 |
|
|
|
|
| 415 |
|
| 416 |
st.subheader(f"Matriz de Confusão para {model_choice}")
|
| 417 |
fig_cm, ax_cm = plt.subplots(figsize=(7, 6))
|
| 418 |
+
disp = ConfusionMatrixDisplay(confusion_matrix=metrics['Confusion Matrix'],
|
| 419 |
+
display_labels=['Não Reclamou (0)', 'Reclamou (1)'])
|
| 420 |
disp.plot(cmap=plt.cm.Blues, ax=ax_cm)
|
| 421 |
ax_cm.set_title(f'Matriz de Confusão para {model_choice}')
|
| 422 |
st.pyplot(fig_cm)
|
|
|
|
| 431 |
|
| 432 |
st.subheader(f"Curva ROC para {model_choice}")
|
| 433 |
fig_roc_single, ax_roc_single = plt.subplots(figsize=(8, 6))
|
| 434 |
+
ax_roc_single.plot(metrics['FPR'], metrics['TPR'], color='darkorange', lw=2,
|
| 435 |
+
label=f'Curva ROC (AUC = {metrics["AUC"]:.2f})')
|
| 436 |
ax_roc_single.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--', label='Classificador Aleatório')
|
| 437 |
ax_roc_single.set_xlabel('Taxa de Falsos Positivos (FPR)')
|
| 438 |
ax_roc_single.set_ylabel('Taxa de Verdadeiros Positivos (TPR)')
|
|
|
|
| 440 |
ax_roc_single.legend(loc='lower right')
|
| 441 |
ax_roc_single.grid(True)
|
| 442 |
st.pyplot(fig_roc_single)
|
| 443 |
+
st.write(
|
| 444 |
+
f"O **AUC** de {metrics['AUC']:.2f} indica a capacidade discriminatória do modelo: quanto mais próximo de 1, melhor o modelo distingue entre as classes.")
|
| 445 |
|
| 446 |
st.subheader("Sensibilidade aos Hiperparâmetros")
|
| 447 |
if model_choice == "K-Nearest Neighbors":
|
|
|
|
| 461 |
Modelos de Boosting como XGBoost e LightGBM são influenciados por `n_estimators` (número de estimadores), `learning_rate` (taxa de aprendizado) e `max_depth`. Uma `learning_rate` menor com mais estimadores pode melhorar o desempenho, mas requer mais tempo de treinamento. `Max_depth` controla a complexidade de cada árvore.
|
| 462 |
""")
|
| 463 |
else:
|
| 464 |
+
st.markdown(
|
| 465 |
+
"Este modelo também possui hiperparâmetros que podem ser ajustados para otimizar o desempenho (ex: `max_depth` para Decision Tree, `n_estimators` para AdaBoosting/Gradient Boosting).")
|
| 466 |
|
| 467 |
with tab5:
|
| 468 |
st.header("5. Tomada de Decisão e Aplicação Gerencial")
|
|
|
|
| 470 |
|
| 471 |
if st.button("Gerar Análise Gerencial"):
|
| 472 |
with st.spinner("Gerando insights gerenciais..."):
|
| 473 |
+
selected_model_results = train_and_evaluate_models(X_train, X_test, y_train, y_test, StandardScaler(),
|
| 474 |
+
model_selected=model_choice)
|
| 475 |
+
|
| 476 |
if model_choice not in selected_model_results or selected_model_results[model_choice]['Model'] is None:
|
| 477 |
+
st.error(
|
| 478 |
+
f"Não foi possível gerar a análise gerencial para o modelo {model_choice}. Ele pode ter falhado no treinamento. Erro: {selected_model_results.get(model_choice, {}).get('Error', 'Desconhecido')}")
|
| 479 |
else:
|
| 480 |
model_instance = selected_model_results[model_choice]["Model"]
|
| 481 |
|
|
|
|
| 484 |
if hasattr(model_instance, 'feature_importances_'):
|
| 485 |
feature_importances = model_instance.feature_importances_
|
| 486 |
feature_names = X.columns.tolist()
|
| 487 |
+
importance_df = pd.DataFrame(
|
| 488 |
+
{'Variável': feature_names, 'Importância Relativa': feature_importances})
|
| 489 |
importance_df = importance_df.sort_values(by='Importância Relativa', ascending=False)
|
| 490 |
st.dataframe(importance_df.head(10).set_index('Variável'))
|
| 491 |
|
|
|
|
| 497 |
elif hasattr(model_instance, 'coef_'):
|
| 498 |
st.info("Para modelos lineares, os coeficientes podem ser interpretados como importância.")
|
| 499 |
else:
|
| 500 |
+
st.info(
|
| 501 |
+
"Não foi possível extrair a importância das variáveis para este tipo de modelo de forma direta.")
|
| 502 |
|
| 503 |
st.subheader("Análise e Recomendações Gerenciais")
|
| 504 |
|