vsalgs commited on
Commit
93e9d20
·
verified ·
1 Parent(s): 36c91cf

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. 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, confusion_matrix, ConfusionMatrixDisplay
 
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("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.")
 
 
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) # Remove coluna original de data
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) # Preenche NaN com 0 após coerção, se houver
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, errors='ignore')
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] # Remove colunas com apenas 1 valor único
71
- df_processed = df_processed.dropna(axis=1, how='all') # Remove colunas totalmente nulas
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("Erro: O conjunto de treino contém apenas uma classe na variável alvo. Verifique o balanceamento ou a divisão dos dados.")
 
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(f"Erro: Coluna '{col}' no X_train_raw não é numérica. Tipo: {X_train_raw[col].dtype}. Verifique o pré-processamento.")
 
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(f"Erro: Coluna '{col}' no X_test_raw não é numérica. Tipo: {X_test_raw[col].dtype}. Verifique o pré-processamento.")
 
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: # Para outros modelos, usamos os dados crus (não escalados)
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 # fallback, não ideal para AUC
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], "y_prob": np.zeros(len(y_test)), "Error": str(e)
 
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("SMOTE cria amostras sintéticas da classe minoritária para balancear os dados, melhorando o desempenho em datasets desbalanceados.")
 
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, min(10, max_features_rfe))
197
- st.sidebar.info(f"O RFE (Recursive Feature Elimination) seleciona as {n_features_rfe} melhores variáveis de forma iterativa.")
 
 
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(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.")
 
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((1, X.shape[1])), columns=X.columns)
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, StandardScaler()).keys()
224
- st.session_state['is_initial_call'] = False # Reseta a flag após a chamada inicial
 
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("A seguir, demonstramos o efeito do balanceamento da variável alvo 'Complain' utilizando a técnica **SMOTE**.")
 
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(f"Erro ao aplicar SMOTE: {e}. Isso pode acontecer se houver poucas amostras na classe minoritária ou muitas features.")
 
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, stratify=y_res)
 
307
  else:
308
- st.warning("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.")
 
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("Para modelos sensíveis à escala (como KNN e SVM), os dados serão automaticamente escalonados (`StandardScaler`) antes do treinamento e da previsão.")
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(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}**.")
 
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(), model_selected=model_choice)
382
-
 
383
  if model_choice not in selected_model_results or selected_model_results[model_choice]['Model'] is None:
384
- st.error(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')}")
 
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'], display_labels=['Não Reclamou (0)', 'Reclamou (1)'])
 
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, label=f'Curva ROC (AUC = {metrics["AUC"]:.2f})')
 
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(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.")
 
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("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).")
 
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(), model_selected=model_choice)
449
-
 
450
  if model_choice not in selected_model_results or selected_model_results[model_choice]['Model'] is None:
451
- st.error(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')}")
 
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({'Variável': feature_names, 'Importância Relativa': feature_importances})
 
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("Não foi possível extrair a importância das variáveis para este tipo de modelo de forma direta.")
 
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