| |
| 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') |
|
|
| |
| 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 |
|
|
| |
| try: |
| from datasets import load_dataset |
| DATASETS_AVAILABLE = True |
| except ImportError as e: |
| st.error(f"❌ Biblioteca 'datasets' não disponível: {e}") |
| DATASETS_AVAILABLE = False |
|
|
| |
| st.set_page_config( |
| page_title="Dashboard - Cancelamento de Reservas", |
| page_icon="🏨", |
| layout="wide", |
| initial_sidebar_state="expanded" |
| ) |
|
|
| |
| 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_huggingface_dataset(self): |
| """Carrega o dataset do Hugging Face""" |
| try: |
| with st.spinner("🌐 Carregando dataset do Hugging Face..."): |
| |
| dataset = load_dataset("SIEP/hotel_bookings") |
| |
| |
| if 'train' in dataset: |
| df = dataset['train'].to_pandas() |
| else: |
| |
| first_split = list(dataset.keys())[0] |
| df = dataset[first_split].to_pandas() |
| |
| st.success(f"✅ Dataset carregado: {df.shape[0]} linhas × {df.shape[1]} colunas") |
| return df |
| |
| except Exception as e: |
| st.error(f"❌ Erro ao carregar dataset do Hugging Face: {str(e)}") |
| return None |
| |
| def load_and_preprocess_data(self, df): |
| """Carrega e pré-processa o dataset""" |
| try: |
| st.info("🔄 Iniciando pré-processamento dos dados...") |
| |
| |
| df_clean = df.copy() |
| |
| |
| 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}'") |
| |
| |
| df_clean = self._handle_missing_values(df_clean) |
| |
| |
| df_encoded = self._encode_categorical_variables(df_clean) |
| |
| |
| X = df_encoded.drop(columns=[target_col]) |
| y = df_encoded[target_col] |
| |
| |
| 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: |
| |
| if candidate != 'is_canceled': |
| df.rename(columns={candidate: 'is_canceled'}, inplace=True) |
| return 'is_canceled' |
| |
| |
| 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() |
| |
| |
| if 'company' in df_clean.columns: |
| df_clean.drop('company', axis=1, inplace=True) |
| |
| |
| for col in df_clean.columns: |
| if df_clean[col].isnull().sum() > 0: |
| if df_clean[col].dtype == 'object': |
| |
| df_clean[col].fillna(df_clean[col].mode()[0], inplace=True) |
| else: |
| |
| 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() |
| |
| |
| categorical_cols = df_encoded.select_dtypes(include=['object']).columns.tolist() |
| |
| if categorical_cols: |
| st.info(f"📊 Codificando {len(categorical_cols)} variáveis categóricas...") |
| |
| |
| 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)) |
| |
| |
| 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: |
| |
| X = X.apply(pd.to_numeric, errors='coerce').fillna(0) |
| |
| |
| X_train, X_test, y_train, y_test = train_test_split( |
| X, y, test_size=0.3, random_state=42, stratify=y |
| ) |
| |
| |
| 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.") |
| |
| |
| 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 |
| } |
| |
| |
| fpr, tpr, _ = roc_curve(self.y_test, y_proba) |
| roc_data = {'fpr': fpr, 'tpr': tpr, 'auc': metrics['AUC-ROC']} |
| |
| |
| 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() |
| |
| |
| 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') |
| )) |
| |
| |
| 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 |
| |
| |
| 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(): |
| |
| st.markdown('<h1 class="main-header">🏨 Dashboard - Cancelamento de Reservas</h1>', |
| unsafe_allow_html=True) |
| |
| |
| dashboard = HotelBookingDashboard() |
| |
| |
| if not dashboard.is_data_loaded: |
| st.markdown(""" |
| <div class="data-source-section"> |
| <h2>📊 Escolha a Fonte dos Dados</h2> |
| <p style="font-size: 1.2rem; margin-bottom: 1.5rem;"> |
| <strong>Carregue os dados do Hugging Face ou faça upload do seu próprio dataset</strong> |
| </p> |
| </div> |
| """, unsafe_allow_html=True) |
| |
| |
| col1, col2 = st.columns(2) |
| |
| with col1: |
| st.markdown("### 🌐 Dataset do Hugging Face") |
| st.markdown(""" |
| **Vantagens:** |
| - Dataset pré-processado |
| - Estrutura consistente |
| - Sem necessidade de upload |
| """) |
| |
| if DATASETS_AVAILABLE: |
| if st.button("🚀 Carregar do Hugging Face", type="primary", use_container_width=True): |
| with st.spinner("Carregando dataset SIEP/hotel_bookings..."): |
| df = dashboard.load_huggingface_dataset() |
| if df is not None: |
| 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("Biblioteca 'datasets' não disponível") |
| st.info("Adicione 'datasets' ao requirements.txt") |
| |
| with col2: |
| st.markdown("### 📁 Upload Manual") |
| st.markdown(""" |
| **Use seu próprio dataset:** |
| - Formato CSV |
| - Coluna target: 'is_canceled' |
| - Estrutura personalizada |
| """) |
| |
| uploaded_file = st.file_uploader( |
| "Selecione o arquivo CSV", |
| type=['csv'], |
| help="Faça upload do dataset de reservas de hotel" |
| ) |
| |
| if uploaded_file is not None: |
| try: |
| with st.spinner("Carregando arquivo..."): |
| df = pd.read_csv(uploaded_file) |
| st.success(f"✅ Dataset carregado: {df.shape[0]} linhas × {df.shape[1]} colunas") |
| |
| |
| with st.expander("👀 Visualizar Dataset"): |
| st.dataframe(df.head(10)) |
| |
| if st.button("🔄 Processar Dataset", type="primary", use_container_width=True): |
| success = dashboard.load_and_preprocess_data(df) |
| if success: |
| st.session_state.data_processed = True |
| st.session_state.dashboard = dashboard |
| st.rerun() |
| |
| except Exception as e: |
| st.error(f"❌ Erro ao carregar arquivo: {str(e)}") |
| |
| |
| with st.expander("📋 Sobre o Dataset", expanded=True): |
| st.markdown(""" |
| **Dataset: Hotel Bookings (SIEP/hotel_bookings)** |
| |
| Este dataset contém informações de reservas de hotel incluindo: |
| |
| **Variáveis Principais:** |
| - `is_canceled`: Indicador de cancelamento (target) |
| - `lead_time`: Tempo entre reserva e chegada |
| - `adr`: Taxa diária média |
| - `adults`, `children`, `babies`: Número de hóspedes |
| - `country`, `market_segment`: Informações demográficas |
| - `previous_cancellations`: Histórico de cancelamentos |
| - `booking_changes`: Número de mudanças na reserva |
| |
| **Objetivo:** Prever se uma reserva será cancelada com base nas características da reserva. |
| """) |
| |
| return |
| |
| |
| |
| |
| if 'dashboard' in st.session_state: |
| dashboard = st.session_state.dashboard |
| |
| |
| st.sidebar.header("⚙️ Configurações do Modelo") |
| |
| |
| algorithm = st.sidebar.selectbox( |
| "Escolha o algoritmo:", |
| ["Regressão Logística", "KNN", "SVM"], |
| index=0 |
| ) |
| |
| |
| 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: |
| 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) |
| |
| |
| train_button = st.sidebar.button("🚀 Treinar Modelo", type="primary", use_container_width=True) |
| |
| |
| 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() |
| |
| |
| 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)) |
| |
| |
| with st.expander("🔍 Análise Exploratória dos Dados"): |
| col1, col2 = st.columns(2) |
| |
| with col1: |
| |
| fig, ax = plt.subplots(figsize=(8, 6)) |
| balance = pd.Series(dashboard.y_train).value_counts() |
| ax.pie(balance.values, labels=['Não Cancelado', 'Cancelado'], autopct='%1.1f%%', startangle=90) |
| ax.set_title('Distribuição de Cancelamentos') |
| st.pyplot(fig) |
| |
| with col2: |
| |
| st.write("**Estatísticas do Dataset:**") |
| stats_df = pd.DataFrame({ |
| 'Métrica': ['Total de Amostras', 'Features', 'Taxa de Cancelamento', 'Balanceamento'], |
| 'Valor': [ |
| f"{dashboard.X_train.shape[0] + dashboard.X_test.shape[0]:,}", |
| f"{dashboard.X_train.shape[1]}", |
| f"{(dashboard.y_train.sum() + dashboard.y_test.sum()) / (len(dashboard.y_train) + len(dashboard.y_test)) * 100:.1f}%", |
| f"{balance[0]}:{balance[1]}" if len(balance) == 2 else "Múltiplas classes" |
| ] |
| }) |
| st.dataframe(stats_df, hide_index=True) |
| |
| |
| if train_button: |
| with st.spinner(f"Treinando modelo {algorithm}..."): |
| |
| 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: |
| model, training_time = dashboard.train_svm( |
| C=C_svm, kernel=kernel, gamma=gamma |
| ) |
| model_name = f"SVM_{kernel}_C={C_svm}" |
| |
| |
| metrics, roc_data, cm = dashboard.evaluate_model(model, model_name, training_time) |
| |
| |
| dashboard.models[model_name] = model |
| dashboard.results[model_name] = metrics |
| |
| |
| st.success(f"✅ Modelo {algorithm} treinado com sucesso em {training_time:.2f} segundos!") |
| |
| |
| 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}") |
| |
| |
| st.subheader("📈 Visualizações") |
| col1, col2 = st.columns(2) |
| |
| with col1: |
| |
| roc_fig = dashboard.plot_roc_comparison(roc_data, model_name) |
| st.plotly_chart(roc_fig, use_container_width=True) |
| |
| with col2: |
| |
| 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) |
| |
| |
| 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.") |
| |
| |
| 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) |
| |
| |
| st.dataframe(results_df.style.format("{:.4f}").background_gradient(cmap='Blues'), |
| use_container_width=True) |
| |
| |
| 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: |
| |
| 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() |