File size: 17,309 Bytes
8854694 4015ccf 8854694 4015ccf 5f48d79 8854694 4015ccf 8854694 4015ccf 8854694 5f48d79 8854694 4015ccf 5f48d79 4015ccf ce82fea 4015ccf ce82fea 4015ccf 8854694 4015ccf ce82fea 4015ccf 8854694 4015ccf 5f48d79 4015ccf 5f48d79 4015ccf 5f48d79 4015ccf 5f48d79 4015ccf 5f48d79 4015ccf 5f48d79 4015ccf 5f48d79 4015ccf ce82fea 5666bcc ce82fea 4015ccf 5666bcc 4015ccf ce82fea 5666bcc ce82fea 5666bcc ce82fea 4015ccf 5f48d79 4015ccf ce82fea 4015ccf 5f48d79 4015ccf 5f48d79 4015ccf 5f48d79 4015ccf |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 |
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.") |