vsalgs commited on
Commit
8b9a604
·
verified ·
1 Parent(s): ef18c0a

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +526 -38
src/streamlit_app.py CHANGED
@@ -1,40 +1,528 @@
1
- import altair as alt
2
- import numpy as np
3
- import pandas as pd
4
  import streamlit as st
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
- """
7
- # Welcome to Streamlit!
8
-
9
- Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
- forums](https://discuss.streamlit.io).
12
-
13
- In the meantime, below is an example of what you can do with just a few lines of code:
14
- """
15
-
16
- num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
- num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
18
-
19
- indices = np.linspace(0, 1, num_points)
20
- theta = 2 * np.pi * num_turns * indices
21
- radius = indices
22
-
23
- x = radius * np.cos(theta)
24
- y = radius * np.sin(theta)
25
-
26
- df = pd.DataFrame({
27
- "x": x,
28
- "y": y,
29
- "idx": indices,
30
- "rand": np.random.randn(num_points),
31
- })
32
-
33
- st.altair_chart(alt.Chart(df, height=700, width=700)
34
- .mark_point(filled=True)
35
- .encode(
36
- x=alt.X("x", axis=None),
37
- y=alt.Y("y", axis=None),
38
- color=alt.Color("idx", legend=None, scale=alt.Scale()),
39
- size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
40
- ))
 
 
 
 
1
  import streamlit as st
2
+ import pandas as pd
3
+ from collections import Counter
4
+ from imblearn.over_sampling import SMOTE
5
+ from sklearn.model_selection import train_test_split
6
+ from sklearn.preprocessing import StandardScaler
7
+ from sklearn.neighbors import KNeighborsClassifier
8
+ from sklearn.svm import SVC
9
+ 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
18
+ 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
32
+ def load_data():
33
+ github_url = "https://raw.githubusercontent.com/Abdulraqib20/Customer-Personality-Analysis/refs/heads/main/marketing_campaign.csv"
34
+ try:
35
+ df = pd.read_csv(github_url, sep='\t')
36
+ except Exception as e:
37
+ st.error(f"Erro ao carregar o arquivo do GitHub: {e}")
38
+ st.stop()
39
+ return df
40
+
41
+
42
+ @st.cache_data
43
+ def preprocess_data(df):
44
+ df_processed = df.copy()
45
+
46
+ # Handle 'Dt_Customer' column
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):
88
+ models = {
89
+ "K-Nearest Neighbors": KNeighborsClassifier(),
90
+ "Support Vector Machine": SVC(probability=True, random_state=42),
91
+ "Decision Tree": DecisionTreeClassifier(random_state=42),
92
+ "Random Forest": RandomForestClassifier(random_state=42),
93
+ "AdaBoosting": AdaBoostClassifier(random_state=42),
94
+ "Gradient Boosting": GradientBoostingClassifier(random_state=42),
95
+ "XGBoosting": XGBClassifier(use_label_encoder=False, eval_metric='logloss', random_state=42),
96
+ "LightGBM": LGBMClassifier(random_state=42)
97
+ }
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
134
+
135
+ # Aplicar escalonamento apenas para os dados de treino e teste
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:
151
+ y_prob = probas[:, 1]
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:
159
+ roc_auc = roc_auc_score(y_test, y_prob)
160
+ fpr, tpr, _ = roc_curve(y_test, y_prob)
161
+ else:
162
+ roc_auc = 0.5
163
+ fpr, tpr = [0, 1], [0, 1]
164
+
165
+ conf_matrix = confusion_matrix(y_test, y_pred)
166
+
167
+ results[name] = {
168
+ "Model": model,
169
+ "Accuracy": accuracy_score(y_test, y_pred),
170
+ "Precision": precision_score(y_test, y_pred, zero_division=0),
171
+ "Recall": recall_score(y_test, y_pred, zero_division=0),
172
+ "F1-score": f1_score(y_test, y_pred, zero_division=0),
173
+ "AUC": roc_auc,
174
+ "Confusion Matrix": conf_matrix,
175
+ "FPR": fpr,
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)
196
+
197
+ X = df_processed.drop('Complain', axis=1)
198
+ y = df_processed['Complain']
199
+
200
+ # --- Sidebar para controle ---
201
+ st.sidebar.header("⚙️ Configurações do Modelo")
202
+
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)
224
+ selector_rfe = selector_rfe.fit(X, y)
225
+ rfe_selected_features_indices = selector_rfe.support_
226
+ X = X.loc[:, rfe_selected_features_indices]
227
+ st.sidebar.success(f"RFE aplicado. Selecionadas {X.shape[1]} features.")
228
+ except Exception as e:
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:",
249
+ list(model_keys)
250
+ )
251
+
252
+ st.sidebar.markdown("---")
253
+ 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:
265
+ st.header("1. Visão Geral dos Dados")
266
+ st.subheader("Primeiras 5 Linhas do Dataset")
267
+ st.dataframe(df.head())
268
+
269
+ st.subheader("Estatísticas Descritivas")
270
+ st.dataframe(df.describe())
271
+
272
+ st.subheader("Informações sobre as Colunas")
273
+ buffer = io.StringIO()
274
+ df.info(buf=buffer)
275
+ s = buffer.getvalue()
276
+ st.text(s)
277
+
278
+ st.subheader("Distribuição da Variável Alvo ('Complain') Original")
279
+ fig, ax = plt.subplots(figsize=(6, 4))
280
+ sns.countplot(x=y, ax=ax)
281
+ ax.set_title("Distribuição Original da Variável 'Complain'")
282
+ ax.set_xlabel("Reclamou (0: Não, 1: Sim)")
283
+ ax.set_ylabel("Contagem")
284
+ st.pyplot(fig)
285
+ st.write(f"Distribuição da variável 'Complain' original: {Counter(y)}")
286
+ st.warning("Observe o desbalanceamento da classe 'Complain' (poucas reclamações).")
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()
295
+
296
+ if balance_data:
297
+ st.subheader("Resultados do SMOTE")
298
+ smote = SMOTE(random_state=42)
299
+ try:
300
+ if len(np.unique(y_display)) < 2:
301
+ st.error("SMOTE não pode ser aplicado: A variável alvo contém apenas uma classe.")
302
+ X_res, y_res = X_display, y_display
303
+ else:
304
+ X_res, y_res = smote.fit_resample(X_display, y_display)
305
+ st.success("Dados balanceados com sucesso!")
306
+ st.write(f"'Complain' variable distribution after SMOTE balancing: {Counter(y_res)}")
307
+
308
+ fig, ax = plt.subplots(figsize=(6, 4))
309
+ sns.countplot(x=y_res, ax=ax)
310
+ ax.set_title("Distribuição da Variável 'Complain' Após SMOTE")
311
+ ax.set_xlabel("Reclamou (0: Não, 1: Sim)")
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.")
320
+ X_res, y_res = X_display, y_display
321
+
322
+ if X_res.empty or y_res.empty:
323
+ st.error("Erro: Os dados pós-balanceamento estão vazios. Verifique o dataset original e o pré-processamento.")
324
+ X_train, X_test, y_train, y_test = pd.DataFrame(), pd.DataFrame(), pd.Series(), pd.Series()
325
+ else:
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.")
349
+ st.stop()
350
+ if X_test.empty or y_test.empty:
351
+ st.error("Os dados de teste estão vazios! Verifique o carregamento ou pré-processamento dos dados.")
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")
360
+ st.write("Avalie o desempenho de diferentes grupos de modelos supervisionados utilizando métricas chave.")
361
+
362
+ if st.button("Treinar e Comparar Todos os Modelos"):
363
+ with st.spinner("Treinando e avaliando modelos..."):
364
+ all_results = train_and_evaluate_models(X_train, X_test, y_train, y_test, StandardScaler())
365
+
366
+ valid_results = {k: v for k, v in all_results.items() if v['Model'] is not None}
367
+
368
+ if not valid_results:
369
+ st.warning("Nenhum modelo pôde ser treinado com sucesso. Verifique seus dados e configurações.")
370
+ else:
371
+ st.subheader("Métricas de Desempenho dos Modelos")
372
+ metrics_df = pd.DataFrame({
373
+ "Modelo": list(valid_results.keys()),
374
+ "Accuracy": [res["Accuracy"] for res in valid_results.values()],
375
+ "Precision": [res["Precision"] for res in valid_results.values()],
376
+ "Recall": [res["Recall"] for res in valid_results.values()],
377
+ "F1-score": [res["F1-score"] for res in valid_results.values()],
378
+ "AUC": [res["AUC"] for res in valid_results.values()]
379
+ })
380
+ st.dataframe(metrics_df.set_index("Modelo").sort_values(by="AUC", ascending=False))
381
+
382
+ st.subheader("Curvas ROC de Todos os Modelos")
383
+ fig_roc_all, ax_roc_all = plt.subplots(figsize=(10, 8))
384
+ for name, metrics in valid_results.items():
385
+ ax_roc_all.plot(metrics['FPR'], metrics['TPR'], label=f'{name} (AUC = {metrics["AUC"]:.2f})')
386
+ ax_roc_all.plot([0, 1], [0, 1], 'k--', label='Aleatório (AUC = 0.50)')
387
+ ax_roc_all.set_xlabel('Taxa de Falsos Positivos (FPR)')
388
+ ax_roc_all.set_ylabel('Taxa de Verdadeiros Positivos (TPR)')
389
+ ax_roc_all.set_title('Curva ROC para Diferentes Modelos')
390
+ ax_roc_all.legend()
391
+ ax_roc_all.grid(True)
392
+ st.pyplot(fig_roc_all)
393
+
394
+ st.subheader("Discussão sobre a Escolha do Melhor Modelo")
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")
403
+ st.write(f"Foco na análise detalhada do modelo: **{model_choice}**.")
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
+
415
+ st.subheader(f"Métricas de Desempenho para {model_choice}")
416
+ st.write(f"**Accuracy:** {metrics['Accuracy']:.4f}")
417
+ st.write(f"**Precision:** {metrics['Precision']:.4f}")
418
+ st.write(f"**Recall:** {metrics['Recall']:.4f}")
419
+ st.write(f"**F1-score:** {metrics['F1-score']:.4f}")
420
+ st.write(f"**AUC:** {metrics['AUC']:.4f}")
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)
429
+
430
+ st.markdown("""
431
+ **Interpretação da Matriz de Confusão:**
432
+ - **Verdadeiros Negativos (TN):** Clientes que não reclamaram e foram previstos corretamente.
433
+ - **Falsos Positivos (FP):** Clientes que não reclamaram, mas foram erroneamente previstos como reclamantes (custo de intervenção desnecessária).
434
+ - **Falsos Negativos (FN):** Clientes que reclamaram, mas foram erroneamente previstos como não reclamantes (custo de perda de oportunidade de intervenção, insatisfação).
435
+ - **Verdadeiros Positivos (TP):** Clientes que reclamaram e foram previstos corretamente.
436
+ """)
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)')
445
+ ax_roc_single.set_title(f'Curva ROC para {model_choice}')
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":
454
+ st.markdown("""
455
+ O KNN é altamente sensível ao `n_neighbors` (número de vizinhos). Pequenos valores podem causar overfitting, enquanto valores grandes podem levar a underfitting. A métrica de distância e a escala dos dados também são cruciais.
456
+ """)
457
+ elif model_choice == "Random Forest":
458
+ st.markdown("""
459
+ O Random Forest é impactado por `n_estimators` (número de árvores), `max_depth` (profundidade máxima) e `min_samples_leaf`. Mais árvores geralmente melhoram o desempenho, mas `max_depth` e `min_samples_leaf` controlam a complexidade e evitam o overfitting das árvores individuais.
460
+ """)
461
+ elif model_choice == "Support Vector Machine":
462
+ st.markdown("""
463
+ O SVM é sensível ao `C` (parâmetro de regularização) e `kernel` (função de kernel). `C` controla a penalidade por erros de classificação, e o `kernel` define a forma do limite de decisão (linear, RBF, etc.). A escala dos dados é fundamental para o SVM.
464
+ """)
465
+ elif model_choice == "XGBoosting" or model_choice == "LightGBM":
466
+ st.markdown("""
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")
475
+ st.write("Análise dos fatores que mais influenciam a ocorrência de reclamações e recomendações práticas.")
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
+
488
+ st.subheader("Importância das Variáveis")
489
+
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
+
498
+ fig_imp, ax_imp = plt.subplots(figsize=(10, 6))
499
+ sns.barplot(x='Importância Relativa', y='Variável', data=importance_df.head(10), ax=ax_imp)
500
+ ax_imp.set_title('Top 10 Variáveis Mais Importantes')
501
+ st.pyplot(fig_imp)
502
+
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
+
511
+ st.markdown("""
512
+ Com base nas variáveis mais importantes, podemos formular estratégias proativas:
513
+
514
+ **Exemplo de Cenário e Recomendação (Ajuste com base nos resultados reais das suas variáveis importantes):**
515
+
516
+ Se, por exemplo, 'MntWines' (gasto com vinho), 'NumWebVisitsMonth' (visitas ao site) e 'Dt_Customer' (dias desde a última compra) forem as variáveis mais importantes:
517
+
518
+ * **Clientes com alto gasto em vinho (`MntWines`)** que apresentam **alta frequência de visitas ao site (`NumWebVisitsMonth`) mas baixo engajamento recente (`Dt_Customer` elevado)** podem estar enfrentando dificuldades para encontrar produtos, informações ou ter problemas não resolvidos.
519
+
520
+ **Recomendação Gerencial:**
521
+
522
+ Priorize esses clientes com **ações proativas de atendimento e retenção**. Por exemplo:
523
+ 1. **Suporte Proativo:** Monitore clientes com alto `NumWebVisitsMonth` que não resultam em compra ou que têm histórico de altos gastos e ofereça ajuda via chat ou contato telefônico personalizado.
524
+ 2. **Campanhas de Reengajamento:** Crie campanhas segmentadas para clientes com `Dt_Customer` elevado, oferecendo descontos em seus produtos preferidos (ex: vinhos) ou convidando-os a fornecer feedback sobre a experiência recente.
525
+ 3. **Melhoria na Experiência Online:** Analise as páginas mais visitadas por esses clientes com `NumWebVisitsMonth` alto para identificar gargalos ou informações ausentes que possam estar gerando frustração.
526
 
527
+ Ao antecipar e resolver proativamente as insatisfações, a empresa pode **melhorar a experiência do consumidor, reduzir as taxas de reclamação e aumentar a lealdade do cliente.**
528
+ """)