import streamlit as st import pandas as pd import numpy as np import plotly.express as px import plotly.graph_objects as go from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.metrics import ( classification_report, confusion_matrix, roc_curve, roc_auc_score, precision_recall_fscore_support, ) from sklearn.linear_model import LogisticRegression from sklearn.neighbors import KNeighborsClassifier from sklearn.svm import SVC from imblearn.over_sampling import SMOTE import time import warnings warnings.filterwarnings("ignore") # --- Configuração da Página --- st.set_page_config( page_title="Dashboard de Previsão de Cancelamento", page_icon="🏨", layout="wide", ) # --- Título e Contexto --- st.title("🏨 Dashboard de Previsão de Cancelamento de Reservas") # --- Funções de Processamento (Otimizadas com Cache) --- @st.cache_data def load_data(file_path): """Carrega o dataset principal. O cache evita recarregar a cada interação.""" try: df = pd.read_csv(file_path) return df except FileNotFoundError: st.error( f"Erro: Arquivo '{file_path}' não encontrado. Faça o upload do arquivo para o seu Hugging Face Space." ) return None @st.cache_data def preprocess_data(df): """Aplica o pré-processamento seguindo as diretrizes da Tarefa 3.""" df_proc = df.copy() # 1. Tratamento de valores faltantes df_proc["country"].fillna(df_proc["country"].mode()[0], inplace=True) df_proc["agent"].fillna(0, inplace=True) df_proc["company"].fillna(0, inplace=True) df_proc["children"].fillna(0, inplace=True) # 2. Tratamento de Outliers (simples, para performance) df_proc = df_proc[(df_proc["adr"] >= 0) & (df_proc["adr"] < 5000)] # 3. Engenharia de Features (simples) df_proc["total_stay"] = ( df_proc["stays_in_weekend_nights"] + df_proc["stays_in_week_nights"] ) df_proc["total_guests"] = ( df_proc["adults"] + df_proc["children"] + df_proc["babies"] ) df_proc = df_proc[df_proc["total_guests"] > 0] # 4. Seleção de Variáveis (Baseado na Tarefa 3 - 8 a 15 features) # Esta seleção é manual para garantir performance e relevância y = df_proc["is_canceled"] numeric_features = [ "lead_time", "total_stay", "total_guests", "adr", "previous_cancellations", "previous_bookings_not_canceled", "booking_changes", "days_in_waiting_list", "total_of_special_requests", ] categorical_features = [ "hotel", "market_segment", "distribution_channel", "deposit_type", "customer_type", "is_repeated_guest", ] all_features = numeric_features + categorical_features df_features = df_proc[all_features] # 5. Codificação de Variáveis Categóricas (Dummies) X = pd.get_dummies(df_features, columns=categorical_features, drop_first=True) return X, y # --- Funções do Modelo --- def get_model(algorithm, params): """Instancia o modelo com base nos parâmetros do usuário.""" if algorithm == "Regressão Logística": model = LogisticRegression( C=params["C_rl"], solver="liblinear", random_state=42, max_iter=1000, ) elif algorithm == "KNN": model = KNeighborsClassifier( n_neighbors=params["k"], metric=params["distance_metric"] ) elif algorithm == "SVM": model = SVC( C=params["C_svm"], kernel=params["kernel"], gamma=params["gamma"] if params["kernel"] == "rbf" else "auto", probability=True, random_state=42, ) return model # --- Funções de Plotagem --- def plot_roc_curve(y_test, y_proba, auc): """Plota a curva ROC usando Plotly.""" fpr, tpr, _ = roc_curve(y_test, y_proba) fig = px.area( x=fpr, y=tpr, title=f"Curva ROC (AUC = {auc:.4f})", labels=dict(x="Taxa de Falsos Positivos", y="Taxa de Verdadeiros Positivos"), width=700, height=500, ) fig.add_shape(type="line", line=dict(dash="dash"), x0=0, x1=1, y0=0, y1=1) fig.update_layout( yaxis_title="Taxa de Verdadeiros Positivos (Sensibilidade)", xaxis_title="Taxa de Falsos Positivos (1 - Especificidade)", ) return fig def plot_confusion_matrix(y_test, y_pred): """Plota a Matriz de Confusão usando Plotly.""" cm = confusion_matrix(y_test, y_pred) fig = px.imshow( cm, labels=dict( x="Previsão do Modelo", y="Valor Real", color="Contagem" ), x=["Não Cancelou (0)", "Cancelou (1)"], y=["Não Cancelou (0)", "Cancelou (1)"], color_continuous_scale="Blues", text_auto=True, ) fig.update_layout( title="Matriz de Confusão", xaxis_title="Previsão do Modelo", yaxis_title="Valor Real", width=600, height=500, ) return fig # --- Configuração da Sidebar (Controles) --- st.sidebar.header("⚙️ Painel de Controle do Analista") df_original = load_data("hotel_bookings.csv") if df_original is not None: # 1. Controles de Amostragem e Divisão st.sidebar.subheader("1. Configuração dos Dados") sample_size = st.sidebar.slider( "Tamanho da Amostra para Treinamento", min_value=1000, max_value=20000, value=3000, step=500, help="Use uma amostra menor para velocidade ou maior para precisão. O dataset completo tem >100k linhas.", ) test_split_pct = st.sidebar.slider( "Percentual de Dados para Teste", min_value=0.1, max_value=0.5, value=0.3, step=0.05, ) use_smote = st.sidebar.checkbox( "Aplicar SMOTE (Corrigir Desbalanceamento)", value=False, help="Pode melhorar o 'Recall', mas aumenta o tempo de treino.", ) # 2. Seleção de Algoritmo st.sidebar.subheader("2. Seleção do Algoritmo") algorithm = st.sidebar.selectbox( "Escolha o Algoritmo", ("Regressão Logística", "KNN", "SVM"), ) # 3. Ajuste de Hiperparâmetros (Dinâmico) st.sidebar.subheader(f"3. Ajuste de Parâmetros ({algorithm})") params = {} if algorithm == "Regressão Logística": params["C_rl"] = st.sidebar.select_slider( "C (Força da Regularização)", options=[0.01, 0.1, 1.0, 10.0, 100.0], value=1.0, help="Valores menores = mais regularização (modelo mais simples).", ) elif algorithm == "KNN": params["k"] = st.sidebar.slider( "k (Número de Vizinhos)", min_value=3, max_value=21, value=5, step=2 ) params["distance_metric"] = st.sidebar.selectbox( "Métrica de Distância", ("euclidean", "manhattan") ) elif algorithm == "SVM": params["kernel"] = st.sidebar.selectbox("Kernel", ("linear", "rbf")) params["C_svm"] = st.sidebar.select_slider( "C (Regularização)", options=[0.1, 1.0, 10.0, 50.0], value=1.0, help="Controla o trade-off entre erro de treino e margem.", ) if params["kernel"] == "rbf": params["gamma"] = st.sidebar.select_slider( "Gamma (Influência do Ponto)", options=[0.001, 0.01, 0.1, 1.0], value=0.1, ) else: params["gamma"] = "auto" # --- Botão de Execução --- st.sidebar.markdown("---") run_button = st.sidebar.button("Executar Análise", type="primary") # --- Área Principal de Exibição --- if run_button: with st.spinner( f"Executando pipeline para {algorithm} com {sample_size} amostras..." ): start_time = time.time() # 1. Amostrar df_sample = df_original.sample(n=sample_size, random_state=42) # 2. Pré-processar X, y = preprocess_data(df_sample) # Captura os nomes das features APÓS o get_dummies feature_names = X.columns.tolist() # 3. Dividir (Train/Test) X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=test_split_pct, random_state=42, stratify=y ) # 4. Escalonar scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test) # 5. Aplicar SMOTE (Opcional) if use_smote: smote = SMOTE(random_state=42) X_train_scaled, y_train = smote.fit_resample(X_train_scaled, y_train) # 6. Treinar Modelo model = get_model(algorithm, params) model.fit(X_train_scaled, y_train) # 7. Avaliar y_pred = model.predict(X_test_scaled) y_proba = model.predict_proba(X_test_scaled)[:, 1] auc = roc_auc_score(y_test, y_proba) report = classification_report(y_test, y_pred, output_dict=True) report_df = pd.DataFrame(report).transpose() ( precision, recall, f1_score, _, ) = precision_recall_fscore_support(y_test, y_pred, average="binary") end_time = time.time() training_time = end_time - start_time # --- Exibição dos Resultados --- st.header(f"Resultados para: {algorithm}") # Métricas Chave st.subheader("Visão Geral das Métricas (Classe 1: 'Cancelou')") col1, col2, col3, col4 = st.columns(4) col1.metric("AUC (Area Under Curve)", f"{auc:.3f}") col2.metric("F1-Score", f"{f1_score:.3f}") col3.metric("Precisão (Precision)", f"{precision:.3f}") col4.metric("Recall (Sensibilidade)", f"{recall:.3f}") st.markdown(f"**Tempo de Treinamento e Avaliação:** {training_time:.2f} segundos") # Gráficos st.subheader("Visualização das Métricas") fig_roc = plot_roc_curve(y_test, y_proba, auc) fig_cm = plot_confusion_matrix(y_test, y_pred) col_graph1, col_graph2 = st.columns(2) with col_graph1: st.plotly_chart(fig_roc, use_container_width=True) with col_graph2: st.plotly_chart(fig_cm, use_container_width=True) st.subheader("Relatório de Classificação Detalhado") st.dataframe(report_df.style.format("{:.3f}")) # Interpretação específica da Regressão Logística if algorithm == "Regressão Logística": st.subheader("Análise de Coeficientes (Interpretabilidade)") coefs = model.coef_[0] odds_ratios = np.exp(coefs) df_coef = pd.DataFrame({ 'Variável': feature_names, 'Coeficiente (Log-Odds)': coefs, 'Odds Ratio (Razão de Chances)': odds_ratios }) # ***** [LINHA CORRIGIDA] ***** # O nome da coluna no 'by=' agora bate com o nome da coluna no DataFrame df_coef = df_coef.sort_values(by="Odds Ratio (Razão de Chances)", ascending=False) st.dataframe(df_coef.style.format({ 'Coeficiente (Log-Odds)': '{:.4f}', 'Odds Ratio (Razão de Chances)': '{:.3f}' }).background_gradient( cmap='RdBu_r', subset=['Odds Ratio (Razão de Chances)', 'Coeficiente (Log-Odds)']) ) st.markdown(""" **Como interpretar esta tabela:** * **Odds Ratio > 1 (Azul):** Aumenta a chance de cancelamento. * *Exemplo: Se `lead_time` tem Odds Ratio de 1.02, cada dia extra de antecedência aumenta a chance de cancelar em 2%.* * **Odds Ratio < 1 (Vermelho):** Diminui a chance de cancelamento (fator de proteção). * *Exemplo: Se `deposit_type_Non Refund` tem Odds Ratio de 0.20, ter um depósito não-reembolsável reduz a chance de cancelar em 80%.* * **Odds Ratio = 1:** Não tem efeito. """) # --- Interpretação Gerencial Automática --- st.header("💡 Interpretação Gerencial e Recomendações") st.subheader(f"Análise Gerencial do Modelo: {algorithm}") if algorithm == "Regressão Logística": st.markdown(""" **O que é?** Um modelo estatístico que calcula a *probabilidade* de cancelamento. É o modelo mais fácil de interpretar. **Ponto Forte (Interpretabilidade):** Como visto na tabela acima, podemos ver exatamente quais fatores (como `lead_time` ou `deposit_type`) mais aumentam ou diminuem as chances de cancelamento. **Ponto Fraco:** Pode não capturar relações complexas entre as variáveis. """) elif algorithm == "KNN": st.markdown(""" **O que é?** Um modelo que classifica uma nova reserva com base nas reservas mais *parecidas* (vizinhas) que já temos no histórico. **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. **Ponto Fraco (Performance):** Lento para prever em datasets muito grandes e muito sensível ao escalonamento dos dados e a features irrelevantes. """) elif algorithm == "SVM": st.markdown(""" **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. **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. **Ponto Fraco (Caixa Preta):** É muito difícil de explicar *por que* o modelo tomou uma decisão específica. """) st.subheader("Tradução das Métricas para o Negócio Hoteleiro") st.markdown(f""" * **Precisão (Precision) = {precision:.2f}:** Das reservas que o modelo *disse* que iriam cancelar, **{precision*100:.1f}%** realmente cancelariam. * *Impacto:* Uma Precisão alta evita que a equipe de retenção perca tempo com clientes que não iriam cancelar. * **Recall (Sensibilidade) = {recall:.2f}:** Das reservas que *realmente* foram canceladas, o modelo conseguiu identificar **{recall*100:.1f}%** delas. * *Impacto:* Este é o custo de "deixar passar". Um Recall baixo significa que muitos cancelamentos estão ocorrendo sem aviso prévio. * **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. """) st.subheader("Ranking e Recomendações (Visão Geral)") st.markdown(""" A "melhor" escolha depende da estratégia da rede hoteleira: 1. **Para Interpretabilidade (Entender o *Porquê*):** * **Vencedor:** **Regressão Logística**. * **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. 2. **Para Ação Preventiva (Maximizar o *Recall*):** * **Vencedor:** Geralmente **SVM** ou **KNN** (com SMOTE) podem ser ajustados para um Recall mais alto. * **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. 3. **Para Eficiência Operacional (Maximizar a *Precisão*):** * **Vencedor:** Geralmente **Regressão Logística** ou **SVM (linear)**. * **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**. """) else: st.warning("O arquivo 'hotel_bookings.csv' não foi carregado. O dashboard não pode continuar.")