# app.py - Dashboard Interativo de Cancelamento de Reservas import streamlit as st import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns import joblib from sklearn.metrics import (accuracy_score, precision_score, recall_score, f1_score, roc_auc_score, roc_curve, confusion_matrix) from sklearn.linear_model import LogisticRegression from sklearn.neighbors import KNeighborsClassifier from sklearn.svm import SVC from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler, LabelEncoder import plotly.graph_objects as go import plotly.express as px import time import warnings warnings.filterwarnings('ignore') # Tentar importar SMOTE, mas continuar funcionando mesmo se falhar try: from imblearn.over_sampling import SMOTE SMOTE_AVAILABLE = True except ImportError as e: st.warning(f"⚠️ SMOTE não disponível: {e}. Continuando sem balanceamento automático.") SMOTE_AVAILABLE = False # Configuração da página st.set_page_config( page_title="Dashboard - Cancelamento de Reservas", page_icon="🏨", layout="wide", initial_sidebar_state="expanded" ) # CSS customizado st.markdown(""" """, unsafe_allow_html=True) class HotelBookingDashboard: def __init__(self): self.models = {} self.results = {} self.X_train = None self.X_test = None self.y_train = None self.y_test = None self.scaler = StandardScaler() self.is_data_loaded = False def load_and_preprocess_data(self, df): """Carrega e pré-processa o dataset""" try: st.info("🔄 Iniciando pré-processamento dos dados...") # Fazer uma cópia do dataframe df_clean = df.copy() # 1. Identificar a coluna target target_col = self._identify_target_column(df_clean) if not target_col: st.error("❌ Não foi possível identificar a coluna target. Procure por colunas como 'is_canceled', 'canceled', etc.") return False st.success(f"✅ Coluna target identificada: '{target_col}'") # 2. Tratamento de valores missing df_clean = self._handle_missing_values(df_clean) # 3. Codificar variáveis categóricas df_encoded = self._encode_categorical_variables(df_clean) # 4. Separar features e target X = df_encoded.drop(columns=[target_col]) y = df_encoded[target_col] # 5. Dividir e balancear dados success = self._split_and_balance_data(X, y) if success: self.is_data_loaded = True st.success("✅ Dados carregados e pré-processados com sucesso!") return True else: return False except Exception as e: st.error(f"❌ Erro no pré-processamento: {str(e)}") return False def _identify_target_column(self, df): """Identifica a coluna target automaticamente""" target_candidates = ['is_canceled', 'canceled', 'cancelled', 'is_cancelled', 'booking_status'] for candidate in target_candidates: if candidate in df.columns: # Se encontrou, renomear para padronizar if candidate != 'is_canceled': df.rename(columns={candidate: 'is_canceled'}, inplace=True) return 'is_canceled' # Se não encontrou, verificar colunas binárias binary_cols = [] for col in df.columns: if df[col].dtype in ['int64', 'float64'] and df[col].nunique() == 2: binary_cols.append(col) if binary_cols: st.warning(f"🔍 Colunas binárias encontradas: {binary_cols}") return binary_cols[0] return None def _handle_missing_values(self, df): """Trata valores missing seguindo as boas práticas""" df_clean = df.copy() # Remover coluna company se existir (muitos NAs) if 'company' in df_clean.columns: df_clean.drop('company', axis=1, inplace=True) # Preencher outros missing values for col in df_clean.columns: if df_clean[col].isnull().sum() > 0: if df_clean[col].dtype == 'object': # Preencher com moda para categóricas df_clean[col].fillna(df_clean[col].mode()[0], inplace=True) else: # Preencher com mediana para numéricas df_clean[col].fillna(df_clean[col].median(), inplace=True) return df_clean def _encode_categorical_variables(self, df): """Codifica variáveis categóricas""" df_encoded = df.copy() # Identificar colunas categóricas categorical_cols = df_encoded.select_dtypes(include=['object']).columns.tolist() if categorical_cols: st.info(f"📊 Codificando {len(categorical_cols)} variáveis categóricas...") # Label Encoding para alta cardinalidade (>20 categorias) high_cardinality = [col for col in categorical_cols if df_encoded[col].nunique() > 20] low_cardinality = [col for col in categorical_cols if df_encoded[col].nunique() <= 20] for col in high_cardinality: le = LabelEncoder() df_encoded[col] = le.fit_transform(df_encoded[col].astype(str)) # One-Hot Encoding para baixa cardinalidade if low_cardinality: df_encoded = pd.get_dummies(df_encoded, columns=low_cardinality, drop_first=True) return df_encoded def _split_and_balance_data(self, X, y): """Divide e balanceia os dados""" try: # Converter todas as colunas para numérico X = X.apply(pd.to_numeric, errors='coerce').fillna(0) # Dividir dados X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=42, stratify=y ) # Aplicar SMOTE se disponível e necessário if (SMOTE_AVAILABLE and y_train.value_counts().min() / y_train.value_counts().max() < 0.3): smote = SMOTE(random_state=42) X_train, y_train = smote.fit_resample(X_train, y_train) st.info("✅ SMOTE aplicado para balanceamento dos dados") elif not SMOTE_AVAILABLE: st.warning("⚠️ SMOTE não disponível. Usando dados originais (pode haver desbalanceamento).") else: st.info("ℹ️ Dados já balanceados, SMOTE não aplicado.") # Escalonar features X_train_scaled = self.scaler.fit_transform(X_train) X_test_scaled = self.scaler.transform(X_test) self.X_train = X_train_scaled self.X_test = X_test_scaled self.y_train = y_train self.y_test = y_test st.success(f"✅ Dados divididos: Treino {X_train_scaled.shape}, Teste {X_test_scaled.shape}") return True except Exception as e: st.error(f"❌ Erro ao dividir dados: {str(e)}") return False def train_logistic_regression(self, C=1.0, penalty='l2', solver='lbfgs'): """Treina Regressão Logística""" model = LogisticRegression(C=C, penalty=penalty, solver=solver, max_iter=1000, random_state=42) start_time = time.time() model.fit(self.X_train, self.y_train) training_time = time.time() - start_time return model, training_time def train_knn(self, n_neighbors=5, metric='euclidean', weights='uniform'): """Treina KNN""" model = KNeighborsClassifier(n_neighbors=n_neighbors, metric=metric, weights=weights) start_time = time.time() model.fit(self.X_train, self.y_train) training_time = time.time() - start_time return model, training_time def train_svm(self, C=1.0, kernel='rbf', gamma='scale'): """Treina SVM""" model = SVC(C=C, kernel=kernel, gamma=gamma, probability=True, random_state=42) start_time = time.time() model.fit(self.X_train, self.y_train) training_time = time.time() - start_time return model, training_time def evaluate_model(self, model, model_name, training_time): """Avalia modelo e retorna métricas""" y_pred = model.predict(self.X_test) y_proba = model.predict_proba(self.X_test)[:, 1] metrics = { 'Acurácia': accuracy_score(self.y_test, y_pred), 'Precisão': precision_score(self.y_test, y_pred, zero_division=0), 'Recall': recall_score(self.y_test, y_pred, zero_division=0), 'F1-Score': f1_score(self.y_test, y_pred, zero_division=0), 'AUC-ROC': roc_auc_score(self.y_test, y_proba), 'Tempo Treino (s)': training_time } # Curva ROC fpr, tpr, _ = roc_curve(self.y_test, y_proba) roc_data = {'fpr': fpr, 'tpr': tpr, 'auc': metrics['AUC-ROC']} # Matriz de confusão cm = confusion_matrix(self.y_test, y_pred) return metrics, roc_data, cm def plot_roc_comparison(self, current_roc, current_model_name): """Plota comparação de curvas ROC""" fig = go.Figure() # Curva do modelo atual fig.add_trace(go.Scatter( x=current_roc['fpr'], y=current_roc['tpr'], mode='lines', name=f'{current_model_name} (AUC = {current_roc["auc"]:.3f})', line=dict(width=3, color='red') )) # Curvas dos outros modelos colors = ['blue', 'green', 'orange', 'purple'] for i, (model_name, model) in enumerate(self.models.items()): if model_name != current_model_name: try: y_proba = model.predict_proba(self.X_test)[:, 1] fpr, tpr, _ = roc_curve(self.y_test, y_proba) auc = roc_auc_score(self.y_test, y_proba) fig.add_trace(go.Scatter( x=fpr, y=tpr, mode='lines', name=f'{model_name} (AUC = {auc:.3f})', line=dict(width=2, color=colors[i % len(colors)], dash='dash') )) except: continue # Linha de referência fig.add_trace(go.Scatter( x=[0, 1], y=[0, 1], mode='lines', name='Classificador Aleatório', line=dict(dash='dash', color='grey') )) fig.update_layout( title='Comparação das Curvas ROC', xaxis_title='Taxa de Falsos Positivos', yaxis_title='Taxa de Verdadeiros Positivos', width=600, height=500 ) return fig def main(): # Header principal st.markdown('

🏨 Dashboard - Cancelamento de Reservas

', unsafe_allow_html=True) # Inicializar dashboard dashboard = HotelBookingDashboard() # ===== SEÇÃO DE CARREGAMENTO DE DADOS ===== if not dashboard.is_data_loaded: st.markdown("""

📊 Upload do Dataset

Faça upload do dataset de reservas de hotel para começar a análise

""", unsafe_allow_html=True) # Upload centralizado col1, col2, col3 = st.columns([1, 2, 1]) with col2: uploaded_file = st.file_uploader( "**Selecione o arquivo CSV do dataset**", type=['csv'], help="Faça upload do dataset de reservas de hotel (ex: hotel_bookings.csv)", key="main_uploader" ) # Instruções with st.expander("📋 Instruções de Uso", expanded=True): st.markdown(""" **Como usar este dashboard:** 1. **📁 Faça upload** do dataset de reservas de hotel (formato CSV) 2. **🔄 Aguarde o processamento** automático dos dados 3. **⚙️ Configure** o algoritmo e parâmetros desejados 4. **🚀 Treine o modelo** e analise os resultados 5. **📊 Compare** o desempenho entre diferentes modelos **Requisitos do dataset:** - Formato CSV - Deve conter uma coluna target (cancelamento) - Colunas típicas: `lead_time`, `adr`, `adults`, `is_canceled`, etc. - Suporta o dataset "Hotel Booking Demand" do Kaggle """) # Processar o arquivo assim que for carregado if uploaded_file is not None: try: with st.spinner("📊 Carregando e analisando o dataset..."): # Ler o arquivo df = pd.read_csv(uploaded_file) # Mostrar informações básicas st.success(f"✅ Dataset carregado: {df.shape[0]} linhas × {df.shape[1]} colunas") # Preview do dataset with st.expander("👀 Visualização do Dataset (primeiras 10 linhas)"): st.dataframe(df.head(10), use_container_width=True) # Informações das colunas with st.expander("📋 Informações das Colunas"): col1, col2 = st.columns(2) with col1: st.write("**Colunas Numéricas:**") numeric_cols = df.select_dtypes(include=[np.number]).columns.tolist() for col in numeric_cols[:10]: # Mostrar apenas as primeiras 10 st.write(f"- {col}") if len(numeric_cols) > 10: st.write(f"- ... e mais {len(numeric_cols) - 10} colunas") with col2: st.write("**Colunas Categóricas:**") categorical_cols = df.select_dtypes(include=['object']).columns.tolist() for col in categorical_cols[:10]: # Mostrar apenas as primeiras 10 st.write(f"- {col}") if len(categorical_cols) > 10: st.write(f"- ... e mais {len(categorical_cols) - 10} colunas") # Processar automaticamente if st.button("🔄 Processar Dataset e Continuar", type="primary", use_container_width=True): with st.spinner("Processando dataset... Isso pode levar alguns segundos"): success = dashboard.load_and_preprocess_data(df) if success: st.session_state.data_processed = True st.session_state.dashboard = dashboard st.rerun() else: st.error("Falha no processamento do dataset. Verifique os dados e tente novamente.") except Exception as e: st.error(f"❌ Erro ao carregar arquivo: {str(e)}") st.info("💡 **Dica:** Verifique se o arquivo é um CSV válido e tente novamente.") # Exemplo de estrutura esperada with st.expander("🎯 Exemplo de Dataset Compatível"): st.markdown(""" **Estrutura típica do dataset Hotel Booking Demand:** ```csv hotel,lead_time,arrival_date_year,arrival_date_month,arrival_date_week_number, arrival_date_day_of_month,stays_in_weekend_nights,stays_in_week_nights,adults, children,babies,meal,country,market_segment,distribution_channel, is_repeated_guest,previous_cancellations,previous_bookings_not_canceled, reserved_room_type,assigned_room_type,booking_changes,deposit_type,agent, company,days_in_waiting_list,customer_type,adr,required_car_parking_spaces, total_of_special_requests,reservation_status,is_canceled ``` **Coluna target:** `is_canceled` (1 = cancelado, 0 = não cancelado) """) return # ===== SEÇÃO PRINCIPAL (quando dados estão carregados) ===== # Recuperar o dashboard do session_state se necessário if 'dashboard' in st.session_state: dashboard = st.session_state.dashboard # Sidebar - Configurações do Modelo st.sidebar.header("⚙️ Configurações do Modelo") # Seleção do algoritmo algorithm = st.sidebar.selectbox( "Escolha o algoritmo:", ["Regressão Logística", "KNN", "SVM"], index=0 ) # Parâmetros específicos st.sidebar.subheader("📊 Parámetros do Modelo") if algorithm == "Regressão Logística": st.sidebar.markdown('
', unsafe_allow_html=True) C_lr = st.sidebar.slider("Parâmetro C (Regularização)", 0.01, 10.0, 1.0, 0.01) penalty = st.sidebar.selectbox("Tipo de Penalidade", ["l2", "l1"]) solver = st.sidebar.selectbox("Algoritmo", ["lbfgs", "liblinear", "saga"]) st.sidebar.markdown('
', unsafe_allow_html=True) elif algorithm == "KNN": st.sidebar.markdown('
', unsafe_allow_html=True) n_neighbors = st.sidebar.slider("Número de Vizinhos (k)", 1, 50, 5) metric = st.sidebar.selectbox("Métrica de Distância", ["euclidean", "manhattan", "minkowski"]) weights = st.sidebar.selectbox("Pesos", ["uniform", "distance"]) st.sidebar.markdown('
', unsafe_allow_html=True) else: # SVM st.sidebar.markdown('
', unsafe_allow_html=True) C_svm = st.sidebar.slider("Parâmetro C", 0.01, 10.0, 1.0, 0.01) kernel = st.sidebar.selectbox("Kernel", ["rbf", "linear", "poly", "sigmoid"]) gamma = st.sidebar.selectbox("Gamma", ["scale", "auto"]) st.sidebar.markdown('
', unsafe_allow_html=True) # Botão de treinamento train_button = st.sidebar.button("🚀 Treinar Modelo", type="primary", use_container_width=True) # Informações na sidebar st.sidebar.markdown("---") st.sidebar.info(""" **📊 Status do Dataset:** - ✅ Dados carregados - 📈 Pronto para treinamento """) st.sidebar.markdown("---") if st.sidebar.button("🔄 Carregar Novo Dataset", use_container_width=True): st.session_state.clear() st.rerun() # Conteúdo principal - Status dos dados st.subheader("📈 Status dos Dados Carregados") col1, col2, col3, col4 = st.columns(4) with col1: st.metric("Amostras de Treino", f"{dashboard.X_train.shape[0]:,}") with col2: st.metric("Amostras de Teste", f"{dashboard.X_test.shape[0]:,}") with col3: st.metric("Features", f"{dashboard.X_train.shape[1]}") with col4: balance = pd.Series(dashboard.y_train).value_counts() if len(balance) == 2: st.metric("Balanceamento", f"{balance[0]}:{balance[1]}") else: st.metric("Classes", len(balance)) # Análise exploratória with st.expander("🔍 Análise Exploratória dos Dados"): col1, col2 = st.columns(2) with col1: # Distribuição do target fig, ax = plt.subplots(figsize=(8, 6)) balance = pd.Series(dashboard.y_train).value_counts() labels = ['Não Cancelado', 'Cancelado'] if len(balance) == 2 else [f'Classe {i}' for i in balance.index] ax.pie(balance.values, labels=labels, autopct='%1.1f%%', startangle=90) ax.set_title('Distribuição do Target') st.pyplot(fig) with col2: # Estatísticas básicas st.write("**Estatísticas do Dataset:**") total_samples = dashboard.X_train.shape[0] + dashboard.X_test.shape[0] cancel_rate = (dashboard.y_train.sum() + dashboard.y_test.sum()) / total_samples * 100 stats_df = pd.DataFrame({ 'Métrica': ['Total de Amostras', 'Features', 'Taxa de Cancelamento', 'Balanceamento'], 'Valor': [ f"{total_samples:,}", f"{dashboard.X_train.shape[1]}", f"{cancel_rate:.1f}%", f"{balance[0]}:{balance[1]}" if len(balance) == 2 else "Múltiplas classes" ] }) st.dataframe(stats_df, hide_index=True) # Conteúdo principal - Resultados do Modelo if train_button: with st.spinner(f"Treinando modelo {algorithm}..."): # Treinar modelo if algorithm == "Regressão Logística": model, training_time = dashboard.train_logistic_regression( C=C_lr, penalty=penalty, solver=solver ) model_name = f"RL_C={C_lr}" elif algorithm == "KNN": model, training_time = dashboard.train_knn( n_neighbors=n_neighbors, metric=metric, weights=weights ) model_name = f"KNN_k={n_neighbors}_{metric}" else: # SVM model, training_time = dashboard.train_svm( C=C_svm, kernel=kernel, gamma=gamma ) model_name = f"SVM_{kernel}_C={C_svm}" # Avaliar metrics, roc_data, cm = dashboard.evaluate_model(model, model_name, training_time) # Salvar modelo dashboard.models[model_name] = model dashboard.results[model_name] = metrics # Resultados st.success(f"✅ Modelo {algorithm} treinado com sucesso em {training_time:.2f} segundos!") # Métricas st.subheader("📊 Métricas de Desempenho") col1, col2, col3, col4, col5 = st.columns(5) with col1: st.metric("Acurácia", f"{metrics['Acurácia']:.4f}") with col2: st.metric("Precisão", f"{metrics['Precisão']:.4f}") with col3: st.metric("Recall", f"{metrics['Recall']:.4f}") with col4: st.metric("F1-Score", f"{metrics['F1-Score']:.4f}") with col5: st.metric("AUC-ROC", f"{metrics['AUC-ROC']:.4f}") # Visualizações st.subheader("📈 Visualizações") col1, col2 = st.columns(2) with col1: # Curva ROC roc_fig = dashboard.plot_roc_comparison(roc_data, model_name) st.plotly_chart(roc_fig, use_container_width=True) with col2: # Matriz de confusão fig_cm, ax = plt.subplots(figsize=(6, 4)) sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', ax=ax) ax.set_xlabel('Predito') ax.set_ylabel('Verdadeiro') ax.set_title('Matriz de Confusão') st.pyplot(fig_cm) # Análise st.subheader("🔍 Análise e Interpretação") col1, col2 = st.columns(2) with col1: st.markdown("### 📋 Avaliação do Desempenho") if metrics['F1-Score'] >= 0.7: st.success("**🎯 Excelente desempenho!** Modelo bem balanceado entre precisão e recall.") elif metrics['F1-Score'] >= 0.5: st.info("**👍 Bom desempenho!** Resultados satisfatórios para aplicação prática.") else: st.warning("**⚠️ Desempenho moderado.** Considere ajustar parâmetros ou features.") if metrics['AUC-ROC'] >= 0.8: st.success("**🔝 Ótima discriminação!** O modelo separa muito bem as classes.") elif metrics['AUC-ROC'] >= 0.7: st.info("**📈 Boa discriminação!** Separação adequada entre cancelamentos e não-cancelamentos.") else: st.warning("**📉 Discriminação moderada.** Há espaço para melhorias na separação das classes.") with col2: st.markdown("### 💡 Recomendações Práticas") recommendations = [] if metrics['Precisão'] < 0.6: recommendations.append("**Aumente o threshold** para reduzir falsos positivos") if metrics['Recall'] < 0.6: recommendations.append("**Diminua o threshold** para capturar mais cancelamentos reais") if algorithm == "KNN" and n_neighbors < 5: recommendations.append("**Aumente o valor de k** para reduzir overfitting") if algorithm == "SVM" and training_time > 5: recommendations.append("**Use kernel linear** para datasets grandes") if metrics['AUC-ROC'] < 0.7: recommendations.append("**Experimente diferentes algoritmos** ou faça feature engineering") for rec in recommendations: st.write(f"• {rec}") if not recommendations: st.success("**✅ Parâmetros bem ajustados!** Continue monitorando o desempenho.") # Ranking st.subheader("🏆 Ranking dos Modelos") if dashboard.results: results_df = pd.DataFrame(dashboard.results).T results_df = results_df.sort_values('F1-Score', ascending=False) # Mostrar tabela st.dataframe(results_df.style.format("{:.4f}").background_gradient(cmap='Blues'), use_container_width=True) # Melhor modelo best_model = results_df.index[0] best_f1 = results_df.loc[best_model, 'F1-Score'] best_auc = results_df.loc[best_model, 'AUC-ROC'] st.markdown(f'''

🎉 Melhor Modelo: {best_model}

F1-Score: {best_f1:.4f} | AUC-ROC: {best_auc:.4f}

Este modelo apresenta o melhor balanceamento entre precisão e recall.

''', unsafe_allow_html=True) else: # Estado: dados carregados mas nenhum modelo treinado st.info(""" **📊 Dataset carregado com sucesso!** Configure o algoritmo e os parâmetros na barra lateral e clique em **'Treinar Modelo'** para iniciar a análise preditiva de cancelamentos. """) if __name__ == "__main__": main()