vsalgs commited on
Commit
36c91cf
·
verified ·
1 Parent(s): 80e6add

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +79 -114
src/streamlit_app.py CHANGED
@@ -10,8 +10,7 @@ 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
- confusion_matrix, ConfusionMatrixDisplay
15
  import matplotlib.pyplot as plt
16
  import seaborn as sns
17
  import numpy as np
@@ -19,13 +18,12 @@ import io
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,7 +36,6 @@ def load_data():
38
  st.stop()
39
  return df
40
 
41
-
42
  @st.cache_data
43
  def preprocess_data(df):
44
  df_processed = df.copy()
@@ -47,41 +44,34 @@ def preprocess_data(df):
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
- # Inclui colunas como Kidhome, Teenhome, AcceptedCmpX, Response que devem ser numéricas
54
  cols_to_coerce_numeric = [
55
  'Kidhome', 'Teenhome', 'Recency', 'MntWines', 'MntFruits', 'MntMeatProducts',
56
  'MntFishProducts', 'MntSweetProducts', 'MntGoldProds', 'NumDealsPurchases',
57
  'NumWebPurchases', 'NumCatalogPurchases', 'NumStorePurchases',
58
  'NumWebVisitsMonth', 'AcceptedCmp1', 'AcceptedCmp2', 'AcceptedCmp3',
59
  'AcceptedCmp4', 'AcceptedCmp5', 'Response', 'Days_Since_Customer', 'Income'
60
- # Adicionado Income aqui para garantir
61
  ]
62
  for col in cols_to_coerce_numeric:
63
  if col in df_processed.columns:
64
  df_processed[col] = pd.to_numeric(df_processed[col], errors='coerce')
65
- df_processed[col] = df_processed[col].fillna(0) # Preenche NaN com 0 após coerção, se houver
66
-
67
- # Lidar com valores ausentes: preencher 'Income' com a média (se ainda houver, após coerção)
68
- # df_processed['Income'] = df_processed['Income'].fillna(df_processed['Income'].mean()) # Removido, já tratado acima
69
 
70
  # Convertendo variáveis categóricas em numéricas (one-hot encoding)
71
  df_processed = pd.get_dummies(df_processed, columns=['Education', 'Marital_Status'], drop_first=True)
72
 
73
  # Excluir colunas irrelevantes e com variância zero
74
  cols_to_drop = ['ID', 'Z_CostContact', 'Z_Revenue']
75
- df_processed = df_processed.drop(columns=[col for col in cols_to_drop if col in df_processed.columns], axis=1,
76
- errors='ignore')
77
-
78
  # Remover colunas com variância zero (constantes) ou com muitos nulos após o pré-processamento
79
- df_processed = df_processed.loc[:, df_processed.nunique() > 1] # Remove colunas com apenas 1 valor único
80
- df_processed = df_processed.dropna(axis=1, how='all') # Remove colunas totalmente nulas
81
 
82
  return df_processed
83
 
84
-
85
  # Função para treinar e avaliar modelos
86
  @st.cache_data(show_spinner=False)
87
  def train_and_evaluate_models(X_train_raw, X_test_raw, y_train, y_test, _scaler, model_selected=None):
@@ -98,36 +88,33 @@ def train_and_evaluate_models(X_train_raw, X_test_raw, y_train, y_test, _scaler,
98
 
99
  results = {}
100
 
101
- # Check if y_train has at least two classes before attempting to train
 
 
 
 
 
102
  if len(np.unique(y_train)) < 2:
103
- if st.session_state.get('is_initial_call', False):
104
- return {name: {} for name in models.keys()}
105
- else:
106
- st.error(
107
- "Erro: O conjunto de treino contém apenas uma classe na variável alvo. Verifique o balanceamento ou a divisão dos dados.")
108
- return {}
109
 
110
- # Check if X_train_raw has enough samples
111
  if X_train_raw.shape[0] == 0:
112
- if st.session_state.get('is_initial_call', False):
113
- return {name: {} for name in models.keys()}
114
- else:
115
- st.error("Erro: Dados de treino com 0 amostras. Não é possível treinar modelos.")
116
- return {}
117
-
118
- # Verificar se os dtypes são numéricos antes de treinar
119
  for col in X_train_raw.columns:
120
  if not pd.api.types.is_numeric_dtype(X_train_raw[col]):
121
- st.error(
122
- f"Erro: Coluna '{col}' no X_train_raw não é numérica. Tipo: {X_train_raw[col].dtype}. Verifique o pré-processamento.")
123
  return {}
124
 
125
  for col in X_test_raw.columns:
126
  if not pd.api.types.is_numeric_dtype(X_test_raw[col]):
127
- st.error(
128
- f"Erro: Coluna '{col}' no X_test_raw não é numérica. Tipo: {X_test_raw[col].dtype}. Verifique o pré-processamento.")
129
  return {}
130
 
 
131
  for name, model in models.items():
132
  if model_selected and name != model_selected:
133
  continue
@@ -136,15 +123,15 @@ def train_and_evaluate_models(X_train_raw, X_test_raw, y_train, y_test, _scaler,
136
  if name in ["K-Nearest Neighbors", "Support Vector Machine"]:
137
  X_train_processed = _scaler.fit_transform(X_train_raw)
138
  X_test_processed = _scaler.transform(X_test_raw)
139
- else: # Para outros modelos, usamos os dados crus (não escalados)
140
  X_train_processed = X_train_raw
141
  X_test_processed = X_test_raw
142
 
143
  try:
144
  model.fit(X_train_processed, y_train)
145
  y_pred = model.predict(X_test_processed)
146
-
147
- # === CORREÇÃO PARA IndexError no predict_proba ===
148
  if hasattr(model, 'predict_proba'):
149
  probas = model.predict_proba(X_test_processed)
150
  if probas.shape[1] > 1:
@@ -152,7 +139,7 @@ def train_and_evaluate_models(X_train_raw, X_test_raw, y_train, y_test, _scaler,
152
  else:
153
  y_prob = probas[:, 0]
154
  else:
155
- y_prob = y_pred # fallback, não ideal para AUC
156
 
157
  # Calcular ROC AUC apenas se y_prob não for totalmente binário (0 ou 1)
158
  if len(np.unique(y_prob)) > 1:
@@ -176,20 +163,16 @@ def train_and_evaluate_models(X_train_raw, X_test_raw, y_train, y_test, _scaler,
176
  "TPR": tpr,
177
  "y_prob": y_prob
178
  }
179
- except ValueError as e:
180
- if not st.session_state.get('is_initial_call', False):
181
- st.warning(
182
- f"Não foi possível treinar o modelo {name} devido a um erro: {e}. Provavelmente dados de teste/treino insuficientes ou de apenas uma classe.")
183
- # Se for chamada inicial (dummy), não mostra nada no front
184
  results[name] = {
185
  "Model": None, "Accuracy": 0, "Precision": 0, "Recall": 0, "F1-score": 0,
186
- "AUC": 0.5, "Confusion Matrix": np.array([[0, 0], [0, 0]]), "FPR": [0, 1], "TPR": [0, 1],
187
- "y_prob": np.zeros(len(y_test))
188
  }
189
  continue
190
  return results
191
 
192
-
193
  # --- Carregar e Pré-processar os dados ---
194
  df = load_data()
195
  df_processed = preprocess_data(df)
@@ -203,21 +186,17 @@ st.sidebar.header("⚙️ Configurações do Modelo")
203
  # Balanceamento da Base
204
  st.sidebar.subheader("Balanceamento de Dados (SMOTE)")
205
  balance_data = st.sidebar.checkbox("Aplicar SMOTE", value=True)
206
- st.sidebar.info(
207
- "SMOTE cria amostras sintéticas da classe minoritária para balancear os dados, melhorando o desempenho em datasets desbalanceados.")
208
 
209
  # Seleção de Variáveis
210
  st.sidebar.subheader("Seleção de Variáveis")
211
  use_rfe = st.sidebar.checkbox("Usar Seleção de Variáveis (RFE)", value=False)
212
  if use_rfe:
213
- # Garante que X tem colunas suficientes para o slider
214
  max_features_rfe = X.shape[1] if X.shape[1] > 5 else 5
215
- n_features_rfe = st.sidebar.slider("Número de Variáveis a Selecionar (RFE)", 5, max_features_rfe,
216
- min(10, max_features_rfe))
217
- st.sidebar.info(
218
- f"O RFE (Recursive Feature Elimination) seleciona as {n_features_rfe} melhores variáveis de forma iterativa.")
219
  estimator_rfe = LogisticRegression(max_iter=1000, random_state=42)
220
-
221
  if X.shape[0] > 0 and X.shape[1] >= n_features_rfe:
222
  try:
223
  selector_rfe = RFE(estimator_rfe, n_features_to_select=n_features_rfe, step=1)
@@ -229,20 +208,20 @@ if use_rfe:
229
  st.sidebar.error(f"Erro ao aplicar RFE: {e}. RFE desabilitado.")
230
  use_rfe = False
231
  else:
232
- st.sidebar.warning(
233
- 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.")
234
  use_rfe = False
235
 
236
  # Escolha do Modelo
237
  st.sidebar.subheader("Seleção de Modelo para Treinamento")
238
 
239
- # === CORREÇÃO: Passar dados dummy robustos para a chamada inicial do selectbox ===
240
  st.session_state['is_initial_call'] = True
 
241
  dummy_X_for_keys = pd.DataFrame(np.zeros((1, X.shape[1])), columns=X.columns)
242
- dummy_y_for_keys = pd.Series([0, 1])
243
- model_keys = train_and_evaluate_models(dummy_X_for_keys, dummy_X_for_keys, dummy_y_for_keys, dummy_y_for_keys,
244
- StandardScaler()).keys()
245
- st.session_state['is_initial_call'] = False
246
 
247
  model_choice = st.sidebar.selectbox(
248
  "Escolha o Modelo Principal para Análise Detalhada:",
@@ -254,11 +233,11 @@ st.sidebar.markdown("Desenvolvido por seu AI Assistant")
254
 
255
  # --- Abas do Dashboard ---
256
  tab1, tab2, tab3, tab4, tab5 = st.tabs([
257
- "1. Visão Geral dos Dados",
258
- "2. Balanceamento de Dados",
259
- "3. Comparação de Modelos",
260
- "4. Análise do Melhor Modelo",
261
- "5. Aplicação Gerencial"
262
  ])
263
 
264
  with tab1:
@@ -287,8 +266,7 @@ with tab1:
287
 
288
  with tab2:
289
  st.header("2. Balanceamento de Dados com SMOTE")
290
- st.write(
291
- "A seguir, demonstramos o efeito do balanceamento da variável alvo 'Complain' utilizando a técnica **SMOTE**.")
292
 
293
  X_display = X.copy()
294
  y_display = y.copy()
@@ -312,8 +290,7 @@ with tab2:
312
  ax.set_ylabel("Contagem")
313
  st.pyplot(fig)
314
  except Exception as e:
315
- st.error(
316
- f"Erro ao aplicar SMOTE: {e}. Isso pode acontecer se houver poucas amostras na classe minoritária ou muitas features.")
317
  X_res, y_res = X_display, y_display
318
  else:
319
  st.info("SMOTE desabilitado. O balanceamento não será aplicado.")
@@ -326,23 +303,21 @@ with tab2:
326
  st.subheader("Divisão dos Dados (Treino/Teste)")
327
  test_size = st.slider("Tamanho do Conjunto de Teste", 0.1, 0.5, 0.3, 0.05)
328
  if len(np.unique(y_res)) > 1:
329
- X_train, X_test, y_train, y_test = train_test_split(X_res, y_res, test_size=test_size, random_state=42,
330
- stratify=y_res)
331
  else:
332
- st.warning(
333
- "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.")
334
  X_train, X_test, y_train, y_test = train_test_split(X_res, y_res, test_size=test_size, random_state=42)
335
 
336
- # --- Mensagens de depuração movidas para cá, se necessário ---
337
- st.write("Shape X_train:", X_train.shape)
338
- st.write("Shape X_test:", X_test.shape)
339
- st.write("Shape y_train:", y_train.shape)
340
- st.write("Shape y_test:", y_test.shape)
341
- st.write("Shape do DataFrame (após pré-processamento):", df_processed.shape)
342
- st.write("Tipos das colunas (após pré-processamento):", df_processed.dtypes)
343
- st.write("Primeiras 5 linhas (após pré-processamento):", df_processed.head())
344
- st.write("Classes em y_train:", np.unique(y_train))
345
- # --- FIM NOVO ---
346
 
347
  if X_train.empty or y_train.empty:
348
  st.error("Os dados de treino estão vazios! Verifique o carregamento ou pré-processamento dos dados.")
@@ -352,8 +327,8 @@ with tab2:
352
  st.stop()
353
 
354
  st.subheader("Escalonamento de Dados")
355
- st.write(
356
- "Para modelos sensíveis à escala (como KNN e SVM), os dados serão automaticamente escalonados (`StandardScaler`) antes do treinamento e da previsão.")
357
 
358
  with tab3:
359
  st.header("3. Comparação de Modelos Supervisionados")
@@ -395,8 +370,7 @@ with tab3:
395
  st.markdown("""
396
  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.
397
  """)
398
- st.success(
399
- 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}**.")
400
 
401
  with tab4:
402
  st.header("4. Análise Detalhada do Modelo Selecionado")
@@ -404,11 +378,10 @@ with tab4:
404
 
405
  if st.button(f"Analisar {model_choice}"):
406
  with st.spinner(f"Analisando {model_choice}..."):
407
- selected_model_results = train_and_evaluate_models(X_train, X_test, y_train, y_test, StandardScaler(),
408
- model_selected=model_choice)
409
-
410
  if model_choice not in selected_model_results or selected_model_results[model_choice]['Model'] is None:
411
- st.error(f"Não foi possível analisar o modelo {model_choice}. Ele pode ter falhado no treinamento.")
412
  else:
413
  metrics = selected_model_results[model_choice]
414
 
@@ -421,8 +394,7 @@ with tab4:
421
 
422
  st.subheader(f"Matriz de Confusão para {model_choice}")
423
  fig_cm, ax_cm = plt.subplots(figsize=(7, 6))
424
- disp = ConfusionMatrixDisplay(confusion_matrix=metrics['Confusion Matrix'],
425
- display_labels=['Não Reclamou (0)', 'Reclamou (1)'])
426
  disp.plot(cmap=plt.cm.Blues, ax=ax_cm)
427
  ax_cm.set_title(f'Matriz de Confusão para {model_choice}')
428
  st.pyplot(fig_cm)
@@ -437,8 +409,7 @@ with tab4:
437
 
438
  st.subheader(f"Curva ROC para {model_choice}")
439
  fig_roc_single, ax_roc_single = plt.subplots(figsize=(8, 6))
440
- ax_roc_single.plot(metrics['FPR'], metrics['TPR'], color='darkorange', lw=2,
441
- label=f'Curva ROC (AUC = {metrics["AUC"]:.2f})')
442
  ax_roc_single.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--', label='Classificador Aleatório')
443
  ax_roc_single.set_xlabel('Taxa de Falsos Positivos (FPR)')
444
  ax_roc_single.set_ylabel('Taxa de Verdadeiros Positivos (TPR)')
@@ -446,8 +417,7 @@ with tab4:
446
  ax_roc_single.legend(loc='lower right')
447
  ax_roc_single.grid(True)
448
  st.pyplot(fig_roc_single)
449
- st.write(
450
- 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.")
451
 
452
  st.subheader("Sensibilidade aos Hiperparâmetros")
453
  if model_choice == "K-Nearest Neighbors":
@@ -467,8 +437,7 @@ with tab4:
467
  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.
468
  """)
469
  else:
470
- st.markdown(
471
- "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).")
472
 
473
  with tab5:
474
  st.header("5. Tomada de Decisão e Aplicação Gerencial")
@@ -476,12 +445,10 @@ with tab5:
476
 
477
  if st.button("Gerar Análise Gerencial"):
478
  with st.spinner("Gerando insights gerenciais..."):
479
- selected_model_results = train_and_evaluate_models(X_train, X_test, y_train, y_test, StandardScaler(),
480
- model_selected=model_choice)
481
-
482
  if model_choice not in selected_model_results or selected_model_results[model_choice]['Model'] is None:
483
- st.error(
484
- f"Não foi possível gerar a análise gerencial para o modelo {model_choice}. Ele pode ter falhado no treinamento.")
485
  else:
486
  model_instance = selected_model_results[model_choice]["Model"]
487
 
@@ -490,8 +457,7 @@ with tab5:
490
  if hasattr(model_instance, 'feature_importances_'):
491
  feature_importances = model_instance.feature_importances_
492
  feature_names = X.columns.tolist()
493
- importance_df = pd.DataFrame(
494
- {'Variável': feature_names, 'Importância Relativa': feature_importances})
495
  importance_df = importance_df.sort_values(by='Importância Relativa', ascending=False)
496
  st.dataframe(importance_df.head(10).set_index('Variável'))
497
 
@@ -503,8 +469,7 @@ with tab5:
503
  elif hasattr(model_instance, 'coef_'):
504
  st.info("Para modelos lineares, os coeficientes podem ser interpretados como importância.")
505
  else:
506
- st.info(
507
- "Não foi possível extrair a importância das variáveis para este tipo de modelo de forma direta.")
508
 
509
  st.subheader("Análise e Recomendações Gerenciais")
510
 
 
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
  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
  st.stop()
37
  return df
38
 
 
39
  @st.cache_data
40
  def preprocess_data(df):
41
  df_processed = df.copy()
 
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 = [
51
  'Kidhome', 'Teenhome', 'Recency', 'MntWines', 'MntFruits', 'MntMeatProducts',
52
  'MntFishProducts', 'MntSweetProducts', 'MntGoldProds', 'NumDealsPurchases',
53
  'NumWebPurchases', 'NumCatalogPurchases', 'NumStorePurchases',
54
  'NumWebVisitsMonth', 'AcceptedCmp1', 'AcceptedCmp2', 'AcceptedCmp3',
55
  'AcceptedCmp4', 'AcceptedCmp5', 'Response', 'Days_Since_Customer', 'Income'
 
56
  ]
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):
 
88
 
89
  results = {}
90
 
91
+ # --- NOVO: Se for uma chamada inicial, apenas retorna as chaves sem tentar treinar ---
92
+ if st.session_state.get('is_initial_call', False):
93
+ return {name: {} for name in models.keys()}
94
+ # --- FIM NOVO ---
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
  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)
137
  if probas.shape[1] > 1:
 
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:
 
163
  "TPR": tpr,
164
  "y_prob": y_prob
165
  }
166
+ except Exception as e:
167
+ # Captura erros de treinamento, mas não os exibe para o usuário final durante a seleção inicial
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
  # 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
  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
215
  st.sidebar.subheader("Seleção de Modelo para Treinamento")
216
 
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:",
 
233
 
234
  # --- Abas do Dashboard ---
235
  tab1, tab2, tab3, tab4, tab5 = st.tabs([
236
+ "Visão Geral dos Dados",
237
+ "Balanceamento de Dados",
238
+ "Comparação de Modelos",
239
+ "Análise do Melhor Modelo",
240
+ "Aplicação Gerencial"
241
  ])
242
 
243
  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
  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
  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}")
312
+ st.write(f"**Shape dos dados de teste:** {X_test.shape}")
313
+ st.write(f"**Shape y_train:** {y_train.shape}")
314
+ st.write(f"**Shape y_test:** {y_test.shape}")
315
+ st.write(f"**Shape do DataFrame (após pré-processamento):** {df_processed.shape}")
316
+ st.write(f"**Tipos das colunas (após pré-processamento):**")
317
+ st.dataframe(df_processed.dtypes.astype(str).reset_index().rename(columns={'index': 'Coluna', 0: 'Tipo de Dado'}))
318
+ st.write(f"**Primeiras 5 linhas (após pré-processamento):**")
319
+ st.dataframe(df_processed.head())
320
+ st.write(f"**Classes em y_train:** {np.unique(y_train)}")
321
 
322
  if X_train.empty or y_train.empty:
323
  st.error("Os dados de treino estão vazios! Verifique o carregamento ou pré-processamento dos dados.")
 
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
  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
 
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
 
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
 
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
  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
  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
 
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
  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
  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