brunaaaz commited on
Commit
4015ccf
·
verified ·
1 Parent(s): f416f9d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +377 -671
app.py CHANGED
@@ -1,698 +1,404 @@
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()
 
 
 
 
 
1
  import streamlit as st
2
  import pandas as pd
3
  import numpy as np
4
+ import plotly.express as px
5
+ import plotly.graph_objects as go
6
+ from sklearn.model_selection import train_test_split
7
+ from sklearn.preprocessing import StandardScaler
8
+ from sklearn.metrics import (
9
+ classification_report,
10
+ confusion_matrix,
11
+ roc_curve,
12
+ roc_auc_score,
13
+ precision_recall_fscore_support,
14
+ )
15
  from sklearn.linear_model import LogisticRegression
16
  from sklearn.neighbors import KNeighborsClassifier
17
  from sklearn.svm import SVC
18
+ from imblearn.over_sampling import SMOTE
 
 
 
19
  import time
20
  import warnings
21
+
22
+ warnings.filterwarnings("ignore")
23
+
24
+ # --- Configuração da Página ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  st.set_page_config(
26
+ page_title="Dashboard de Previsão de Cancelamento",
27
  page_icon="🏨",
28
  layout="wide",
 
29
  )
30
 
31
+ # --- Título e Contexto ---
32
+ st.title("🏨 Dashboard de Previsão de Cancelamento de Reservas")
33
+ st.markdown(
34
+ """
35
+ **Sua Missão como Analista de Dados:**
36
+ Você é analista de dados em uma rede internacional de hotéis. Sua missão é desenvolver e comparar três modelos preditivos (Regressão Logística, KNN e SVM) capazes de identificar antecipadamente as reservas com maior probabilidade de cancelamento. Esta ferramenta permite simular esse processo de forma interativa.
37
+ """
38
+ )
39
+
40
+
41
+ # --- Funções de Processamento (Otimizadas com Cache) ---
42
+ @st.cache_data
43
+ def load_data(file_path):
44
+ """Carrega o dataset principal. O cache evita recarregar a cada interação."""
45
+ try:
46
+ df = pd.read_csv(file_path)
47
+ return df
48
+ except FileNotFoundError:
49
+ st.error(
50
+ f"Erro: Arquivo '{file_path}' não encontrado. Faça o upload do arquivo para o seu Hugging Face Space."
51
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  return None
53
+
54
+
55
+ @st.cache_data
56
+ def preprocess_data(df):
57
+ """Aplica o pré-processamento seguindo as diretrizes da Tarefa 3."""
58
+ df_proc = df.copy()
59
+
60
+ # 1. Tratamento de valores faltantes
61
+ # Preenche 'country' com a moda (mais comum)
62
+ df_proc["country"].fillna(df_proc["country"].mode()[0], inplace=True)
63
+ # Para 'agent' e 'company', NaN significa "Não Aplicável" ou "Direto". Substituímos por 0.
64
+ df_proc["agent"].fillna(0, inplace=True)
65
+ df_proc["company"].fillna(0, inplace=True)
66
+ # Assume que NaN em 'children' significa 0 crianças.
67
+ df_proc["children"].fillna(0, inplace=True)
68
+
69
+ # 2. Tratamento de Outliers (simples, para performance)
70
+ # Remove 'adr' (Average Daily Rate) irrealista
71
+ df_proc = df_proc[(df_proc["adr"] >= 0) & (df_proc["adr"] < 5000)]
72
+
73
+ # 3. Engenharia de Features (simples)
74
+ # Cria 'total_stay' e 'total_guests'
75
+ df_proc["total_stay"] = (
76
+ df_proc["stays_in_weekend_nights"] + df_proc["stays_in_week_nights"]
77
+ )
78
+ df_proc["total_guests"] = (
79
+ df_proc["adults"] + df_proc["children"] + df_proc["babies"]
80
+ )
81
+
82
+ # Remove hóspedes com 0 pessoas (inválido)
83
+ df_proc = df_proc[df_proc["total_guests"] > 0]
84
+
85
+ # 4. Seleção de Variáveis (Baseado na Tarefa 3 - 8 a 15 features)
86
+ # Variável Alvo
87
+ y = df_proc["is_canceled"]
88
+
89
+ # Features Numéricas
90
+ numeric_features = [
91
+ "lead_time",
92
+ "total_stay",
93
+ "total_guests",
94
+ "adr",
95
+ "previous_cancellations",
96
+ "previous_bookings_not_canceled",
97
+ "booking_changes",
98
+ "days_in_waiting_list",
99
+ "total_of_special_requests",
100
+ ]
101
+
102
+ # Features Categóricas
103
+ categorical_features = [
104
+ "hotel",
105
+ "market_segment",
106
+ "distribution_channel",
107
+ "deposit_type",
108
+ "customer_type",
109
+ "is_repeated_guest",
110
+ ]
111
+
112
+ # Garante que todas as colunas existem
113
+ all_features = numeric_features + categorical_features
114
+ df_features = df_proc[all_features]
115
+
116
+ # 5. Codificação de Variáveis Categóricas (Dummies)
117
+ X = pd.get_dummies(df_features, columns=categorical_features, drop_first=True)
118
+
119
+ return X, y
120
+
121
+
122
+ # --- Funções do Modelo ---
123
+ def get_model(algorithm, params):
124
+ """Instancia o modelo com base nos parâmetros do usuário."""
125
+ if algorithm == "Regressão Logística":
126
+ model = LogisticRegression(
127
+ C=params["C_rl"],
128
+ solver="liblinear", # Bom para datasets menores e binários
129
+ random_state=42,
130
+ max_iter=1000,
131
+ )
132
+ elif algorithm == "KNN":
133
+ model = KNeighborsClassifier(
134
+ n_neighbors=params["k"], metric=params["distance_metric"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  )
136
+ elif algorithm == "SVM":
137
+ model = SVC(
138
+ C=params["C_svm"],
139
+ kernel=params["kernel"],
140
+ gamma=params["gamma"] if params["kernel"] == "rbf" else "auto",
141
+ probability=True, # Necessário para Curva ROC
142
+ random_state=42,
143
+ )
144
+ return model
145
 
146
+
147
+ # --- Funções de Plotagem ---
148
+ def plot_roc_curve(y_test, y_proba, auc):
149
+ """Plota a curva ROC usando Plotly."""
150
+ fpr, tpr, _ = roc_curve(y_test, y_proba)
151
+ fig = px.area(
152
+ x=fpr,
153
+ y=tpr,
154
+ title=f"Curva ROC (AUC = {auc:.4f})",
155
+ labels=dict(x="Taxa de Falsos Positivos", y="Taxa de Verdadeiros Positivos"),
156
+ width=700,
157
+ height=500,
158
+ )
159
+ fig.add_shape(type="line", line=dict(dash="dash"), x0=0, x1=1, y0=0, y1=1)
160
+ fig.update_layout(
161
+ yaxis_title="Taxa de Verdadeiros Positivos (Sensibilidade)",
162
+ xaxis_title="Taxa de Falsos Positivos (1 - Especificidade)",
163
+ )
164
+ return fig
165
+
166
+
167
+ def plot_confusion_matrix(y_test, y_pred):
168
+ """Plota a Matriz de Confusão usando Plotly."""
169
+ cm = confusion_matrix(y_test, y_pred)
170
+ cm_text = [[str(y) for y in x] for x in cm]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
 
172
+ fig = px.imshow(
173
+ cm,
174
+ labels=dict(
175
+ x="Previsão do Modelo", y="Valor Real", color="Contagem"
176
+ ),
177
+ x=["Não Cancelou (0)", "Cancelou (1)"],
178
+ y=["Não Cancelou (0)", "Cancelou (1)"],
179
+ color_continuous_scale="Blues",
180
+ text_auto=True,
181
+ )
182
 
183
+ fig.update_layout(
184
+ title="Matriz de Confusão",
185
+ xaxis_title="Previsão do Modelo",
186
+ yaxis_title="Valor Real",
187
+ width=600,
188
+ height=500,
189
+ )
190
+ return fig
191
+
192
+
193
+ # --- Configuração da Sidebar (Controles) ---
194
+ st.sidebar.header("⚙️ Painel de Controle do Analista")
195
+
196
+ df_original = load_data("hotel_bookings.csv")
197
+
198
+ if df_original is not None:
199
+ # 1. Controles de Amostragem e Divisão
200
+ st.sidebar.subheader("1. Configuração dos Dados")
201
+ sample_size = st.sidebar.slider(
202
+ "Tamanho da Amostra para Treinamento",
203
+ min_value=1000,
204
+ max_value=20000,
205
+ value=3000,
206
+ step=500,
207
+ help="Use uma amostra menor para velocidade ou maior para precisão. O dataset completo tem >100k linhas.",
208
+ )
209
+ test_split_pct = st.sidebar.slider(
210
+ "Percentual de Dados para Teste",
211
+ min_value=0.1,
212
+ max_value=0.5,
213
+ value=0.3,
214
+ step=0.05,
215
+ )
216
+ use_smote = st.sidebar.checkbox(
217
+ "Aplicar SMOTE (Corrigir Desbalanceamento)",
218
+ value=False,
219
+ help="Pode melhorar o 'Recall', mas aumenta o tempo de treino.",
220
+ )
221
+
222
+ # 2. Seleção de Algoritmo
223
+ st.sidebar.subheader("2. Seleção do Algoritmo")
224
  algorithm = st.sidebar.selectbox(
225
+ "Escolha o Algoritmo",
226
+ ("Regressão Logística", "KNN", "SVM"),
 
227
  )
228
+
229
+ # 3. Ajuste de Hiperparâmetros (Dinâmico)
230
+ st.sidebar.subheader(f"3. Ajuste de Parâmetros ({algorithm})")
231
+ params = {}
232
+
233
  if algorithm == "Regressão Logística":
234
+ params["C_rl"] = st.sidebar.select_slider(
235
+ "C (Força da Regularização)",
236
+ options=[0.01, 0.1, 1.0, 10.0, 100.0],
237
+ value=1.0,
238
+ help="Valores menores = mais regularização (modelo mais simples).",
239
+ )
240
+
241
  elif algorithm == "KNN":
242
+ params["k"] = st.sidebar.slider(
243
+ "k (Número de Vizinhos)", min_value=3, max_value=21, value=5, step=2
244
+ )
245
+ params["distance_metric"] = st.sidebar.selectbox(
246
+ "Métrica de Distância", ("euclidean", "manhattan")
247
+ )
248
+
249
+ elif algorithm == "SVM":
250
+ params["kernel"] = st.sidebar.selectbox("Kernel", ("linear", "rbf"))
251
+ params["C_svm"] = st.sidebar.select_slider(
252
+ "C (Regularização)",
253
+ options=[0.1, 1.0, 10.0, 50.0],
254
+ value=1.0,
255
+ help="Controla o trade-off entre erro de treino e margem.",
256
+ )
257
+ if params["kernel"] == "rbf":
258
+ params["gamma"] = st.sidebar.select_slider(
259
+ "Gamma (Influência do Ponto)",
260
+ options=[0.001, 0.01, 0.1, 1.0],
261
+ value=0.1,
262
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
263
  else:
264
+ params["gamma"] = "auto"
265
+
266
+ # --- Botão de Execução ---
267
+ st.sidebar.markdown("---")
268
+ run_button = st.sidebar.button("Executar Análise", type="primary")
269
+
270
+ # --- Área Principal de Exibição ---
271
+ if run_button:
272
+ with st.spinner(
273
+ f"Executando pipeline para {algorithm} com {sample_size} amostras..."
274
+ ):
275
+ start_time = time.time()
276
+
277
+ # 1. Amostrar
278
+ df_sample = df_original.sample(n=sample_size, random_state=42)
279
+
280
+ # 2. Pré-processar
281
+ X, y = preprocess_data(df_sample)
282
+
283
+ # 3. Dividir (Train/Test)
284
+ X_train, X_test, y_train, y_test = train_test_split(
285
+ X, y, test_size=test_split_pct, random_state=42, stratify=y
286
+ )
287
+
288
+ # 4. Escalonar (MUITO importante para KNN e SVM)
289
+ scaler = StandardScaler()
290
+ X_train_scaled = scaler.fit_transform(X_train)
291
+ X_test_scaled = scaler.transform(X_test)
292
+
293
+ # 5. Aplicar SMOTE (Opcional)
294
+ if use_smote:
295
+ smote = SMOTE(random_state=42)
296
+ X_train_scaled, y_train = smote.fit_resample(X_train_scaled, y_train)
297
+
298
+ # 6. Treinar Modelo
299
+ model = get_model(algorithm, params)
300
+ model.fit(X_train_scaled, y_train)
301
+
302
+ # 7. Avaliar
303
+ y_pred = model.predict(X_test_scaled)
304
+ y_proba = model.predict_proba(X_test_scaled)[:, 1]
305
+ auc = roc_auc_score(y_test, y_proba)
306
+ report = classification_report(y_test, y_pred, output_dict=True)
307
+ report_df = pd.DataFrame(report).transpose()
308
+
309
+ # Extrai métricas específicas para classe 1 (Cancelamento)
310
+ (
311
+ precision,
312
+ recall,
313
+ f1_score,
314
+ _,
315
+ ) = precision_recall_fscore_support(y_test, y_pred, average="binary")
316
+
317
+
318
+ end_time = time.time()
319
+ training_time = end_time - start_time
320
+
321
+ # --- Exibição dos Resultados ---
322
+ st.header(f"Resultados para: {algorithm}")
323
+
324
+ # Métricas Chave
325
+ st.subheader("Visão Geral das Métricas (Classe 1: 'Cancelou')")
326
+ col1, col2, col3, col4 = st.columns(4)
327
+ col1.metric("AUC (Area Under Curve)", f"{auc:.3f}")
328
+ col2.metric("F1-Score", f"{f1_score:.3f}")
329
+ col3.metric("Precisão (Precision)", f"{precision:.3f}")
330
+ col4.metric("Recall (Sensibilidade)", f"{recall:.3f}")
331
+
332
+ st.markdown(f"**Tempo de Treinamento e Avaliação:** {training_time:.2f} segundos")
333
+
334
+ # Gráficos
335
+ st.subheader("Visualização das Métricas")
336
+ fig_roc = plot_roc_curve(y_test, y_proba, auc)
337
+ fig_cm = plot_confusion_matrix(y_test, y_pred)
338
+
339
+ col_graph1, col_graph2 = st.columns(2)
340
+ with col_graph1:
341
+ st.plotly_chart(fig_roc, use_container_width=True)
342
+ with col_graph2:
343
+ st.plotly_chart(fig_cm, use_container_width=True)
344
+
345
+ st.subheader("Relatório de Classificação Detalhado")
346
+ st.dataframe(report_df.style.format("{:.3f}"))
347
+
348
+ # --- Interpretação Gerencial Automática ---
349
+ st.header("💡 Interpretação Gerencial e Recomendações")
350
+
351
+ st.subheader(f"Análise Gerencial do Modelo: {algorithm}")
352
+
353
  if algorithm == "Regressão Logística":
354
+ st.markdown("""
355
+ **O que é?** Um modelo estatístico que calcula a *probabilidade* de cancelamento. É o modelo mais fácil de interpretar.
356
+ **Ponto Forte (Interpretabilidade):** Podemos ver exatamente quais fatores (como `lead_time` ou `deposit_type`) mais aumentam ou diminuem as chances de cancelamento.
357
+ **Ponto Fraco:** Pode não capturar relações complexas entre as variáveis.
358
+ """)
359
  elif algorithm == "KNN":
360
+ st.markdown("""
361
+ **O que é?** Um modelo que classifica uma nova reserva com base nas reservas mais *parecidas* (vizinhas) que já temos no histórico.
362
+ **Ponto Forte (Intuitivo):** Fácil de entender. "Diga-me quem são seus vizinhos e eu direi quem você é". Bom para capturar padrões locais.
363
+ **Ponto Fraco (Performance):** Lento para prever em datasets muito grandes e muito sensível ao escalonamento dos dados e a features irrelevantes.
364
+ """)
365
+ elif algorithm == "SVM":
366
+ st.markdown("""
367
+ **O que é?** Um modelo que tenta encontrar a *melhor fronteira* ou "linha" que separa os cancelamentos dos não-cancelamentos, maximizando a distância entre os dois grupos.
368
+ **Ponto Forte (Poder Preditivo):** Especialmente com o kernel 'RBF', pode encontrar relações não-lineares complexas que outros modelos não veem. Geralmente tem alta acurácia.
369
+ **Ponto Fraco (Caixa Preta):** É muito difícil de explicar *por que* o modelo tomou uma decisão específica.
370
+ """)
371
+
372
+ st.subheader("Tradução das Métricas para o Negócio Hoteleiro")
373
+ st.markdown(f"""
374
+ * **Precisão (Precision) = {precision:.2f}:** Das reservas que o modelo *disse* que iriam cancelar, **{precision*100:.1f}%** realmente cancelariam.
375
+ * *Impacto:* Uma Precisão alta evita que a equipe de retenção perca tempo com clientes que não iriam cancelar.
376
 
377
+ * **Recall (Sensibilidade) = {recall:.2f}:** Das reservas que *realmente* foram canceladas, o modelo conseguiu identificar **{recall*100:.1f}%** delas.
378
+ * *Impacto:* Este é o custo de "deixar passar". Um Recall baixo significa que muitos cancelamentos estão ocorrendo sem aviso prévio.
379
+
380
+ * **AUC = {auc:.2f}:** Mede a capacidade *geral* do modelo de distinguir entre um cancelamento e uma não-cancelamento. Um valor de 0.5 é um chute; 1.0 é a perfeição. **{auc*100:.1f}%** é um indicador de quão robusto é o modelo.
381
+ """)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
382
 
383
+ st.subheader("Ranking e Recomendações (Visão Geral)")
384
+ st.markdown("""
385
+ A "melhor" escolha depende da estratégia da rede hoteleira:
386
+
387
+ 1. **Para Interpretabilidade (Entender o *Porquê*):**
388
+ * **Vencedor:** **Regressão Logística**.
389
+ * **Ação:** Use este modelo para entender os *drivers* do cancelamento. Se `lead_time` alto é um fator de risco, a equipe de marketing pode criar ações de engajamento para reservas feitas com muita antecedência.
390
+
391
+ 2. **Para Ação Preventiva (Maximizar o *Recall*):**
392
+ * **Vencedor:** Geralmente **SVM** ou **KNN** (com SMOTE) podem ser ajustados para um Recall mais alto.
393
+ * **Ação:** Se a estratégia é "não deixar nenhum cancelamento passar despercebido" (mesmo que isso gere alguns falsos positivos), priorizamos o **Recall**. Podemos enviar um e-mail de confirmação ou uma pequena oferta para *todas* as reservas de alto risco sinalizadas pelo modelo.
394
+
395
+ 3. **Para Eficiência Operacional (Maximizar a *Precisão*):**
396
+ * **Vencedor:** Geralmente **Regressão Logística** ou **SVM (linear)**.
397
+ * **Ação:** Se temos uma equipe de retenção pequena e cara (ex: ligações telefônicas), queremos ter certeza de que cada reserva sinalizada é *realmente* de alto risco. Priorizamos a **Precisão**.
398
+
399
+ **Recomendação Prática (Exemplo):**
400
+ "O modelo de Regressão Logística (AUC de ~0.85) mostrou que reservas do tipo 'Transient' (não-grupo) com `deposit_type` = 'Non Refund' e `lead_time` > 120 dias têm 70% mais chance de cancelar. Recomenda-se uma política de overbooking de 3% para esse segmento específico ou um contato proativo 60 dias antes do check-in."
401
+ """)
402
+
403
+ else:
404
+ st.warning("O arquivo 'hotel_bookings.csv' não foi carregado. O dashboard não pode continuar.")