|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
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_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>📊 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) |
|
|
|
|
|
|
|
|
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" |
|
|
) |
|
|
|
|
|
|
|
|
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 |
|
|
""") |
|
|
|
|
|
|
|
|
if uploaded_file is not None: |
|
|
try: |
|
|
with st.spinner("📊 Carregando e analisando o dataset..."): |
|
|
|
|
|
df = pd.read_csv(uploaded_file) |
|
|
|
|
|
|
|
|
st.success(f"✅ Dataset carregado: {df.shape[0]} linhas × {df.shape[1]} colunas") |
|
|
|
|
|
|
|
|
with st.expander("👀 Visualização do Dataset (primeiras 10 linhas)"): |
|
|
st.dataframe(df.head(10), use_container_width=True) |
|
|
|
|
|
|
|
|
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]: |
|
|
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]: |
|
|
st.write(f"- {col}") |
|
|
if len(categorical_cols) > 10: |
|
|
st.write(f"- ... e mais {len(categorical_cols) - 10} colunas") |
|
|
|
|
|
|
|
|
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.") |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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() |
|
|
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: |
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
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() |