brunaaaz commited on
Commit
5f48d79
·
verified ·
1 Parent(s): aa0b13f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +656 -315
app.py CHANGED
@@ -1,357 +1,698 @@
 
1
  import streamlit as st
2
  import pandas as pd
3
  import numpy as np
4
  import matplotlib.pyplot as plt
5
  import seaborn as sns
6
- from sklearn.model_selection import train_test_split, cross_val_score
7
- from sklearn.preprocessing import StandardScaler
 
8
  from sklearn.linear_model import LogisticRegression
9
  from sklearn.neighbors import KNeighborsClassifier
10
  from sklearn.svm import SVC
11
- from sklearn.metrics import roc_curve, auc, classification_report, confusion_matrix, precision_recall_curve
12
- import os
 
 
 
13
  import warnings
14
  warnings.filterwarnings('ignore')
15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  # Configuração da página
17
  st.set_page_config(
18
- page_title="Previsão de Cancelamentos Hoteleiros",
19
  page_icon="🏨",
20
- layout="wide"
 
21
  )
22
 
23
- # Título principal
24
- st.title("🏨 Dashboard de Previsão de Cancelamentos em Reservas Hoteleiras")
25
  st.markdown("""
26
- **Objetivo**: Desenvolver e comparar modelos preditivos para identificar reservas com maior probabilidade de cancelamento,
27
- permitindo ações preventivas como overbooking controlado e ofertas promocionais direcionadas.
28
- """)
29
-
30
- # Carregar dados
31
- @st.cache_data
32
- def load_data():
33
- """
34
- Carrega dados sintéticos (evita problemas de compatibilidade)
35
- """
36
- np.random.seed(42)
37
- n_samples = 4000
38
-
39
- # Criar dados mais realistas baseados no dataset real
40
- data = {
41
- 'hotel': np.random.choice(['Resort Hotel', 'City Hotel'], n_samples, p=[0.6, 0.4]),
42
- 'is_canceled': np.random.choice([0, 1], n_samples, p=[0.65, 0.35]),
43
- 'lead_time': np.random.gamma(2, 50, n_samples).astype(int),
44
- 'arrival_date_year': np.random.choice([2015, 2016, 2017], n_samples, p=[0.3, 0.4, 0.3]),
45
- 'arrival_date_month': np.random.choice([
46
- 'January', 'February', 'March', 'April', 'May', 'June',
47
- 'July', 'August', 'September', 'October', 'November', 'December'
48
- ], n_samples),
49
- 'stays_in_weekend_nights': np.random.poisson(1, n_samples),
50
- 'stays_in_week_nights': np.random.poisson(3, n_samples),
51
- 'adults': np.random.choice([1, 2, 3, 4], n_samples, p=[0.1, 0.7, 0.15, 0.05]),
52
- 'children': np.random.choice([0, 1, 2], n_samples, p=[0.8, 0.15, 0.05]),
53
- 'babies': np.random.choice([0, 1], n_samples, p=[0.95, 0.05]),
54
- 'meal': np.random.choice(['BB', 'HB', 'FB', 'SC'], n_samples, p=[0.7, 0.2, 0.05, 0.05]),
55
- 'country': np.random.choice(['PRT', 'GBR', 'FRA', 'ESP', 'DEU', 'ITA', 'IRL', 'BEL'], n_samples),
56
- 'market_segment': np.random.choice([
57
- 'Direct', 'Corporate', 'Online TA', 'Offline TA/TO', 'Complementary', 'Groups'
58
- ], n_samples, p=[0.2, 0.1, 0.5, 0.15, 0.02, 0.03]),
59
- 'distribution_channel': np.random.choice(['Direct', 'Corporate', 'TA/TO'], n_samples, p=[0.2, 0.1, 0.7]),
60
- 'is_repeated_guest': np.random.choice([0, 1], n_samples, p=[0.95, 0.05]),
61
- 'previous_cancellations': np.random.poisson(0.1, n_samples),
62
- 'previous_bookings_not_canceled': np.random.poisson(0.5, n_samples),
63
- 'reserved_room_type': np.random.choice(['A', 'B', 'C', 'D', 'E', 'F', 'G'], n_samples, p=[0.4, 0.2, 0.15, 0.1, 0.08, 0.05, 0.02]),
64
- 'assigned_room_type': np.random.choice(['A', 'B', 'C', 'D', 'E', 'F', 'G'], n_samples, p=[0.4, 0.2, 0.15, 0.1, 0.08, 0.05, 0.02]),
65
- 'booking_changes': np.random.poisson(0.3, n_samples),
66
- 'deposit_type': np.random.choice(['No Deposit', 'Non Refund', 'Refundable'], n_samples, p=[0.85, 0.1, 0.05]),
67
- 'customer_type': np.random.choice(['Transient', 'Contract', 'Transient-Party', 'Group'], n_samples, p=[0.7, 0.1, 0.15, 0.05]),
68
- 'adr': np.random.gamma(5, 20, n_samples) + 50, # ADR entre 50-250
69
- 'required_car_parking_spaces': np.random.choice([0, 1], n_samples, p=[0.9, 0.1]),
70
- 'total_of_special_requests': np.random.poisson(0.5, n_samples),
71
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
 
73
- df = pd.DataFrame(data)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
 
75
- # Ajustar lead_time para ser mais realista
76
- df['lead_time'] = np.clip(df['lead_time'], 0, 400)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
 
78
- # Ajustar ADR para ter valores mais realistas
79
- df['adr'] = np.clip(df['adr'], 50, 300)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
 
81
- return df
82
-
83
- df = load_data()
84
-
85
- # Sidebar para configurações
86
- st.sidebar.header("⚙️ Configurações do Modelo")
87
-
88
- # Seleção do algoritmo
89
- algorithm = st.sidebar.selectbox(
90
- "Selecione o algoritmo:",
91
- ["Regressão Logística", "K-Nearest Neighbors", "Support Vector Machine"]
92
- )
93
-
94
- # Parâmetros específicos por algoritmo
95
- if algorithm == "Regressão Logística":
96
- st.sidebar.subheader("Parâmetros da Regressão Logística")
97
- penalty = st.sidebar.selectbox("Penalidade", ["l1", "l2", "none"])
98
- C = st.sidebar.slider("Parâmetro C (Regularização)", 0.01, 10.0, 1.0, 0.1)
99
- solver = st.sidebar.selectbox("Solver", ["liblinear", "lbfgs"])
100
 
101
- elif algorithm == "K-Nearest Neighbors":
102
- st.sidebar.subheader("Parâmetros do KNN")
103
- k = st.sidebar.slider("Número de vizinhos (k)", 1, 15, 5)
104
- metric = st.sidebar.selectbox("Métrica de distância", ["euclidean", "manhattan"])
105
- weights = st.sidebar.selectbox("Pesos", ["uniform", "distance"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
 
107
- elif algorithm == "Support Vector Machine":
108
- st.sidebar.subheader("Parâmetros do SVM")
109
- kernel = st.sidebar.selectbox("Kernel", ["linear", "rbf", "poly"])
110
- C_svm = st.sidebar.slider("Parâmetro C (SVM)", 0.01, 10.0, 1.0, 0.1)
111
- gamma = st.sidebar.selectbox("Gamma", ["scale", "auto"])
112
-
113
- # Configurações gerais
114
- st.sidebar.subheader("Configurações Gerais")
115
- test_size = st.sidebar.slider("Tamanho do conjunto de teste", 0.1, 0.5, 0.2, 0.05)
116
- cross_validation = st.sidebar.slider("Número de folds para validação cruzada", 2, 5, 3)
117
-
118
- # Análise exploratória
119
- st.header("📊 Análise Exploratória dos Dados")
120
-
121
- col1, col2 = st.columns(2)
122
-
123
- with col1:
124
- st.subheader("Distribuição de Cancelamentos")
125
- fig, ax = plt.subplots(figsize=(6, 4))
126
- df['is_canceled'].value_counts().plot(kind='bar', ax=ax, color=['skyblue', 'salmon'])
127
- ax.set_title('Distribuição de Cancelamentos')
128
- ax.set_xlabel('Cancelado')
129
- ax.set_ylabel('Contagem')
130
- st.pyplot(fig)
131
-
132
- with col2:
133
- st.subheader("Lead Time vs Cancelamentos")
134
- fig, ax = plt.subplots(figsize=(6, 4))
135
- sns.boxplot(data=df, x='is_canceled', y='lead_time', ax=ax)
136
- ax.set_title('Lead Time por Status de Cancelamento')
137
- ax.set_xlabel('Cancelado')
138
- ax.set_ylabel('Lead Time (dias)')
139
- st.pyplot(fig)
140
-
141
- # Informações do dataset
142
- st.sidebar.header("📊 Informações do Dataset")
143
- st.sidebar.write(f"**Registros**: {len(df):,}")
144
- st.sidebar.write(f"**Cancelamentos**: {df['is_canceled'].sum():,} ({df['is_canceled'].mean():.1%})")
145
- st.sidebar.write(f"**Variáveis**: {len(df.columns)}")
146
-
147
- # Pré-processamento dos dados
148
- st.header("🔧 Pré-processamento dos Dados")
149
-
150
- # Preparar dados para modelagem
151
- X = df.drop('is_canceled', axis=1)
152
- y = df['is_canceled']
153
-
154
- # Codificar variáveis categóricas
155
- X_encoded = pd.get_dummies(X, drop_first=True)
156
-
157
- # Dividir dados
158
- X_train, X_test, y_train, y_test = train_test_split(
159
- X_encoded, y, test_size=test_size, random_state=42, stratify=y
160
- )
161
-
162
- # Normalizar dados
163
- scaler = StandardScaler()
164
- X_train_scaled = scaler.fit_transform(X_train)
165
- X_test_scaled = scaler.transform(X_test)
166
-
167
- st.success(f"✅ Dados pré-processados: {X_train.shape[0]} amostras de treino, {X_test.shape[0]} amostras de teste")
168
-
169
- # Treinamento do modelo
170
- st.header("🤖 Treinamento do Modelo")
171
-
172
- def train_and_evaluate_model(algorithm, X_train, X_test, y_train, y_test, params):
173
- if algorithm == "Regressão Logística":
174
- model = LogisticRegression(
175
- penalty=params.get('penalty', 'l2'),
176
- C=params.get('C', 1.0),
177
- solver=params.get('solver', 'liblinear'),
178
- random_state=42,
179
- max_iter=1000
180
- )
181
- elif algorithm == "K-Nearest Neighbors":
182
- model = KNeighborsClassifier(
183
- n_neighbors=params.get('k', 5),
184
- metric=params.get('metric', 'euclidean'),
185
- weights=params.get('weights', 'uniform')
186
- )
187
- elif algorithm == "Support Vector Machine":
188
- model = SVC(
189
- kernel=params.get('kernel', 'rbf'),
190
- C=params.get('C_svm', 1.0),
191
- gamma=params.get('gamma', 'scale'),
192
- probability=True,
193
- random_state=42
194
- )
195
 
196
- # Treinar modelo
197
- model.fit(X_train, y_train)
 
 
 
 
 
 
198
 
199
- # Previsões
200
- y_pred = model.predict(X_test)
201
- y_pred_proba = model.predict_proba(X_test)[:, 1]
 
 
 
 
 
202
 
203
- return model, y_pred, y_pred_proba
204
-
205
- # Coletar parâmetros
206
- params = {}
207
- if algorithm == "Regressão Logística":
208
- params = {'penalty': penalty, 'C': C, 'solver': solver}
209
- elif algorithm == "K-Nearest Neighbors":
210
- params = {'k': k, 'metric': metric, 'weights': weights}
211
- elif algorithm == "Support Vector Machine":
212
- params = {'kernel': kernel, 'C_svm': C_svm, 'gamma': gamma}
213
-
214
- # Treinar modelo
215
- with st.spinner(f"Treinando modelo {algorithm}..."):
216
- try:
217
- model, y_pred, y_pred_proba = train_and_evaluate_model(
218
- algorithm, X_train_scaled, X_test_scaled, y_train, y_test, params
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
  )
220
 
221
- # Validação cruzada
222
- cv_scores = cross_val_score(model, X_train_scaled, y_train, cv=cross_validation, scoring='accuracy')
223
- st.info(f"📊 Acurácia média na validação cruzada ({cross_validation} folds): {cv_scores.mean():.3f} (± {cv_scores.std():.3f})")
224
-
225
- # Métricas de avaliação
226
- st.header("📈 Avaliação do Modelo")
227
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  col1, col2 = st.columns(2)
229
-
230
  with col1:
231
- # Matriz de confusão
232
- st.subheader("Matriz de Confusão")
233
- cm = confusion_matrix(y_test, y_pred)
234
- fig, ax = plt.subplots(figsize=(5, 4))
235
- sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', ax=ax)
236
- ax.set_xlabel('Predito')
237
- ax.set_ylabel('Real')
238
- ax.set_title('Matriz de Confusão')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
  st.pyplot(fig)
240
-
241
  with col2:
242
- # Relatório de classificação
243
- st.subheader("Métricas Principais")
244
- report = classification_report(y_test, y_pred, output_dict=True)
245
- metrics_df = pd.DataFrame({
246
- 'Métrica': ['Acurácia', 'Precisão', 'Recall', 'F1-Score'],
247
  'Valor': [
248
- report['accuracy'],
249
- report['1']['precision'],
250
- report['1']['recall'],
251
- report['1']['f1-score']
252
  ]
253
  })
254
- st.dataframe(metrics_df.style.format({"Valor": "{:.3f}"}))
255
-
256
- # Curva ROC
257
- st.subheader("Curva ROC")
258
- fpr, tpr, thresholds = roc_curve(y_test, y_pred_proba)
259
- roc_auc = auc(fpr, tpr)
260
-
261
- fig, ax = plt.subplots(figsize=(8, 6))
262
- ax.plot(fpr, tpr, color='darkorange', lw=2, label=f'Curva ROC (AUC = {roc_auc:.3f})')
263
- ax.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--', label='Aleatório')
264
- ax.set_xlim([0.0, 1.0])
265
- ax.set_ylim([0.0, 1.05])
266
- ax.set_xlabel('Taxa de Falsos Positivos')
267
- ax.set_ylabel('Taxa de Verdadeiros Positivos')
268
- ax.set_title('Curva ROC')
269
- ax.legend(loc="lower right")
270
- st.pyplot(fig)
271
-
272
- # Comparação de modelos
273
- st.header("🏆 Comparação de Modelos")
274
-
275
- # Simular resultados para comparação
276
- models_comparison = {
277
- 'Modelo': [algorithm, 'K-Nearest Neighbors', 'Support Vector Machine'],
278
- 'AUC': [roc_auc, 0.78, 0.82],
279
- 'Acurácia': [cv_scores.mean(), 0.75, 0.80],
280
- 'Precisão': [report['1']['precision'], 0.72, 0.78],
281
- 'Recall': [report['1']['recall'], 0.68, 0.75],
282
- 'F1-Score': [report['1']['f1-score'], 0.70, 0.76]
283
- }
284
-
285
- comparison_df = pd.DataFrame(models_comparison)
286
- st.dataframe(comparison_df.style.format("{:.3f}").highlight_max(axis=0))
287
-
288
- # Ranking do melhor modelo
289
- best_model_idx = comparison_df['AUC'].idxmax()
290
- best_model = comparison_df.loc[best_model_idx, 'Modelo']
291
- best_auc = comparison_df.loc[best_model_idx, 'AUC']
292
-
293
- st.success(f"🎯 **Melhor modelo**: {best_model} (AUC: {best_auc:.3f})")
294
-
295
- # Recomendações práticas
296
- st.header("💡 Recomendações Práticas")
297
-
298
- st.markdown("""
299
- **Com base na análise realizada, recomenda-se:**
300
-
301
- 1. **Segmentação de Clientes**: Focar em reservas com lead time > 100 dias
302
- 2. **Política de Overbooking**: Aplicar overbooking de 3-5% para reservas de alto risco
303
- 3. **Ações Preventivas**: Oferecer upgrades para reservas identificadas como risco médio-alto
304
- 4. **Comunicação Proativa**: Contato com clientes de alto risco 48h antes do check-in
305
-
306
- **Fatores de risco identificados:**
307
- - Lead time elevado (> 100 dias)
308
- - Histórico de cancelamentos anteriores
309
- - Tipo de depósito não reembolsável
310
- - Canal de distribuição Online TA
311
- """)
312
-
313
- # Seção de previsão individual
314
- st.header("🎯 Previsão Individual")
315
-
316
  col1, col2 = st.columns(2)
317
-
318
  with col1:
319
- lead_time = st.slider("Lead Time (dias)", 0, 400, 30, key="lead_time")
320
- adults = st.slider("Número de Adultos", 1, 4, 2, key="adults")
321
- previous_cancellations = st.slider("Cancelamentos Anteriores", 0, 5, 0, key="prev_cancels")
322
-
323
  with col2:
324
- deposit_type = st.selectbox("Tipo de Depósito", ["No Deposit", "Non Refund", "Refundable"], key="deposit")
325
- market_segment = st.selectbox("Segmento de Mercado", ["Direct", "Corporate", "Online TA", "Offline TA/TO"], key="market")
326
- customer_type = st.selectbox("Tipo de Cliente", ["Transient", "Contract", "Transient-Party", "Group"], key="customer")
327
-
328
- if st.button("Prever Probabilidade de Cancelamento"):
329
- # Simular predição baseada nas entradas
330
- risk_factors = 0
331
- if lead_time > 100:
332
- risk_factors += 1
333
- if previous_cancellations > 0:
334
- risk_factors += 1
335
- if deposit_type == "No Deposit":
336
- risk_factors += 1
337
- if market_segment == "Online TA":
338
- risk_factors += 1
339
-
340
- probability = min(0.95, 0.2 + (risk_factors * 0.2))
341
-
342
- st.info(f"📊 Probabilidade estimada de cancelamento: {probability:.2f}")
343
-
344
- if probability > 0.6:
345
- st.warning("⚠️ Reserva de ALTO RISCO - Recomenda-se ação preventiva imediata")
346
- elif probability > 0.4:
347
- st.warning("⚠️ Reserva de risco MODERADO - Monitorar e contatar proativamente")
348
  else:
349
- st.success(" Reserva de BAIXO RISCO - Manter acompanhamento padrão")
350
-
351
- except Exception as e:
352
- st.error(f" Erro no treinamento do modelo: {str(e)}")
353
- st.info("💡 Tente ajustar os parâmetros do modelo ou reduzir a complexidade")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
354
 
355
- # Rodapé
356
- st.markdown("---")
357
- st.markdown("**Dashboard desenvolvido para análise preditiva de cancelamentos hoteleiros | Hugging Face Spaces**")
 
1
+ # app.py - Dashboard Interativo com Dataset do Hugging Face
2
  import streamlit as st
3
  import pandas as pd
4
  import numpy as np
5
  import matplotlib.pyplot as plt
6
  import seaborn as sns
7
+ import joblib
8
+ from sklearn.metrics import (accuracy_score, precision_score, recall_score, f1_score,
9
+ roc_auc_score, roc_curve, confusion_matrix)
10
  from sklearn.linear_model import LogisticRegression
11
  from sklearn.neighbors import KNeighborsClassifier
12
  from sklearn.svm import SVC
13
+ from sklearn.model_selection import train_test_split
14
+ from sklearn.preprocessing import StandardScaler, LabelEncoder
15
+ import plotly.graph_objects as go
16
+ import plotly.express as px
17
+ import time
18
  import warnings
19
  warnings.filterwarnings('ignore')
20
 
21
+ # Tentar importar SMOTE, mas continuar funcionando mesmo se falhar
22
+ try:
23
+ from imblearn.over_sampling import SMOTE
24
+ SMOTE_AVAILABLE = True
25
+ except ImportError as e:
26
+ st.warning(f"⚠️ SMOTE não disponível: {e}. Continuando sem balanceamento automático.")
27
+ SMOTE_AVAILABLE = False
28
+
29
+ # Tentar importar a biblioteca datasets do Hugging Face
30
+ try:
31
+ from datasets import load_dataset
32
+ DATASETS_AVAILABLE = True
33
+ except ImportError as e:
34
+ st.error(f"❌ Biblioteca 'datasets' não disponível: {e}")
35
+ DATASETS_AVAILABLE = False
36
+
37
  # Configuração da página
38
  st.set_page_config(
39
+ page_title="Dashboard - Cancelamento de Reservas",
40
  page_icon="🏨",
41
+ layout="wide",
42
+ initial_sidebar_state="expanded"
43
  )
44
 
45
+ # CSS customizado
 
46
  st.markdown("""
47
+ <style>
48
+ .main-header {
49
+ font-size: 2.5rem;
50
+ color: #1f77b4;
51
+ text-align: center;
52
+ margin-bottom: 2rem;
53
+ }
54
+ .metric-card {
55
+ background-color: #f0f2f6;
56
+ padding: 1rem;
57
+ border-radius: 10px;
58
+ border-left: 4px solid #1f77b4;
59
+ margin: 0.5rem 0;
60
+ }
61
+ .best-model {
62
+ background-color: #d4edda;
63
+ border-left: 4px solid #28a745;
64
+ padding: 1rem;
65
+ border-radius: 10px;
66
+ margin: 1rem 0;
67
+ }
68
+ .parameter-section {
69
+ background-color: #e8f4f8;
70
+ padding: 1rem;
71
+ border-radius: 10px;
72
+ margin: 1rem 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  }
74
+ .data-source-section {
75
+ background-color: #e7f3ff;
76
+ padding: 2rem;
77
+ border-radius: 10px;
78
+ border: 2px solid #2196F3;
79
+ text-align: center;
80
+ margin: 2rem 0;
81
+ }
82
+ .upload-section {
83
+ background-color: #fff3cd;
84
+ padding: 2rem;
85
+ border-radius: 10px;
86
+ border: 2px dashed #ffc107;
87
+ text-align: center;
88
+ margin: 2rem 0;
89
+ }
90
+ </style>
91
+ """, unsafe_allow_html=True)
92
+
93
+ class HotelBookingDashboard:
94
+ def __init__(self):
95
+ self.models = {}
96
+ self.results = {}
97
+ self.X_train = None
98
+ self.X_test = None
99
+ self.y_train = None
100
+ self.y_test = None
101
+ self.scaler = StandardScaler()
102
+ self.is_data_loaded = False
103
 
104
+ def load_huggingface_dataset(self):
105
+ """Carrega o dataset do Hugging Face"""
106
+ try:
107
+ with st.spinner("🌐 Carregando dataset do Hugging Face..."):
108
+ # Carregar o dataset
109
+ dataset = load_dataset("SIEP/hotel_bookings")
110
+
111
+ # Converter para pandas DataFrame
112
+ if 'train' in dataset:
113
+ df = dataset['train'].to_pandas()
114
+ else:
115
+ # Se não tiver split 'train', pega o primeiro split disponível
116
+ first_split = list(dataset.keys())[0]
117
+ df = dataset[first_split].to_pandas()
118
+
119
+ st.success(f"✅ Dataset carregado: {df.shape[0]} linhas × {df.shape[1]} colunas")
120
+ return df
121
+
122
+ except Exception as e:
123
+ st.error(f"❌ Erro ao carregar dataset do Hugging Face: {str(e)}")
124
+ return None
125
 
126
+ def load_and_preprocess_data(self, df):
127
+ """Carrega e pré-processa o dataset"""
128
+ try:
129
+ st.info("🔄 Iniciando pré-processamento dos dados...")
130
+
131
+ # Fazer uma cópia do dataframe
132
+ df_clean = df.copy()
133
+
134
+ # 1. Identificar a coluna target
135
+ target_col = self._identify_target_column(df_clean)
136
+ if not target_col:
137
+ st.error("❌ Não foi possível identificar a coluna target. Procure por colunas como 'is_canceled', 'canceled', etc.")
138
+ return False
139
+
140
+ st.success(f"✅ Coluna target identificada: '{target_col}'")
141
+
142
+ # 2. Tratamento de valores missing
143
+ df_clean = self._handle_missing_values(df_clean)
144
+
145
+ # 3. Codificar variáveis categóricas
146
+ df_encoded = self._encode_categorical_variables(df_clean)
147
+
148
+ # 4. Separar features e target
149
+ X = df_encoded.drop(columns=[target_col])
150
+ y = df_encoded[target_col]
151
+
152
+ # 5. Dividir e balancear dados
153
+ success = self._split_and_balance_data(X, y)
154
+
155
+ if success:
156
+ self.is_data_loaded = True
157
+ st.success("✅ Dados carregados e pré-processados com sucesso!")
158
+ return True
159
+ else:
160
+ return False
161
+
162
+ except Exception as e:
163
+ st.error(f"❌ Erro no pré-processamento: {str(e)}")
164
+ return False
165
 
166
+ def _identify_target_column(self, df):
167
+ """Identifica a coluna target automaticamente"""
168
+ target_candidates = ['is_canceled', 'canceled', 'cancelled', 'is_cancelled', 'booking_status']
169
+
170
+ for candidate in target_candidates:
171
+ if candidate in df.columns:
172
+ # Se encontrou, renomear para padronizar
173
+ if candidate != 'is_canceled':
174
+ df.rename(columns={candidate: 'is_canceled'}, inplace=True)
175
+ return 'is_canceled'
176
+
177
+ # Se não encontrou, verificar colunas binárias
178
+ binary_cols = []
179
+ for col in df.columns:
180
+ if df[col].dtype in ['int64', 'float64'] and df[col].nunique() == 2:
181
+ binary_cols.append(col)
182
+
183
+ if binary_cols:
184
+ st.warning(f"🔍 Colunas binárias encontradas: {binary_cols}")
185
+ return binary_cols[0]
186
+
187
+ return None
188
 
189
+ def _handle_missing_values(self, df):
190
+ """Trata valores missing seguindo as boas práticas"""
191
+ df_clean = df.copy()
192
+
193
+ # Remover coluna company se existir (muitos NAs)
194
+ if 'company' in df_clean.columns:
195
+ df_clean.drop('company', axis=1, inplace=True)
196
+
197
+ # Preencher outros missing values
198
+ for col in df_clean.columns:
199
+ if df_clean[col].isnull().sum() > 0:
200
+ if df_clean[col].dtype == 'object':
201
+ # Preencher com moda para categóricas
202
+ df_clean[col].fillna(df_clean[col].mode()[0], inplace=True)
203
+ else:
204
+ # Preencher com mediana para numéricas
205
+ df_clean[col].fillna(df_clean[col].median(), inplace=True)
206
+
207
+ return df_clean
208
 
209
+ def _encode_categorical_variables(self, df):
210
+ """Codifica variáveis categóricas"""
211
+ df_encoded = df.copy()
212
+
213
+ # Identificar colunas categóricas
214
+ categorical_cols = df_encoded.select_dtypes(include=['object']).columns.tolist()
215
+
216
+ if categorical_cols:
217
+ st.info(f"📊 Codificando {len(categorical_cols)} variáveis categóricas...")
218
+
219
+ # Label Encoding para alta cardinalidade (>20 categorias)
220
+ high_cardinality = [col for col in categorical_cols if df_encoded[col].nunique() > 20]
221
+ low_cardinality = [col for col in categorical_cols if df_encoded[col].nunique() <= 20]
222
+
223
+ for col in high_cardinality:
224
+ le = LabelEncoder()
225
+ df_encoded[col] = le.fit_transform(df_encoded[col].astype(str))
226
+
227
+ # One-Hot Encoding para baixa cardinalidade
228
+ if low_cardinality:
229
+ df_encoded = pd.get_dummies(df_encoded, columns=low_cardinality, drop_first=True)
230
+
231
+ return df_encoded
232
 
233
+ def _split_and_balance_data(self, X, y):
234
+ """Divide e balanceia os dados"""
235
+ try:
236
+ # Converter todas as colunas para numérico
237
+ X = X.apply(pd.to_numeric, errors='coerce').fillna(0)
238
+
239
+ # Dividir dados
240
+ X_train, X_test, y_train, y_test = train_test_split(
241
+ X, y, test_size=0.3, random_state=42, stratify=y
242
+ )
243
+
244
+ # Aplicar SMOTE se disponível e necessário
245
+ if (SMOTE_AVAILABLE and
246
+ y_train.value_counts().min() / y_train.value_counts().max() < 0.3):
247
+ smote = SMOTE(random_state=42)
248
+ X_train, y_train = smote.fit_resample(X_train, y_train)
249
+ st.info("✅ SMOTE aplicado para balanceamento dos dados")
250
+ elif not SMOTE_AVAILABLE:
251
+ st.warning("⚠️ SMOTE não disponível. Usando dados originais (pode haver desbalanceamento).")
252
+ else:
253
+ st.info("ℹ️ Dados já balanceados, SMOTE não aplicado.")
254
+
255
+ # Escalonar features
256
+ X_train_scaled = self.scaler.fit_transform(X_train)
257
+ X_test_scaled = self.scaler.transform(X_test)
258
+
259
+ self.X_train = X_train_scaled
260
+ self.X_test = X_test_scaled
261
+ self.y_train = y_train
262
+ self.y_test = y_test
263
+
264
+ st.success(f"✅ Dados divididos: Treino {X_train_scaled.shape}, Teste {X_test_scaled.shape}")
265
+ return True
266
+
267
+ except Exception as e:
268
+ st.error(f" Erro ao dividir dados: {str(e)}")
269
+ return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
 
271
+ def train_logistic_regression(self, C=1.0, penalty='l2', solver='lbfgs'):
272
+ """Treina Regressão Logística"""
273
+ model = LogisticRegression(C=C, penalty=penalty, solver=solver,
274
+ max_iter=1000, random_state=42)
275
+ start_time = time.time()
276
+ model.fit(self.X_train, self.y_train)
277
+ training_time = time.time() - start_time
278
+ return model, training_time
279
 
280
+ def train_knn(self, n_neighbors=5, metric='euclidean', weights='uniform'):
281
+ """Treina KNN"""
282
+ model = KNeighborsClassifier(n_neighbors=n_neighbors, metric=metric,
283
+ weights=weights)
284
+ start_time = time.time()
285
+ model.fit(self.X_train, self.y_train)
286
+ training_time = time.time() - start_time
287
+ return model, training_time
288
 
289
+ def train_svm(self, C=1.0, kernel='rbf', gamma='scale'):
290
+ """Treina SVM"""
291
+ model = SVC(C=C, kernel=kernel, gamma=gamma, probability=True,
292
+ random_state=42)
293
+ start_time = time.time()
294
+ model.fit(self.X_train, self.y_train)
295
+ training_time = time.time() - start_time
296
+ return model, training_time
297
+
298
+ def evaluate_model(self, model, model_name, training_time):
299
+ """Avalia modelo e retorna métricas"""
300
+ y_pred = model.predict(self.X_test)
301
+ y_proba = model.predict_proba(self.X_test)[:, 1]
302
+
303
+ metrics = {
304
+ 'Acurácia': accuracy_score(self.y_test, y_pred),
305
+ 'Precisão': precision_score(self.y_test, y_pred, zero_division=0),
306
+ 'Recall': recall_score(self.y_test, y_pred, zero_division=0),
307
+ 'F1-Score': f1_score(self.y_test, y_pred, zero_division=0),
308
+ 'AUC-ROC': roc_auc_score(self.y_test, y_proba),
309
+ 'Tempo Treino (s)': training_time
310
+ }
311
+
312
+ # Curva ROC
313
+ fpr, tpr, _ = roc_curve(self.y_test, y_proba)
314
+ roc_data = {'fpr': fpr, 'tpr': tpr, 'auc': metrics['AUC-ROC']}
315
+
316
+ # Matriz de confusão
317
+ cm = confusion_matrix(self.y_test, y_pred)
318
+
319
+ return metrics, roc_data, cm
320
+
321
+ def plot_roc_comparison(self, current_roc, current_model_name):
322
+ """Plota comparação de curvas ROC"""
323
+ fig = go.Figure()
324
+
325
+ # Curva do modelo atual
326
+ fig.add_trace(go.Scatter(
327
+ x=current_roc['fpr'], y=current_roc['tpr'],
328
+ mode='lines', name=f'{current_model_name} (AUC = {current_roc["auc"]:.3f})',
329
+ line=dict(width=3, color='red')
330
+ ))
331
+
332
+ # Curvas dos outros modelos
333
+ colors = ['blue', 'green', 'orange', 'purple']
334
+ for i, (model_name, model) in enumerate(self.models.items()):
335
+ if model_name != current_model_name:
336
+ try:
337
+ y_proba = model.predict_proba(self.X_test)[:, 1]
338
+ fpr, tpr, _ = roc_curve(self.y_test, y_proba)
339
+ auc = roc_auc_score(self.y_test, y_proba)
340
+
341
+ fig.add_trace(go.Scatter(
342
+ x=fpr, y=tpr, mode='lines',
343
+ name=f'{model_name} (AUC = {auc:.3f})',
344
+ line=dict(width=2, color=colors[i % len(colors)], dash='dash')
345
+ ))
346
+ except:
347
+ continue
348
+
349
+ # Linha de referência
350
+ fig.add_trace(go.Scatter(
351
+ x=[0, 1], y=[0, 1], mode='lines',
352
+ name='Classificador Aleatório', line=dict(dash='dash', color='grey')
353
+ ))
354
+
355
+ fig.update_layout(
356
+ title='Comparação das Curvas ROC',
357
+ xaxis_title='Taxa de Falsos Positivos',
358
+ yaxis_title='Taxa de Verdadeiros Positivos',
359
+ width=600, height=500
360
  )
361
 
362
+ return fig
 
 
 
 
 
363
 
364
+ def main():
365
+ # Header principal
366
+ st.markdown('<h1 class="main-header">🏨 Dashboard - Cancelamento de Reservas</h1>',
367
+ unsafe_allow_html=True)
368
+
369
+ # Inicializar dashboard
370
+ dashboard = HotelBookingDashboard()
371
+
372
+ # ===== SEÇÃO DE CARREGAMENTO DE DADOS =====
373
+ if not dashboard.is_data_loaded:
374
+ st.markdown("""
375
+ <div class="data-source-section">
376
+ <h2>📊 Escolha a Fonte dos Dados</h2>
377
+ <p style="font-size: 1.2rem; margin-bottom: 1.5rem;">
378
+ <strong>Carregue os dados do Hugging Face ou faça upload do seu próprio dataset</strong>
379
+ </p>
380
+ </div>
381
+ """, unsafe_allow_html=True)
382
+
383
+ # Opções de carregamento
384
  col1, col2 = st.columns(2)
385
+
386
  with col1:
387
+ st.markdown("### 🌐 Dataset do Hugging Face")
388
+ st.markdown("""
389
+ **Vantagens:**
390
+ - Dataset pré-processado
391
+ - Estrutura consistente
392
+ - Sem necessidade de upload
393
+ """)
394
+
395
+ if DATASETS_AVAILABLE:
396
+ if st.button("🚀 Carregar do Hugging Face", type="primary", use_container_width=True):
397
+ with st.spinner("Carregando dataset SIEP/hotel_bookings..."):
398
+ df = dashboard.load_huggingface_dataset()
399
+ if df is not None:
400
+ success = dashboard.load_and_preprocess_data(df)
401
+ if success:
402
+ st.session_state.data_processed = True
403
+ st.session_state.dashboard = dashboard
404
+ st.rerun()
405
+ else:
406
+ st.error("Biblioteca 'datasets' não disponível")
407
+ st.info("Adicione 'datasets' ao requirements.txt")
408
+
409
+ with col2:
410
+ st.markdown("### 📁 Upload Manual")
411
+ st.markdown("""
412
+ **Use seu próprio dataset:**
413
+ - Formato CSV
414
+ - Coluna target: 'is_canceled'
415
+ - Estrutura personalizada
416
+ """)
417
+
418
+ uploaded_file = st.file_uploader(
419
+ "Selecione o arquivo CSV",
420
+ type=['csv'],
421
+ help="Faça upload do dataset de reservas de hotel"
422
+ )
423
+
424
+ if uploaded_file is not None:
425
+ try:
426
+ with st.spinner("Carregando arquivo..."):
427
+ df = pd.read_csv(uploaded_file)
428
+ st.success(f"✅ Dataset carregado: {df.shape[0]} linhas × {df.shape[1]} colunas")
429
+
430
+ # Preview
431
+ with st.expander("👀 Visualizar Dataset"):
432
+ st.dataframe(df.head(10))
433
+
434
+ if st.button("🔄 Processar Dataset", type="primary", use_container_width=True):
435
+ success = dashboard.load_and_preprocess_data(df)
436
+ if success:
437
+ st.session_state.data_processed = True
438
+ st.session_state.dashboard = dashboard
439
+ st.rerun()
440
+
441
+ except Exception as e:
442
+ st.error(f"❌ Erro ao carregar arquivo: {str(e)}")
443
+
444
+ # Informações sobre o dataset
445
+ with st.expander("📋 Sobre o Dataset", expanded=True):
446
+ st.markdown("""
447
+ **Dataset: Hotel Bookings (SIEP/hotel_bookings)**
448
+
449
+ Este dataset contém informações de reservas de hotel incluindo:
450
+
451
+ **Variáveis Principais:**
452
+ - `is_canceled`: Indicador de cancelamento (target)
453
+ - `lead_time`: Tempo entre reserva e chegada
454
+ - `adr`: Taxa diária média
455
+ - `adults`, `children`, `babies`: Número de hóspedes
456
+ - `country`, `market_segment`: Informações demográficas
457
+ - `previous_cancellations`: Histórico de cancelamentos
458
+ - `booking_changes`: Número de mudanças na reserva
459
+
460
+ **Objetivo:** Prever se uma reserva será cancelada com base nas características da reserva.
461
+ """)
462
+
463
+ return
464
+
465
+ # ===== SEÇÃO PRINCIPAL (quando dados estão carregados) =====
466
+
467
+ # Recuperar o dashboard do session_state se necessário
468
+ if 'dashboard' in st.session_state:
469
+ dashboard = st.session_state.dashboard
470
+
471
+ # Sidebar - Configurações do Modelo
472
+ st.sidebar.header("⚙️ Configurações do Modelo")
473
+
474
+ # Seleção do algoritmo
475
+ algorithm = st.sidebar.selectbox(
476
+ "Escolha o algoritmo:",
477
+ ["Regressão Logística", "KNN", "SVM"],
478
+ index=0
479
+ )
480
+
481
+ # Parâmetros específicos
482
+ st.sidebar.subheader("📊 Parâmetros do Modelo")
483
+
484
+ if algorithm == "Regressão Logística":
485
+ st.sidebar.markdown('<div class="parameter-section">', unsafe_allow_html=True)
486
+ C_lr = st.sidebar.slider("Parâmetro C (Regularização)", 0.01, 10.0, 1.0, 0.01)
487
+ penalty = st.sidebar.selectbox("Tipo de Penalidade", ["l2", "l1"])
488
+ solver = st.sidebar.selectbox("Algoritmo", ["lbfgs", "liblinear", "saga"])
489
+ st.sidebar.markdown('</div>', unsafe_allow_html=True)
490
+
491
+ elif algorithm == "KNN":
492
+ st.sidebar.markdown('<div class="parameter-section">', unsafe_allow_html=True)
493
+ n_neighbors = st.sidebar.slider("Número de Vizinhos (k)", 1, 50, 5)
494
+ metric = st.sidebar.selectbox("Métrica de Distância",
495
+ ["euclidean", "manhattan", "minkowski"])
496
+ weights = st.sidebar.selectbox("Pesos", ["uniform", "distance"])
497
+ st.sidebar.markdown('</div>', unsafe_allow_html=True)
498
+
499
+ else: # SVM
500
+ st.sidebar.markdown('<div class="parameter-section">', unsafe_allow_html=True)
501
+ C_svm = st.sidebar.slider("Parâmetro C", 0.01, 10.0, 1.0, 0.01)
502
+ kernel = st.sidebar.selectbox("Kernel", ["rbf", "linear", "poly", "sigmoid"])
503
+ gamma = st.sidebar.selectbox("Gamma", ["scale", "auto"])
504
+ st.sidebar.markdown('</div>', unsafe_allow_html=True)
505
+
506
+ # Botão de treinamento
507
+ train_button = st.sidebar.button("🚀 Treinar Modelo", type="primary", use_container_width=True)
508
+
509
+ # Informações na sidebar
510
+ st.sidebar.markdown("---")
511
+ st.sidebar.info("""
512
+ **📊 Status do Dataset:**
513
+ - ✅ Dados carregados
514
+ - 📈 Pronto para treinamento
515
+ """)
516
+
517
+ st.sidebar.markdown("---")
518
+ if st.sidebar.button("🔄 Carregar Novo Dataset", use_container_width=True):
519
+ st.session_state.clear()
520
+ st.rerun()
521
+
522
+ # Conteúdo principal - Status dos dados
523
+ st.subheader("📈 Status dos Dados Carregados")
524
+
525
+ col1, col2, col3, col4 = st.columns(4)
526
+ with col1:
527
+ st.metric("Amostras de Treino", f"{dashboard.X_train.shape[0]:,}")
528
+ with col2:
529
+ st.metric("Amostras de Teste", f"{dashboard.X_test.shape[0]:,}")
530
+ with col3:
531
+ st.metric("Features", f"{dashboard.X_train.shape[1]}")
532
+ with col4:
533
+ balance = pd.Series(dashboard.y_train).value_counts()
534
+ if len(balance) == 2:
535
+ st.metric("Balanceamento", f"{balance[0]}:{balance[1]}")
536
+ else:
537
+ st.metric("Classes", len(balance))
538
+
539
+ # Análise exploratória
540
+ with st.expander("🔍 Análise Exploratória dos Dados"):
541
+ col1, col2 = st.columns(2)
542
+
543
+ with col1:
544
+ # Distribuição do target
545
+ fig, ax = plt.subplots(figsize=(8, 6))
546
+ balance = pd.Series(dashboard.y_train).value_counts()
547
+ ax.pie(balance.values, labels=['Não Cancelado', 'Cancelado'], autopct='%1.1f%%', startangle=90)
548
+ ax.set_title('Distribuição de Cancelamentos')
549
  st.pyplot(fig)
550
+
551
  with col2:
552
+ # Estatísticas básicas
553
+ st.write("**Estatísticas do Dataset:**")
554
+ stats_df = pd.DataFrame({
555
+ 'Métrica': ['Total de Amostras', 'Features', 'Taxa de Cancelamento', 'Balanceamento'],
 
556
  'Valor': [
557
+ f"{dashboard.X_train.shape[0] + dashboard.X_test.shape[0]:,}",
558
+ f"{dashboard.X_train.shape[1]}",
559
+ f"{(dashboard.y_train.sum() + dashboard.y_test.sum()) / (len(dashboard.y_train) + len(dashboard.y_test)) * 100:.1f}%",
560
+ f"{balance[0]}:{balance[1]}" if len(balance) == 2 else "Múltiplas classes"
561
  ]
562
  })
563
+ st.dataframe(stats_df, hide_index=True)
564
+
565
+ # Conteúdo principal - Resultados do Modelo
566
+ if train_button:
567
+ with st.spinner(f"Treinando modelo {algorithm}..."):
568
+ # Treinar modelo
569
+ if algorithm == "Regressão Logística":
570
+ model, training_time = dashboard.train_logistic_regression(
571
+ C=C_lr, penalty=penalty, solver=solver
572
+ )
573
+ model_name = f"RL_C={C_lr}"
574
+
575
+ elif algorithm == "KNN":
576
+ model, training_time = dashboard.train_knn(
577
+ n_neighbors=n_neighbors, metric=metric, weights=weights
578
+ )
579
+ model_name = f"KNN_k={n_neighbors}_{metric}"
580
+
581
+ else: # SVM
582
+ model, training_time = dashboard.train_svm(
583
+ C=C_svm, kernel=kernel, gamma=gamma
584
+ )
585
+ model_name = f"SVM_{kernel}_C={C_svm}"
586
+
587
+ # Avaliar
588
+ metrics, roc_data, cm = dashboard.evaluate_model(model, model_name, training_time)
589
+
590
+ # Salvar modelo
591
+ dashboard.models[model_name] = model
592
+ dashboard.results[model_name] = metrics
593
+
594
+ # Resultados
595
+ st.success(f"✅ Modelo {algorithm} treinado com sucesso em {training_time:.2f} segundos!")
596
+
597
+ # Métricas
598
+ st.subheader("📊 Métricas de Desempenho")
599
+ col1, col2, col3, col4, col5 = st.columns(5)
600
+ with col1: st.metric("Acurácia", f"{metrics['Acurácia']:.4f}")
601
+ with col2: st.metric("Precisão", f"{metrics['Precisão']:.4f}")
602
+ with col3: st.metric("Recall", f"{metrics['Recall']:.4f}")
603
+ with col4: st.metric("F1-Score", f"{metrics['F1-Score']:.4f}")
604
+ with col5: st.metric("AUC-ROC", f"{metrics['AUC-ROC']:.4f}")
605
+
606
+ # Visualizações
607
+ st.subheader("📈 Visualizações")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
608
  col1, col2 = st.columns(2)
609
+
610
  with col1:
611
+ # Curva ROC
612
+ roc_fig = dashboard.plot_roc_comparison(roc_data, model_name)
613
+ st.plotly_chart(roc_fig, use_container_width=True)
614
+
615
  with col2:
616
+ # Matriz de confusão
617
+ fig_cm, ax = plt.subplots(figsize=(6, 4))
618
+ sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', ax=ax)
619
+ ax.set_xlabel('Predito')
620
+ ax.set_ylabel('Verdadeiro')
621
+ ax.set_title('Matriz de Confusão')
622
+ st.pyplot(fig_cm)
623
+
624
+ # Análise
625
+ st.subheader("🔍 Análise e Interpretação")
626
+ col1, col2 = st.columns(2)
627
+
628
+ with col1:
629
+ st.markdown("### 📋 Avaliação do Desempenho")
630
+ if metrics['F1-Score'] >= 0.7:
631
+ st.success("**🎯 Excelente desempenho!** Modelo bem balanceado entre precisão e recall.")
632
+ elif metrics['F1-Score'] >= 0.5:
633
+ st.info("**👍 Bom desempenho!** Resultados satisfatórios para aplicação prática.")
 
 
 
 
 
 
634
  else:
635
+ st.warning("**⚠️ Desempenho moderado.** Considere ajustar parâmetros ou features.")
636
+
637
+ if metrics['AUC-ROC'] >= 0.8:
638
+ st.success("**🔝 Ótima discriminação!** O modelo separa muito bem as classes.")
639
+ elif metrics['AUC-ROC'] >= 0.7:
640
+ st.info("**📈 Boa discriminação!** Separação adequada entre cancelamentos e não-cancelamentos.")
641
+ else:
642
+ st.warning("**📉 Discriminação moderada.** Há espaço para melhorias na separação das classes.")
643
+
644
+ with col2:
645
+ st.markdown("### 💡 Recomendações Práticas")
646
+ recommendations = []
647
+
648
+ if metrics['Precisão'] < 0.6:
649
+ recommendations.append("**Aumente o threshold** para reduzir falsos positivos")
650
+ if metrics['Recall'] < 0.6:
651
+ recommendations.append("**Diminua o threshold** para capturar mais cancelamentos reais")
652
+ if algorithm == "KNN" and n_neighbors < 5:
653
+ recommendations.append("**Aumente o valor de k** para reduzir overfitting")
654
+ if algorithm == "SVM" and training_time > 5:
655
+ recommendations.append("**Use kernel linear** para datasets grandes")
656
+ if metrics['AUC-ROC'] < 0.7:
657
+ recommendations.append("**Experimente diferentes algoritmos** ou faça feature engineering")
658
+
659
+ for rec in recommendations:
660
+ st.write(f"• {rec}")
661
+
662
+ if not recommendations:
663
+ st.success("**✅ Parâmetros bem ajustados!** Continue monitorando o desempenho.")
664
+
665
+ # Ranking
666
+ st.subheader("🏆 Ranking dos Modelos")
667
+ if dashboard.results:
668
+ results_df = pd.DataFrame(dashboard.results).T
669
+ results_df = results_df.sort_values('F1-Score', ascending=False)
670
+
671
+ # Mostrar tabela
672
+ st.dataframe(results_df.style.format("{:.4f}").background_gradient(cmap='Blues'),
673
+ use_container_width=True)
674
+
675
+ # Melhor modelo
676
+ best_model = results_df.index[0]
677
+ best_f1 = results_df.loc[best_model, 'F1-Score']
678
+ best_auc = results_df.loc[best_model, 'AUC-ROC']
679
+
680
+ st.markdown(f'''
681
+ <div class="best-model">
682
+ <h3>🎉 Melhor Modelo: {best_model}</h3>
683
+ <p><strong>F1-Score:</strong> {best_f1:.4f} | <strong>AUC-ROC:</strong> {best_auc:.4f}</p>
684
+ <p>Este modelo apresenta o melhor balanceamento entre precisão e recall.</p>
685
+ </div>
686
+ ''', unsafe_allow_html=True)
687
+
688
+ else:
689
+ # Estado: dados carregados mas nenhum modelo treinado
690
+ st.info("""
691
+ **📊 Dataset carregado com sucesso!**
692
+
693
+ Configure o algoritmo e os parâmetros na barra lateral e clique em **'Treinar Modelo'**
694
+ para iniciar a análise preditiva de cancelamentos.
695
+ """)
696
 
697
+ if __name__ == "__main__":
698
+ main()