brunaaaz's picture
Update app.py
643f40b verified
raw
history blame
28.7 kB
# 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("""
<style>
.main-header {
font-size: 2.5rem;
color: #1f77b4;
text-align: center;
margin-bottom: 2rem;
}
.metric-card {
background-color: #f0f2f6;
padding: 1rem;
border-radius: 10px;
border-left: 4px solid #1f77b4;
margin: 0.5rem 0;
}
.best-model {
background-color: #d4edda;
border-left: 4px solid #28a745;
padding: 1rem;
border-radius: 10px;
margin: 1rem 0;
}
.parameter-section {
background-color: #e8f4f8;
padding: 1rem;
border-radius: 10px;
margin: 1rem 0;
}
.data-source-section {
background-color: #e7f3ff;
padding: 2rem;
border-radius: 10px;
border: 2px solid #2196F3;
text-align: center;
margin: 2rem 0;
}
.upload-section {
background-color: #fff3cd;
padding: 2rem;
border-radius: 10px;
border: 2px dashed #ffc107;
text-align: center;
margin: 2rem 0;
}
</style>
""", 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('<h1 class="main-header">🏨 Dashboard - Cancelamento de Reservas</h1>',
unsafe_allow_html=True)
# Inicializar dashboard
dashboard = HotelBookingDashboard()
# ===== SEÇÃO DE CARREGAMENTO DE DADOS =====
if not dashboard.is_data_loaded:
st.markdown("""
<div class="data-source-section">
<h2>📊 Upload do Dataset</h2>
<p style="font-size: 1.2rem; margin-bottom: 1.5rem;">
<strong>Faça upload do dataset de reservas de hotel para começar a análise</strong>
</p>
</div>
""", 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('<div class="parameter-section">', 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('</div>', unsafe_allow_html=True)
elif algorithm == "KNN":
st.sidebar.markdown('<div class="parameter-section">', 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('</div>', unsafe_allow_html=True)
else: # SVM
st.sidebar.markdown('<div class="parameter-section">', 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('</div>', 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'''
<div class="best-model">
<h3>🎉 Melhor Modelo: {best_model}</h3>
<p><strong>F1-Score:</strong> {best_f1:.4f} | <strong>AUC-ROC:</strong> {best_auc:.4f}</p>
<p>Este modelo apresenta o melhor balanceamento entre precisão e recall.</p>
</div>
''', 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()