Update app.py
Browse files
app.py
CHANGED
|
@@ -30,7 +30,12 @@ st.set_page_config(
|
|
| 30 |
|
| 31 |
# --- Título e Contexto ---
|
| 32 |
st.title("🏨 Dashboard de Previsão de Cancelamento de Reservas")
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
|
| 36 |
# --- Funções de Processamento (Otimizadas com Cache) ---
|
|
@@ -53,35 +58,27 @@ def preprocess_data(df):
|
|
| 53 |
df_proc = df.copy()
|
| 54 |
|
| 55 |
# 1. Tratamento de valores faltantes
|
| 56 |
-
# Preenche 'country' com a moda (mais comum)
|
| 57 |
df_proc["country"].fillna(df_proc["country"].mode()[0], inplace=True)
|
| 58 |
-
# Para 'agent' e 'company', NaN significa "Não Aplicável" ou "Direto". Substituímos por 0.
|
| 59 |
df_proc["agent"].fillna(0, inplace=True)
|
| 60 |
df_proc["company"].fillna(0, inplace=True)
|
| 61 |
-
# Assume que NaN em 'children' significa 0 crianças.
|
| 62 |
df_proc["children"].fillna(0, inplace=True)
|
| 63 |
|
| 64 |
# 2. Tratamento de Outliers (simples, para performance)
|
| 65 |
-
# Remove 'adr' (Average Daily Rate) irrealista
|
| 66 |
df_proc = df_proc[(df_proc["adr"] >= 0) & (df_proc["adr"] < 5000)]
|
| 67 |
|
| 68 |
# 3. Engenharia de Features (simples)
|
| 69 |
-
# Cria 'total_stay' e 'total_guests'
|
| 70 |
df_proc["total_stay"] = (
|
| 71 |
df_proc["stays_in_weekend_nights"] + df_proc["stays_in_week_nights"]
|
| 72 |
)
|
| 73 |
df_proc["total_guests"] = (
|
| 74 |
df_proc["adults"] + df_proc["children"] + df_proc["babies"]
|
| 75 |
)
|
| 76 |
-
|
| 77 |
-
# Remove hóspedes com 0 pessoas (inválido)
|
| 78 |
df_proc = df_proc[df_proc["total_guests"] > 0]
|
| 79 |
|
| 80 |
# 4. Seleção de Variáveis (Baseado na Tarefa 3 - 8 a 15 features)
|
| 81 |
-
#
|
| 82 |
y = df_proc["is_canceled"]
|
| 83 |
|
| 84 |
-
# Features Numéricas
|
| 85 |
numeric_features = [
|
| 86 |
"lead_time",
|
| 87 |
"total_stay",
|
|
@@ -94,7 +91,6 @@ def preprocess_data(df):
|
|
| 94 |
"total_of_special_requests",
|
| 95 |
]
|
| 96 |
|
| 97 |
-
# Features Categóricas
|
| 98 |
categorical_features = [
|
| 99 |
"hotel",
|
| 100 |
"market_segment",
|
|
@@ -104,7 +100,6 @@ def preprocess_data(df):
|
|
| 104 |
"is_repeated_guest",
|
| 105 |
]
|
| 106 |
|
| 107 |
-
# Garante que todas as colunas existem
|
| 108 |
all_features = numeric_features + categorical_features
|
| 109 |
df_features = df_proc[all_features]
|
| 110 |
|
|
@@ -120,7 +115,7 @@ def get_model(algorithm, params):
|
|
| 120 |
if algorithm == "Regressão Logística":
|
| 121 |
model = LogisticRegression(
|
| 122 |
C=params["C_rl"],
|
| 123 |
-
solver="liblinear",
|
| 124 |
random_state=42,
|
| 125 |
max_iter=1000,
|
| 126 |
)
|
|
@@ -133,7 +128,7 @@ def get_model(algorithm, params):
|
|
| 133 |
C=params["C_svm"],
|
| 134 |
kernel=params["kernel"],
|
| 135 |
gamma=params["gamma"] if params["kernel"] == "rbf" else "auto",
|
| 136 |
-
probability=True,
|
| 137 |
random_state=42,
|
| 138 |
)
|
| 139 |
return model
|
|
@@ -162,7 +157,6 @@ def plot_roc_curve(y_test, y_proba, auc):
|
|
| 162 |
def plot_confusion_matrix(y_test, y_pred):
|
| 163 |
"""Plota a Matriz de Confusão usando Plotly."""
|
| 164 |
cm = confusion_matrix(y_test, y_pred)
|
| 165 |
-
cm_text = [[str(y) for y in x] for x in cm]
|
| 166 |
|
| 167 |
fig = px.imshow(
|
| 168 |
cm,
|
|
@@ -274,6 +268,9 @@ if df_original is not None:
|
|
| 274 |
|
| 275 |
# 2. Pré-processar
|
| 276 |
X, y = preprocess_data(df_sample)
|
|
|
|
|
|
|
|
|
|
| 277 |
|
| 278 |
# 3. Dividir (Train/Test)
|
| 279 |
X_train, X_test, y_train, y_test = train_test_split(
|
|
@@ -281,6 +278,8 @@ if df_original is not None:
|
|
| 281 |
)
|
| 282 |
|
| 283 |
# 4. Escalonar (MUITO importante para KNN e SVM)
|
|
|
|
|
|
|
| 284 |
scaler = StandardScaler()
|
| 285 |
X_train_scaled = scaler.fit_transform(X_train)
|
| 286 |
X_test_scaled = scaler.transform(X_test)
|
|
@@ -301,7 +300,6 @@ if df_original is not None:
|
|
| 301 |
report = classification_report(y_test, y_pred, output_dict=True)
|
| 302 |
report_df = pd.DataFrame(report).transpose()
|
| 303 |
|
| 304 |
-
# Extrai métricas específicas para classe 1 (Cancelamento)
|
| 305 |
(
|
| 306 |
precision,
|
| 307 |
recall,
|
|
@@ -339,6 +337,42 @@ if df_original is not None:
|
|
| 339 |
|
| 340 |
st.subheader("Relatório de Classificação Detalhado")
|
| 341 |
st.dataframe(report_df.style.format("{:.3f}"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 342 |
|
| 343 |
# --- Interpretação Gerencial Automática ---
|
| 344 |
st.header("💡 Interpretação Gerencial e Recomendações")
|
|
@@ -348,7 +382,7 @@ if df_original is not None:
|
|
| 348 |
if algorithm == "Regressão Logística":
|
| 349 |
st.markdown("""
|
| 350 |
**O que é?** Um modelo estatístico que calcula a *probabilidade* de cancelamento. É o modelo mais fácil de interpretar.
|
| 351 |
-
**Ponto Forte (Interpretabilidade):**
|
| 352 |
**Ponto Fraco:** Pode não capturar relações complexas entre as variáveis.
|
| 353 |
""")
|
| 354 |
elif algorithm == "KNN":
|
|
@@ -390,9 +424,6 @@ if df_original is not None:
|
|
| 390 |
3. **Para Eficiência Operacional (Maximizar a *Precisão*):**
|
| 391 |
* **Vencedor:** Geralmente **Regressão Logística** ou **SVM (linear)**.
|
| 392 |
* **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**.
|
| 393 |
-
|
| 394 |
-
**Recomendação Prática (Exemplo):**
|
| 395 |
-
"O modelo de Regressão Logística (AUC de ~0.85) mostrou que reservas do tipo 'Transient' (não-grupo) com `deposit_type` = 'Non Refund' e `lead_time` > 120 dias têm 70% mais chance de cancelar. Recomenda-se uma política de overbooking de 3% para esse segmento específico ou um contato proativo 60 dias antes do check-in."
|
| 396 |
""")
|
| 397 |
|
| 398 |
else:
|
|
|
|
| 30 |
|
| 31 |
# --- Título e Contexto ---
|
| 32 |
st.title("🏨 Dashboard de Previsão de Cancelamento de Reservas")
|
| 33 |
+
st.markdown(
|
| 34 |
+
"""
|
| 35 |
+
**Sua Missão como Analista de Dados:**
|
| 36 |
+
Você é analista de dados em uma rede internacional de hotéis. Sua missão é desenvolver e comparar três modelos preditivos (Regressão Logística, KNN e SVM) capazes de identificar antecipadamente as reservas com maior probabilidade de cancelamento. Esta ferramenta permite simular esse processo de forma interativa.
|
| 37 |
+
"""
|
| 38 |
+
)
|
| 39 |
|
| 40 |
|
| 41 |
# --- Funções de Processamento (Otimizadas com Cache) ---
|
|
|
|
| 58 |
df_proc = df.copy()
|
| 59 |
|
| 60 |
# 1. Tratamento de valores faltantes
|
|
|
|
| 61 |
df_proc["country"].fillna(df_proc["country"].mode()[0], inplace=True)
|
|
|
|
| 62 |
df_proc["agent"].fillna(0, inplace=True)
|
| 63 |
df_proc["company"].fillna(0, inplace=True)
|
|
|
|
| 64 |
df_proc["children"].fillna(0, inplace=True)
|
| 65 |
|
| 66 |
# 2. Tratamento de Outliers (simples, para performance)
|
|
|
|
| 67 |
df_proc = df_proc[(df_proc["adr"] >= 0) & (df_proc["adr"] < 5000)]
|
| 68 |
|
| 69 |
# 3. Engenharia de Features (simples)
|
|
|
|
| 70 |
df_proc["total_stay"] = (
|
| 71 |
df_proc["stays_in_weekend_nights"] + df_proc["stays_in_week_nights"]
|
| 72 |
)
|
| 73 |
df_proc["total_guests"] = (
|
| 74 |
df_proc["adults"] + df_proc["children"] + df_proc["babies"]
|
| 75 |
)
|
|
|
|
|
|
|
| 76 |
df_proc = df_proc[df_proc["total_guests"] > 0]
|
| 77 |
|
| 78 |
# 4. Seleção de Variáveis (Baseado na Tarefa 3 - 8 a 15 features)
|
| 79 |
+
# Esta seleção é manual para garantir performance e relevância
|
| 80 |
y = df_proc["is_canceled"]
|
| 81 |
|
|
|
|
| 82 |
numeric_features = [
|
| 83 |
"lead_time",
|
| 84 |
"total_stay",
|
|
|
|
| 91 |
"total_of_special_requests",
|
| 92 |
]
|
| 93 |
|
|
|
|
| 94 |
categorical_features = [
|
| 95 |
"hotel",
|
| 96 |
"market_segment",
|
|
|
|
| 100 |
"is_repeated_guest",
|
| 101 |
]
|
| 102 |
|
|
|
|
| 103 |
all_features = numeric_features + categorical_features
|
| 104 |
df_features = df_proc[all_features]
|
| 105 |
|
|
|
|
| 115 |
if algorithm == "Regressão Logística":
|
| 116 |
model = LogisticRegression(
|
| 117 |
C=params["C_rl"],
|
| 118 |
+
solver="liblinear",
|
| 119 |
random_state=42,
|
| 120 |
max_iter=1000,
|
| 121 |
)
|
|
|
|
| 128 |
C=params["C_svm"],
|
| 129 |
kernel=params["kernel"],
|
| 130 |
gamma=params["gamma"] if params["kernel"] == "rbf" else "auto",
|
| 131 |
+
probability=True,
|
| 132 |
random_state=42,
|
| 133 |
)
|
| 134 |
return model
|
|
|
|
| 157 |
def plot_confusion_matrix(y_test, y_pred):
|
| 158 |
"""Plota a Matriz de Confusão usando Plotly."""
|
| 159 |
cm = confusion_matrix(y_test, y_pred)
|
|
|
|
| 160 |
|
| 161 |
fig = px.imshow(
|
| 162 |
cm,
|
|
|
|
| 268 |
|
| 269 |
# 2. Pré-processar
|
| 270 |
X, y = preprocess_data(df_sample)
|
| 271 |
+
|
| 272 |
+
# **NOVO**: Captura os nomes das features APÓS o get_dummies
|
| 273 |
+
feature_names = X.columns.tolist()
|
| 274 |
|
| 275 |
# 3. Dividir (Train/Test)
|
| 276 |
X_train, X_test, y_train, y_test = train_test_split(
|
|
|
|
| 278 |
)
|
| 279 |
|
| 280 |
# 4. Escalonar (MUITO importante para KNN e SVM)
|
| 281 |
+
# Nota: RL com 'liblinear' não precisa de escalonamento,
|
| 282 |
+
# mas vamos manter para consistência e performance.
|
| 283 |
scaler = StandardScaler()
|
| 284 |
X_train_scaled = scaler.fit_transform(X_train)
|
| 285 |
X_test_scaled = scaler.transform(X_test)
|
|
|
|
| 300 |
report = classification_report(y_test, y_pred, output_dict=True)
|
| 301 |
report_df = pd.DataFrame(report).transpose()
|
| 302 |
|
|
|
|
| 303 |
(
|
| 304 |
precision,
|
| 305 |
recall,
|
|
|
|
| 337 |
|
| 338 |
st.subheader("Relatório de Classificação Detalhado")
|
| 339 |
st.dataframe(report_df.style.format("{:.3f}"))
|
| 340 |
+
|
| 341 |
+
# --- [NOVA SEÇÃO ADICIONADA] ---
|
| 342 |
+
# Interpretação específica da Regressão Logística
|
| 343 |
+
if algorithm == "Regressão Logística":
|
| 344 |
+
st.subheader("Análise de Coeficientes (Interpretabilidade)")
|
| 345 |
+
|
| 346 |
+
# Captura coeficientes e odds ratios
|
| 347 |
+
coefs = model.coef_[0]
|
| 348 |
+
odds_ratios = np.exp(coefs)
|
| 349 |
+
|
| 350 |
+
df_coef = pd.DataFrame({
|
| 351 |
+
'Variável': feature_names,
|
| 352 |
+
'Coeficiente (Log-Odds)': coefs,
|
| 353 |
+
'Odds Ratio (Razão de Chances)': odds_ratios
|
| 354 |
+
})
|
| 355 |
+
|
| 356 |
+
df_coef = df_coef.sort_values(by="Odds Ratio", ascending=False)
|
| 357 |
+
|
| 358 |
+
st.dataframe(df_coef.style.format({
|
| 359 |
+
'Coeficiente (Log-Odds)': '{:.4f}',
|
| 360 |
+
'Odds Ratio (Razão de Chances)': '{:.3f}'
|
| 361 |
+
}).background_gradient(
|
| 362 |
+
cmap='RdBu_r',
|
| 363 |
+
subset=['Odds Ratio', 'Coeficiente (Log-Odds)'])
|
| 364 |
+
)
|
| 365 |
+
|
| 366 |
+
st.markdown("""
|
| 367 |
+
**Como interpretar esta tabela:**
|
| 368 |
+
* **Odds Ratio > 1 (Azul):** Aumenta a chance de cancelamento.
|
| 369 |
+
* *Exemplo: Se `lead_time` tem Odds Ratio de 1.02, cada dia extra de antecedência aumenta a chance de cancelar em 2%.*
|
| 370 |
+
* **Odds Ratio < 1 (Vermelho):** Diminui a chance de cancelamento (fator de proteção).
|
| 371 |
+
* *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%.*
|
| 372 |
+
* **Odds Ratio = 1:** Não tem efeito.
|
| 373 |
+
""")
|
| 374 |
+
# --- [FIM DA NOVA SEÇÃO] ---
|
| 375 |
+
|
| 376 |
|
| 377 |
# --- Interpretação Gerencial Automática ---
|
| 378 |
st.header("💡 Interpretação Gerencial e Recomendações")
|
|
|
|
| 382 |
if algorithm == "Regressão Logística":
|
| 383 |
st.markdown("""
|
| 384 |
**O que é?** Um modelo estatístico que calcula a *probabilidade* de cancelamento. É o modelo mais fácil de interpretar.
|
| 385 |
+
**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.
|
| 386 |
**Ponto Fraco:** Pode não capturar relações complexas entre as variáveis.
|
| 387 |
""")
|
| 388 |
elif algorithm == "KNN":
|
|
|
|
| 424 |
3. **Para Eficiência Operacional (Maximizar a *Precisão*):**
|
| 425 |
* **Vencedor:** Geralmente **Regressão Logística** ou **SVM (linear)**.
|
| 426 |
* **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**.
|
|
|
|
|
|
|
|
|
|
| 427 |
""")
|
| 428 |
|
| 429 |
else:
|