brunaaaz's picture
Update app.py
22ca76f verified
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.")