<- read.csv("Hotel Reservations.csv")
dt #head(dt)
datatable(dt, options = list(scrollX = TRUE))
Analiza i przewidywanie anulowania rezerwacji hotelowych
Internetowe kanały rezerwacji hotelowych całkowicie zmieniły sposób dokonywania rezerwacji oraz zachowania klientów. Duża część rezerwacji jest anulowana lub kończy się tzw. no-show, czyli niepojawieniem się gościa w hotelu. Najczęstsze powody anulowania to zmiana planów, kolizje w terminarzu lub inne nieprzewidziane okoliczności.
Często ułatwieniem jest możliwość bezpłatnego lub taniego anulowania rezerwacji, co z punktu widzenia gościa jest wygodne i korzystne. Dla hoteli jednak stanowi to wyzwanie, ponieważ negatywnie wpływa na przychody i utrudnia efektywne zarządzanie dostępnością pokoi.
1 Cel projektu
Celem projektu jest zbudowanie modelu klasyfikacyjnego, który będzie przewidywał, czy dana rezerwacja w hotelu zostanie anulowana.
2 Zbiór danych
2.1 Opis zbioru
Źródło: https://www.kaggle.com/datasets/ahsan81/hotel-reservations-classification-dataset
Zbiór danych zawiera informacje o 36 275 rezerwacjach hotelowych. Każdy rekord reprezentuje pojedynczą rezerwację. Zmienną docelową (target) jest booking_status
, która przyjmuje wartości “Canceled” (anulowana) lub “Not_Canceled” (nieanulowana).
Zbiór zawiera 19 zmiennych:
Opis zmiennych | ||||
---|---|---|---|---|
Zmienna | Opis | Typ | Zakres | Uwagi |
Booking_ID | Unikalny identyfikator każdej rezerwacji | character | INN00001-INN36275 | - |
no_of_adults | Liczba dorosłych | numeric | 0 - 4 | - |
no_of_children | Liczba dzieci | numeric | 0 – 10 | - |
no_of_weekend_nights | Liczba nocy weekendowych (sobota lub niedziela), w których gość zatrzymał się lub zarezerwował pobyt w hotelu | numeric | 0-7 | - |
no_of_week_nights | Liczba nocy w tygodniu (od poniedziałku do piątku), w których gość zatrzymał się lub zarezerwował pobyt w hotelu | numeric | 0 - 17 | - |
type_of_meal_plan | Rodzaj planu posiłków zarezerwowanego przez klienta | factor | Meal Plan 1, Meal Plan 2, Meal Plan 3, Not Selected | 4 poziomy |
required_car_parking_space | Czy klient potrzebuje miejsca parkingowego? | factor | 0, 1 | 0 = Nie, 1 = Tak |
room_type_reserved | Rodzaj pokoju zarezerwowanego przez klienta. Wartości są szyfrowane (kodowane) przez INN Hotels. | factor | Room_Type 1, Room_Type 2, Room_Type 3, Room_Type 4, Room_Type 5, Room_Type 6, Room_Type 7 | 7 poziomów |
lead_time | Liczba dni między datą rezerwacji a datą przyjazdu | numeric | 0–460 | - |
arrival_year | Rok daty przyjazdu | factor | 2017, 2018 | 2 poziomy |
arrival_month | Miesiąc daty przyjazdu | factor | 1-12 | 1-styczeń, 2-luty, 3-marzec, 4-kwiecień, 5-maj, 6-czerwiec, 7-lipiec, 8-sierpień, 9-wrzesień, 10-październik, 11-listopad, 12-grudzień |
arrival_date | Dzień miesiąca daty przyjazdu | numeric | 1-31 | - |
market_segment_type | Oznaczenie segmentu rynku | factor | Aviation Complementary, Corporate, Offline, Online | 5 poziomów |
repeated_guest | Czy klient jest powracającym gościem | factor | 0, 1 | 0 = Nie, 1 = Tak |
no_of_previous_cancellations | Liczba poprzednich rezerwacji, które zostały anulowane przez klienta przed obecną rezerwacją | numeric | 0–13 | - |
no_of_previous_bookings _not_canceled | Liczba poprzednich rezerwacji, które nie zostały anulowane przez klienta przed obecną rezerwacją | numeric | 0 - 60 | - |
avg_price_per_room | Średnia cena rezerwacji za dzień; ceny pokoi są dynamiczne. (w euro) | numeric | 0 - 550 | - |
no_of_special_requests | Całkowita liczba specjalnych próśb złożonych przez klienta (np. wysokie piętro, widok z pokoju itp.) | numeric | 0 - 5 | - |
booking_status | Zmienna docelowa wskazująca, czy rezerwacja została anulowana, czy nie | factor | Not_Canceled, Cancelled | Not_Canceled = rezerwacja nieanulowana, Cancelled = rezerwacja anulowana |
2.2 Braki danych
invisible(suppressMessages(mice::md.pattern(dt, rotate.names = TRUE)))
/\ /\
{ `---' }
{ O O }
==> V <== No need for mice. This data set is completely observed.
\ \|/ /
`-----'
W zbiorze danych nie występują braki — każda zmienna została w pełni zaobserwowana. Dane są gotowe do analizy bez dodatkowego czyszczenia.
2.3 Analiza zbioru danych
Zanim przystąpimy do budowy modeli, przyjrzymy się bliżej danym. Sprawdzimy m.in. jak wygląda rozkład statusu rezerwacji oraz jakie zależności występują między zmiennymi. Taka analiza pozwoli nam lepiej zrozumieć dane i przygotować je do dalszych etapów pracy.
2.3.1 Procentowy rozkład zmiennej booking_status
library(dplyr)
%>%
dt count(booking_status) %>%
mutate(percent = n / sum(n) * 100) %>%
ggplot(aes(x = booking_status, y = percent, fill = booking_status)) +
geom_col() +
geom_text(aes(label = sprintf("%.1f%%", percent)), vjust = -0.5, size = 5) +
scale_fill_manual(values = c("Canceled" = "salmon", "Not_Canceled" = "lightgreen")) +
scale_y_continuous(limits = c(0, 75)) +
labs(
#title = "Procentowy rozkład zmiennej booking_status",
x = "Status rezerwacji",
y = "Procent (%)") +
theme_minimal()
Na podstawie wykresu możemy zauważyć, że zmienna docelowa booking_status
jest niezbalansowana. Większość obserwacji – 67,2% – dotyczy rezerwacji zrealizowanych (nieanulowanych), natomiast 32,8% stanowią rezerwacje anulowane. Taka dysproporcja klas może mieć istotny wpływ na skuteczność modeli klasyfikacyjnych i wymaga odpowiednich technik balansowania danych, takich jak np. oversampling.
2.3.2 Lead time a status rezerwacji hotelowej
Lead time to odstęp (w dniach) między rezerwacją a przyjazdem.
ggplot(dt, aes(x = booking_status, y = lead_time, fill = booking_status)) +
geom_boxplot() +
labs(
#title = "Odstęp (w dniach) między rezerwacją a przyjazdem w podziale na status rezerwacji",
x = "Status rezerwacji",
y = "Liczba dni od rezerwacji do przyjazdu",
fill = "Status rezerwacji") +
theme_minimal() +
scale_fill_manual(values = c("Canceled" = "salmon", "Not_Canceled" = "lightgreen"))
Anulowane rezerwacje (Cancelled) są zwykle dokonywane z większym wyprzedzeniem - mediana wynosi ponad 100 dni, podczas gdy nieanulowane (Not_Cancelled) – znacznie później (bliżej przyjazdu) - mediana wynosi ponieżej 50 dni, z wieloma wartościami odstającymi.
2.3.3 Liczba rezerwacji względem dorosłych i dzieci
%>%
dt group_by(no_of_adults, no_of_children) %>%
summarize(count = n(), .groups = "drop") %>%
ggplot(aes(x = factor(no_of_adults), y = factor(no_of_children), fill = count)) +
geom_tile(color = "white") +
geom_text(aes(label = count), size = 4) +
scale_fill_gradient(low = "seashell", high = "rosybrown") +
labs(
#title = "Liczba rezerwacji względem liczby dorosłych i dzieci",
x = "Liczba dorosłych",
y = "Liczba dzieci",
fill = "Liczba\nrezerwacji"
+
) theme_minimal()
Najwięcej rezerwacji dotyczyło dwóch dorosłych i brak dzieci - 23 719 rezerwacji, co wskazuje na najczęstszy profil gości – pary dorosłych bez dzieci. Drugi najczęstszy układ to jedna osoba dorosła bez dzieci. (7551 rezerwacji). Rezerwacje z dziećmi są znacznie rzadsze.
2.3.4 Korelacja zmiennych liczbowych:
# Zmienne liczbowe
<- dt[sapply(dt, is.numeric)]
numeric_vars
# Oblicza korelacje
<- cor(numeric_vars, use = "complete.obs")
corr_matrix
%>%
dt select(where(is.numeric)) %>%
cor() %>%
::corrplot(method = "color",
corrplottype = "upper",
tl.srt = 45,
number.cex = 0.7,
tl.cex = 0.7,
tl.col = "black",
addCoef.col = "black",
col = colorRampPalette(c("midnightblue", "white", "maroon"))(100))
no_of_previous_cancellations
(liczba wcześniejszych anulowanych rezerwacji) i no_of_previous_bookings_not_canceled
: (liczba wcześniejszych nieanulowanych rezerwacji) są najmocniej skorelowane (ale nie silnie). Oznacza to, że klienci, którzy mają więcej wcześniejszych rezerwacji (ogółem), mają też zwykle zarówno więcej anulowanych, jak i nieanulowanych rezerwacji.
Istnieje słaba, dodatnia korelacja między ceną rezerwacji za dzień (avg_price_per_room
), a liczbą osób w rezerwacji (no_of_adults
0.3, no_of_children
- 0.34). Ceny rosną wraz z liczbą gości, co jest dość logiczne.
2.3.5 Liczba rezerwacji w czasie
<- dt %>%
p mutate(year_month = make_date(as.numeric(as.character(arrival_year)), as.numeric(as.character(arrival_month)), arrival_date))
<- p %>%
monthly_counts count(year_month)
<- monthly_counts %>%
w ggplot(aes(x = year_month, y = n)) +
geom_area(fill = "maroon", alpha = 0.5) +
geom_line(color = "maroon") +
labs(
x = "Miesiąc",
y = "Liczba rezerwacji"
#title = "Liczba rezerwacji"
+
) theme_ipsum()
ggplotly(w)
Na wykresie przedstawiono liczbę rezerwacji dokonanych w okresie od 1 lipca 2017 roku do końca 2018 roku. Zauważalny jest lekki spadek liczby rezerwacji w miesiącach zimowych, na przełomie 2017 i 2018 roku.
Największe natężenie rezerwacji odnotowano 13 października 2018 roku, kiedy to zarejestrowano aż 254 rezerwacje w ciągu jednego dnia. Może to być związane z okresem wzmożonego ruchu turystycznego lub wydarzeniami sezonowymi.
2.3.6 Rozkłady zmiennych numerycznych
# Kolumny numeryczne
<- c("no_of_children", "no_of_adults", "no_of_week_nights",
num_cols "no_of_weekend_nights", "arrival_date", "lead_time",
"no_of_previous_cancellations", "no_of_previous_bookings_not_canceled",
"no_of_special_requests", "avg_price_per_room")
# Wykresy
<- lapply(num_cols, function(col) {
plots ggplot(dt, aes_string(x = col)) +
geom_histogram(bins = 30, fill = "skyblue", color = "white") +
ggtitle(col) +
theme_minimal()
})
# Wyświetlenie ich razem
wrap_plots(plots, ncol = 3)
Dane są mocno skośne w przypadku wielu zmiennych – co może wpływać na wyniki modeli predykcyjnych.
Typowy klient to nowy gość, bez dzieci, dwoje dorosłych, pobyt 1–3 noce, bez specjalnych życzeń.
Część zmiennych może wymagać transformacji lub standaryzacji przed modelowaniem (np. lead_time
, avg_price_per_room
).
3 Metoda analizy
W celu zbudowania modelu klasyfikacyjnego, który przewiduje, czy dana rezerwacja zostanie anulowana, wykorzystamy pięć popularnych algorytmów uczenia maszynowego, różniących się założeniami, mechanizmami działania oraz odpornością na złożoność danych.
3.1 Regresja logistyczna (Logistic Regression)
Regresja logistyczna to liniowy model klasyfikacyjny, który estymuje prawdopodobieństwo przynależności do jednej z dwóch klas. Jest łatwa do interpretacji i często używana jako punkt odniesienia.
3.2 Drzewo decyzyjne (Decision Tree)
Model oparty na strukturze drzewa, który dokonuje podziałów zbioru danych na podstawie wartości predyktorów. Łatwy do interpretacji i wizualizacji, choć podatny na przeuczenie (overfitting).
3.3 Las losowy (Random Forest)
To model składający się z wielu drzew decyzyjnych, budowanych na losowych podzbiorach danych i zmiennych. Dzięki uśrednianiu wyników poszczególnych drzew model ten uzyskuje wysoką stabilość i dokładność, a także zmniejsza ryzyko przeuczenia. Jest to jeden z najczęściej stosowanych modeli klasyfikacyjnych.
3.4 Naive Bayes
Model probabilistyczny bazujący na twierdzeniu Bayesa i założeniu niezależności między predyktorami. Mimo swojej prostoty, często daje zaskakująco dobre wyniki w klasyfikacji, szczególnie przy dużych zbiorach danych i niezrównoważonych klasach.
3.5 XGBoost (Extreme Gradient Boosting)
Zaawansowany model oparty na boostingowaniu drzew decyzyjnych. Buduje kolejne drzewa na podstawie błędów poprzednich. Jest bardzo skuteczny, zwłaszcza przy dużych i złożonych zbiorach danych, ale mniej interpretowalny niż prostsze modele.
4 Modelowanie klasyfikacyjne
4.1 Podział na zbiór uczący i testowy
Proces modelowania rozpoczynamy od podziału danych na zbiór uczący i testowy. Zbiór treningowy stanowi 2/3 całkowitej liczby obserwacji, a testowy pozostałą 1/3.
Analiza rozkładu zmiennej docelowej (booking_status
) ujawniła znaczną nierównowagę klas — około 67,2% obserwacji to rezerwacje nieanulowane (Not_Canceled
), a 32,8% to rezerwacje anulowane (Canceled
). Taka dysproporcja może prowadzić do uprzywilejowania klasy dominującej i zaniżenia skuteczności klasyfikacji dla mniejszościowej klasy.
Aby zniwelować ten problem, zastosujemy technikę oversamplingu na zbiorze treningowym. Polega ona na sztucznym zwiększeniu liczby przykładów należących do klasy mniejszościowej poprzez ich losowe powielanie. Dzięki temu model uczy się na bardziej zbalansowanym zbiorze.
Dodatkowo, aby uzyskać rzetelną ocenę wydajności modelu, zastosowano walidację krzyżową (cross-validation). Dane uczące podzielone zostały na 5 części (tzw. foldy) przy użyciu 5-krotnej walidacji krzyżowej ze stratyfikacją względem zmiennej booking_status
, co zapewnia zachowanie proporcji klas w każdym foldzie.
set.seed(2025)
# Sprawdzenie zmiennych o zerowej lub bliskiej zeru wariancji
##nzv <- nearZeroVar(dt, saveMetrics = TRUE)
##nzv #brak
##dt <- dt[, !nzv$zeroVar]
<- dt[, -1] #usuwamy kolumnę id
dt
# Podział na zbiór uczący i testowy
<- initial_split(dt, prop = 2/3, strata = booking_status)
split <- training(split)
train <- testing(split)
test
## Oversamplig
<- ovun.sample(booking_status ~ ., data = train, method = "over", N = 33000)$data
train #method = "over" – stosujemy oversampling, czyli zwiększamy liczbę przykładów klasy mniejszościowej przez kopiowanie
#N = 1000 – nowy zbiór danych ma mieć łącznie 1000 obserwacji
# Dla Decision Tree i Random FOrest
<- recipe(booking_status ~ ., data = train)
rec
# Dla LR, XGBoost, Naive Bayes
<- recipe(booking_status ~ ., data = train) %>%
rec_dummy step_dummy(all_nominal_predictors()) %>%
step_zv(all_predictors())
<- vfold_cv(data = train, v = 5, strata = booking_status) #v = 5 – 5-krotna walidacja krzyżowa (podzielenie danych na 5 części)
dt_folds
<- trainControl(method = "repeatedcv",
control number = 5,
#repeats = 5,
summaryFunction = twoClassSummary,
classProbs = TRUE) # 5-krotna walidacja
# Logistic Regression, Decision Tree, Random Forest, Gradient Boosting, XGBoost
# 1. Logistic Regression
<- logistic_reg(mode = "classification") %>%
lr set_engine("glm")
# 2. Decision Tree
<- decision_tree(mode = "classification") %>%
dt set_engine("rpart")
# 3. Random Forest
<- rand_forest(mode = "classification") %>%
rf set_engine("ranger", importance = "impurity")
# 4. Naive Bayes
<- naive_Bayes(mode = "classification") %>%
nb set_engine("naivebayes")
# 5. XGBoost (identycznie, ale możesz ustawić inne parametry)
<- boost_tree(mode = "classification") %>%
xgb set_engine("xgboost")
<- workflow_set(
classification_models preproc = list(
rec_d = rec_dummy,
rec = rec,
rec = rec,
rec_d = rec_dummy,
rec_d = rec_dummy),
models = list(
lr = lr,
dt = dt,
rf = rf,
nb = nb,
xgb = xgb
),cross = FALSE
)
<- control_resamples(save_pred = TRUE, save_workflow = TRUE)
keep_pred #Tworzy obiekt kontrolny do funkcji fit_resamples():
# save_pred = TRUE – zapisz przewidywania (predykcje) dla każdej obserwacji w walidacji krzyżowej,
# save_workflow = TRUE – zapisz cały workflow razem z wynikami
#rm(kap, recall)
<- metric_set(yardstick::accuracy, yardstick::kap, yardstick::roc_auc, yardstick::precision, yardstick::recall, yardstick::f_meas)
metrs # zestaw metryk: dokładność, statystyka Kappa(zgodność modeli i klasyfikacji losowej), pole pod krzywą ROC, precyzja(jak wiele z przewidzianych pozytywnych to prawdziwe pozytywne), czułość(jak wiele z prawdziwych pozytywnych zostało wykrytych), F1-score(harmoniczna średnia precyzji i czułości)
<- classification_models %>%
classification_models workflow_map("fit_resamples", seed = 2025, verbose = TRUE,
resamples = dt_folds,
control = keep_pred,
metrics = metrs) # mapujemy fit_resamples na kazdy przeplyw
#workflow_map() – stosuje określoną funkcję ("fit_resamples") do wszystkich workflowów w obiekcie classification_models, czyli np. naszych różnych modeli (np. regresja logistyczna, las losowy i knn).
#"fit_resamples" – mówi: przeprowadź walidację krzyżową dla każdego workflowa
4.1.1 Regresja Logistyczna (Logistic Regression)
Regresja logistyczna należy do grupy liniowych metod klasyfikacyjnych i wykorzystuje funkcję logistyczną do modelowania prawdopodobieństwa przynależności obserwacji do jednej z dwóch klas. Formalnie jest częścią uogólnionych modeli liniowych (GLM). Technika ta znajduje zastosowanie, gdy zmienna zależna przyjmuje dwie możliwe wartości – zazwyczaj oznaczane jako 1 (sukces) oraz 0 (porażka).
W ramach tego podejścia modeluje się warunkowe prawdopodobieństwo sukcesu jako funkcję liniowej kombinacji zmiennych objaśniających \(X\):
Ogólna postać modelu:
\(Y \sim B(1,p)\)
\(p(X) = E(Y|X) = \frac{exp(\beta X)}{1 + \exp(\beta X)}\)
gdzie \(B(1,p)\) jest rozkładem dwumianowym o prawdopodobieństwie sukcesu \(p\), a \(\beta X\) oznacza kombinację liniową parametrów modelu i wartości zmiennych niezależnych, przyjmując, że \(x_0 = 1\). Jako funkcji łączącej (czyli opisującej związek między kombinacją liniową predyktorów i prawdopodobieństwem sukcesu) użyto logitu.
<- classification_models %>%
logreg_fit extract_workflow("rec_d_lr") %>%
fit(data = train)
<- predict(logreg_fit, new_data = test, type = "prob") %>%
logreg_preds bind_cols(predict(logreg_fit, new_data = test)) %>%
bind_cols(test %>% select(booking_status))
# Metryki
bind_rows(
::metrics(logreg_preds, truth = booking_status, estimate = .pred_class),
yardstick::roc_auc(logreg_preds, truth = booking_status, .pred_Not_Canceled),
yardstick::precision(logreg_preds, truth = booking_status, estimate = .pred_class),
yardstick::recall(logreg_preds, truth = booking_status, estimate = .pred_class),
yardstick::specificity(logreg_preds, truth = booking_status, estimate = .pred_class),
yardstick::f_meas(logreg_preds, truth = booking_status, estimate = .pred_class)
yardstick%>%
) select(.metric, .estimate) %>%
mutate(.metric = case_when(
== "accuracy" ~ "Dokładność (Accuracy)",
.metric == "roc_auc" ~ "ROC AUC",
.metric == "precision" ~ "Precyzja (Precision)",
.metric == "recall" ~ "Czułość (Recall)",
.metric == "specificity" ~ "Specyficzność (Specificity)",
.metric == "f_meas" ~ "F1-score",
.metric TRUE ~ .metric
%>%
)) gt() %>%
fmt_number(columns = vars(.estimate), decimals = 3) %>%
cols_label(
.metric = "Miara",
.estimate = "Wartość"
%>%
) tab_header(
title = "Metryki modelu Regresji Logistycznej"
%>%
) tab_style(
style = list(
cell_text(weight = "bold", color = "darkblue")
),locations = cells_column_labels(columns = everything())
)
Metryki modelu Regresji Logistycznej | |
---|---|
Miara | Wartość |
Dokładność (Accuracy) | 0.777 |
kap | 0.524 |
ROC AUC | 0.870 |
Precyzja (Precision) | 0.880 |
Czułość (Recall) | 0.774 |
Specyficzność (Specificity) | 0.783 |
F1-score | 0.823 |
4.1.1.1 Confusion Matrix
<- conf_mat(logreg_preds, truth = booking_status, estimate = .pred_class)
cm
autoplot(cm, type = "heatmap") +
scale_fill_gradient(low = "white", high = "steelblue") +
labs(title = "Macierz pomyłek - Regresja Logistyczna") +
theme_minimal() +
theme(legend.position = "right")
Model osiąga zrównoważone wyniki: accuracy 0.777, precision 0.880, recall 0.774 i F1-score 0.82. Wysoki ROC AUC (0.870) potwierdza jego dobrą zdolność klasyfikacyjną. To stabilny model bazowy, który dobrze rozróżnia obie klasy.
4.1.2 Drzewo decyzyjne (Decision Tree)
Drzewo decyzyjne to hierarchiczna struktura służąca do tworzenia modeli klasyfikacyjnych lub regresyjnych. Jest szczególnie użyteczne w sytuacjach, gdy relacje między zmiennymi niezależnymi a zmienną docelową są trudne do opisania za pomocą standardowych funkcji matematycznych. Struktura drzewa składa się z korzenia (root), węzłów pośrednich (nodes) oraz liści (leaves). Proces rozpoczyna się w korzeniu, który reprezentuje pierwszy podział zbioru danych. Każdy podział prowadzi do kolejnych węzłów potomnych, a końcowe węzły, w których dalsze podziały nie występują, nazywane są liśćmi. Elementy drzewa łączone są za pomocą gałęzi (branches), tworząc całą jego strukturę.
W naszym przypadku służy do klasyfikacji, gdzie każdy liść wskazuje najbardziej prawdopodobną klasę na podstawie warunków podziału prowadzących do danego węzła końcowego. Celem algorytmu budującego drzewo jest maksymalizacja jednorodności klas w obrębie liści, co oznacza, że model stara się tak dobierać podziały, aby dane trafiające do każdego liścia były możliwie jak najbardziej spójne względem klasy, do której należą.
Jedną z największych zalet drzew decyzyjnych jest ich czytelność – można je łatwo przedstawić graficznie, co umożliwia intuicyjne zrozumienie, jak model podejmuje decyzje.
<- classification_models %>%
dt_fit extract_workflow("rec_dt") %>%
fit(data = train)
<- predict(dt_fit, new_data = test, type = "prob") %>%
dt_preds bind_cols(predict(dt_fit, new_data = test)) %>%
bind_cols(test %>% select(booking_status))
# Metryki dla Decision Tree
bind_rows(
::metrics(dt_preds, truth = booking_status, estimate = .pred_class),
yardstick::roc_auc(dt_preds, truth = booking_status, .pred_Not_Canceled),
yardstick::precision(dt_preds, truth = booking_status, estimate = .pred_class),
yardstick::recall(dt_preds, truth = booking_status, estimate = .pred_class),
yardstick::specificity(dt_preds, truth = booking_status, estimate = .pred_class),
yardstick::f_meas(dt_preds, truth = booking_status, estimate = .pred_class)
yardstick%>%
) select(.metric, .estimate) %>%
mutate(.metric = case_when(
== "accuracy" ~ "Dokładność (Accuracy)",
.metric == "roc_auc" ~ "ROC AUC",
.metric == "precision" ~ "Precyzja (Precision)",
.metric == "recall" ~ "Czułość (Recall)",
.metric == "specificity" ~ "Specyficzność (Specificity)",
.metric == "f_meas" ~ "F1-score",
.metric TRUE ~ .metric
%>%
)) gt() %>%
fmt_number(columns = vars(.estimate), decimals = 3) %>%
cols_label(
.metric = "Miara",
.estimate = "Wartość"
%>%
) tab_header(
title = "Metryki modelu Decision Tree"
%>%
) tab_style(
style = list(
cell_text(weight = "bold", color = "lightgreen")
),locations = cells_column_labels(columns = everything())
)
Metryki modelu Decision Tree | |
---|---|
Miara | Wartość |
Dokładność (Accuracy) | 0.797 |
kap | 0.547 |
ROC AUC | 0.797 |
Precyzja (Precision) | 0.862 |
Czułość (Recall) | 0.831 |
Specyficzność (Specificity) | 0.727 |
F1-score | 0.846 |
4.1.2.1 Confusion Matrix
<- conf_mat(dt_preds, truth = booking_status, estimate = .pred_class)
cm_dt
autoplot(cm_dt, type = "heatmap") +
scale_fill_gradient(low = "white", high = "lightgreen") +
labs(title = "Macierz pomyłek - Decision Tree") +
theme_minimal() +
theme(legend.position = "right")
Model osiąga dobrą dokładność (0.797) oraz Kappa (0.547). Ma wysoką czułość (0.831) i precyzję (0.862), co przekłada się na dobry F1-score (0.846). ROC AUC wynosi 0.797, co świadczy o przyzwoitej zdolności rozróżniania klas.
4.1.2.2 Wizualizacja drzewa decyzyjnego
library(rpart.plot)
<- dt_fit %>%
rpart_model extract_fit_parsnip() %>%
$fit
.
rpart.plot(rpart_model, type = 2, extra = 106, fallen.leaves = TRUE, main = "Drzewo decyzyjne")
To skuteczny i zrównoważony model do przewidywania anulacji rezerwacji
4.1.3 Las losowy (Random Forest)
Lasy losowe stanowią rozszerzenie metody baggingu, wzbogacone o dodatkowy element losowości. Podczas konstruowania każdego drzewa wchodzącego w skład lasu, losowo wybiera się \(m\) predyktorów spośród wszystkich \(p\) dostępnych zmiennych. Budowa drzewa opiera się wyłącznie na tej losowej podpróbie cech. Zazwyczaj wartość \(m\) przyjmuje się jako pierwiastek z liczby wszystkich predyktorów, czyli \(m = \sqrt{p}\).
<- classification_models %>%
rf_fit extract_workflow("rec_rf") %>%
fit(data = train)
<- predict(rf_fit, new_data = test, type = "prob") %>%
preds bind_cols(predict(rf_fit, new_data = test)) %>%
bind_cols(test %>% select(booking_status))
# Accuracy, F1, ROC AUC
bind_rows(
::metrics(preds, truth = booking_status, estimate = .pred_class),
yardstick::roc_auc(preds, truth = booking_status, .pred_Not_Canceled),
yardstick::precision(preds, truth = booking_status, estimate = .pred_class),
yardstick::recall(preds, truth = booking_status, estimate = .pred_class),
yardstick::specificity(preds, truth = booking_status, estimate = .pred_class),
yardstick::f_meas(preds, truth = booking_status, estimate = .pred_class)
yardstick%>%
) select(.metric, .estimate) %>%
mutate(.metric = case_when(
== "accuracy" ~ "Dokładność (Accuracy)",
.metric == "roc_auc" ~ "ROC AUC",
.metric == "precision" ~ "Precyzja (Precision)",
.metric == "recall" ~ "Czułość (Recall)",
.metric == "specificity" ~ "Specyficzność (Specificity)",
.metric == "f_meas" ~ "F1-score",
.metric TRUE ~ .metric
%>%
)) gt() %>%
fmt_number(columns = vars(.estimate), decimals = 3) %>%
cols_label(
.metric = "Miara",
.estimate = "Wartość"
%>%
) tab_header(
title = "Metryki modelu Random Forest"
%>%
) tab_style(
style = list(
cell_text(weight = "bold", color = "darkgreen")
),locations = cells_column_labels(columns = everything())
)
Metryki modelu Random Forest | |
---|---|
Miara | Wartość |
Dokładność (Accuracy) | 0.897 |
kap | 0.765 |
ROC AUC | 0.955 |
Precyzja (Precision) | 0.920 |
Czułość (Recall) | 0.927 |
Specyficzność (Specificity) | 0.834 |
F1-score | 0.924 |
4.1.3.1 Confusion Matrix
<- conf_mat(preds, truth = booking_status, estimate = .pred_class)
cm_rf
autoplot(cm_rf, type = "heatmap") +
scale_fill_gradient(low = "white", high = "darkgreen") +
labs(title = "Macierz pomyłek - Random Forest") +
theme_minimal() +
theme(legend.position = "right")
Model osiąga bardzo wysoką dokładność (0.897) i Kappa (0.765). Zarówno precyzja (0.920), jak i czułość (0.927) są na wysokim poziomie, co przekłada się na dość dobry F1-score (0.924). ROC AUC (0.955) wskazuje na bardzo dobrą zdolność rozróżniania klas. To najlepiej sprawdzający się model w zadaniu przewidywania anulacji rezerwacji.
4.1.3.2 Wykres ważności zmiennych
<- vip(extract_fit_parsnip(rf_fit)$fit, num_features = 10)
p
+
p ::geom_col(fill = "darkgreen") +
ggplot2::theme_minimal() ggplot2
Z wykresu ważności zmiennych wynika, że najważniejszym czynnikiem rozróżniającym jest lead_time — czyli czas od dokonania rezerwacji do planowanego przyjazdu. Kolejne istotne zmienne to avg_price_per_room, no_of_special_requests oraz arrival_month, które również znacząco wpływają na przewidywanie anulacji rezerwacji.
4.1.4 Naive Bayes
W naiwnym klasyfikatorze Bayesa zakłada się warunkową niezależność poszczególnych atrybutów względem klasy do której (zgodnie z hipotezą) ma należeć dany obiekt. Założenie to często nie jest spełnione i stąd nazwa “naiwny”.
# Dopasowanie modelu Naive Bayes
<- classification_models %>%
nb_fit extract_workflow("rec_d_nb") %>%
fit(data = train)
# Predykcje
<- predict(nb_fit, new_data = test, type = "prob") %>%
nb_preds bind_cols(predict(nb_fit, new_data = test)) %>%
bind_cols(test %>% select(booking_status))
# Metryki
bind_rows(
::metrics(nb_preds, truth = booking_status, estimate = .pred_class),
yardstick::roc_auc(nb_preds, truth = booking_status, .pred_Not_Canceled),
yardstick::precision(nb_preds, truth = booking_status, estimate = .pred_class),
yardstick::recall(nb_preds, truth = booking_status, estimate = .pred_class),
yardstick::specificity(nb_preds, truth = booking_status, estimate = .pred_class),
yardstick::f_meas(nb_preds, truth = booking_status, estimate = .pred_class)
yardstick%>%
) select(.metric, .estimate) %>%
mutate(.metric = case_when(
== "accuracy" ~ "Dokładność (Accuracy)",
.metric == "roc_auc" ~ "ROC AUC",
.metric == "precision" ~ "Precyzja (Precision)",
.metric == "recall" ~ "Czułość (Recall)",
.metric == "specificity" ~ "Specyficzność (Specificity)",
.metric == "f_meas" ~ "F1-score",
.metric TRUE ~ .metric
%>%
)) gt() %>%
fmt_number(columns = vars(.estimate), decimals = 3) %>%
cols_label(
.metric = "Miara",
.estimate = "Wartość"
%>%
) tab_header(
title = "Metryki modelu Naive Bayes"
%>%
)tab_style(
style = list(
cell_text(weight = "bold", color = "darkorange")
),locations = cells_column_labels(columns = everything())
)
Metryki modelu Naive Bayes | |
---|---|
Miara | Wartość |
Dokładność (Accuracy) | 0.373 |
kap | 0.044 |
ROC AUC | 0.839 |
Precyzja (Precision) | 0.989 |
Czułość (Recall) | 0.068 |
Specyficzność (Specificity) | 0.998 |
F1-score | 0.127 |
Model Naive Bayes osiąga bardzo wysoką precyzję (0.989) oraz specyficzność (0.998) i jednocześnie bardzo niska czułość (Recall = 0.068) i F1-score (0.127), co oznacza, że model nie radzi sobie z wykrywaniem anulowanych rezerwacji.
4.1.4.1 Confusion Matrix
<- conf_mat(nb_preds, truth = booking_status, estimate = .pred_class)
cm_nb
autoplot(cm_nb, type = "heatmap") +
scale_fill_gradient(low = "white", high = "darkorange") +
labs(title = "Macierz pomyłek - Naive Bayes") +
theme_minimal() +
theme(legend.position = "right")
Naive Bayes jest zbyt uproszczonym modelem dla tego problemu — jego silne założenie niezależności cech i skrajna asymetria predykcji skutkują bardzo słabym wynikiem ogólnym, mimo wysokiej precyzji. W kontekście przewidywania anulacji rezerwacji (prawie zawsze wykrywa, że rezerwacja jest odwołana), ten model jest niewystarczający praktycznie.
4.1.5 XGBoost
W metodzie boosting odpowiednio modyfikuje się drzewo wyjściowe w kolejnych krokach, ucząc je na tym samym zbiorze treningowym. Uczenie drzew klasyfikacyjnych metodą boosting przebiega w sposób podobny do uczenia drzew regresyjnych. Wynik takiego procesu zależy od trzech głównych parametrów:
- Liczby drzew \(B\) - zbyt duża wartość \(B\) może prowadzić do przeuczenia modelu. Najczęściej parametr ten ustalany jest na podstawie walidacji krzyżowej.
- Parametru “kurczenia” \(\lambda\) - kontroluje on tempo uczenia się kolejnych drzew. Bardzo małe wartości \(\lambda\) mogą wymagać dobrania większego \(B\), aby zapewnić dobrą jakość predykcyjną modelu.
- Liczby podziałów w drzewach \(d\), która decyduje o złożoności pojedynczego drzewa.
<- classification_models %>%
xgb_fit extract_workflow("rec_d_xgb") %>%
fit(data = train)
<- predict(xgb_fit, new_data = test, type = "prob") %>%
xgb_preds bind_cols(predict(xgb_fit, new_data = test)) %>%
bind_cols(test %>% select(booking_status))
bind_rows(
::metrics(xgb_preds, truth = booking_status, estimate = .pred_class),
yardstick::roc_auc(xgb_preds, truth = booking_status, .pred_Not_Canceled),
yardstick::precision(xgb_preds, truth = booking_status, estimate = .pred_class),
yardstick::recall(xgb_preds, truth = booking_status, estimate = .pred_class),
yardstick::specificity(xgb_preds, truth = booking_status, estimate = .pred_class),
yardstick::f_meas(xgb_preds, truth = booking_status, estimate = .pred_class)
yardstick%>%
) select(.metric, .estimate) %>%
mutate(.metric = case_when(
== "accuracy" ~ "Dokładność (Accuracy)",
.metric == "roc_auc" ~ "ROC AUC",
.metric == "precision" ~ "Precyzja (Precision)",
.metric == "recall" ~ "Czułość (Recall)",
.metric == "specificity" ~ "Specyficzność (Specificity)",
.metric == "f_meas" ~ "F1-score",
.metric TRUE ~ .metric
%>%
)) gt() %>%
fmt_number(columns = vars(.estimate), decimals = 3) %>%
cols_label(
.metric = "Miara",
.estimate = "Wartość"
%>%
) tab_header(
title = "Metryki modelu XGBoost"
%>%
)tab_style(
style = list(
cell_text(weight = "bold", color = "maroon")
),locations = cells_column_labels(columns = everything())
)
Metryki modelu XGBoost | |
---|---|
Miara | Wartość |
Dokładność (Accuracy) | 0.861 |
kap | 0.690 |
ROC AUC | 0.932 |
Precyzja (Precision) | 0.907 |
Czułość (Recall) | 0.884 |
Specyficzność (Specificity) | 0.814 |
F1-score | 0.896 |
4.1.5.1 Confusion Matrix
<- conf_mat(xgb_preds, truth = booking_status, estimate = .pred_class)
cm_xgb
autoplot(cm_xgb, type = "heatmap") +
scale_fill_gradient(low = "white", high = "maroon") +
labs(title = "Macierz pomyłek - XGBoost") +
theme_minimal() +
theme(legend.position = "right")
Model osiąga wysoką dokładność (0.861) oraz Kappa (0.690). Zarówno precyzja (0.907), jak i czułość (0.884) są bardzo dobre, co daje dobry wynik F1-score (0.896). ROC AUC (0.932) potwierdza wysoką zdolność rozróżniania klas. To bardzo skuteczny model, ustępujący nieco tylko Random Forest.
5 Porównanie modeli
library(ggrepel)
<- autoplot(classification_models, metric = "accuracy")+
acc geom_text_repel(aes(label = model))+
theme(legend.position = "none")
<- autoplot(classification_models, metric = "kap")+
kap geom_text_repel(aes(label = model))+
theme(legend.position = "none")
<- autoplot(classification_models, metric = "roc_auc")+
roc geom_text_repel(aes(label = model))+
theme(legend.position = "none")
<- autoplot(classification_models, metric = "recall")+
recall geom_text_repel(aes(label = model))+
theme(legend.position = "none")
<- autoplot(classification_models, metric = "f_meas")+
f1 geom_text_repel(aes(label = model))+
theme(legend.position = "none")
<- autoplot(classification_models, metric = "precision")+
prec geom_text_repel(aes(label = model))+
theme(legend.position = "none")
::grid.arrange(acc, kap, roc, recall, prec, f1) gridExtra
Random Forest (las losowy) oraz Boosted Tree (XGBoost) okazują się najlepiej radzącymi sobie modelami w zadaniu klasyfikacyjnym. Spośród wszystkich testowanych algorytmów osiągają najwyższe wyniki w kluczowych metrykach, takich jak Accuracy, Kappa, Recall, F1 Score oraz ROC AUC. Z kolei Naive Bayes, mimo że charakteryzuje się bardzo wysoką precyzją, znacząco odstaje od pozostałych modeli pod względem pozostałych miar skuteczności, co ogranicza jego przydatność w praktyce.
5.1 Krzywa ROC
%>%
classification_models collect_predictions() %>%
group_by(model) %>%
roc_curve(booking_status, .pred_Not_Canceled) %>%
autoplot() +
theme_minimal()
Krzywa ROC ilustruje zależność pomiędzy czułością (sensitivity) a specyficznością (1 - specificity), pozwalając ocenić skuteczność modeli przy różnych progach decyzyjnych.
Można zauważyć, że najlepszy wynik osiągnął model lasu losowego, którego krzywa znajduje się najwyżej względem pozostałych, co świadczy o bardzo wysokiej skuteczności klasyfikacji. Niewiele ustępuje mu model boost_tree, którego krzywa również znajduje się bardzo blisko lewego górnego rogu wykresu, wskazując na wysoką czułość i niską liczbę fałszywie pozytywnych wyników.
Model regresji logistycznej wykazuje umiarkowaną skuteczność – jego krzywa przebiega pośrodku wykresu. Model drzewa decyzyjnego prezentuje się nieco słabiej, a jego krzywa znajduje się poniżej tej uzyskanej przez regresję logistyczną.
Najgorsze wyniki osiągnął model naive_Bayes. Jego krzywa przebiega najbliżej przekątnej wykresu, która reprezentuje klasyfikację losową. Oznacza to, że skuteczność tego modelu jest niska i niewystarczająca.
Podsumowując, najbardziej efektywnym modelem w tym porównaniu okazał się las losowy, natomiast najmniej skutecznym – naiwny klasyfikator Bayesa.
5.2 Metryki
Porównanie modeli klasyfikacyjnych | |||||
---|---|---|---|---|---|
Model | Accuracy | F1_Score | Precision | Recall | ROC_AUC |
Regresja logistyczna | 0.781 | 0.778 | 0.778 | 0.777 | 0.870 |
Decision T | 0.780 | 0.788 | 0.751 | 0.828 | 0.796 |
Random Forest | 0.927 | 0.926 | 0.935 | 0.917 | 0.979 |
Naive Bayes | 0.576 | 0.231 | 0.957 | 0.158 | 0.845 |
XGBoost | 0.858 | 0.860 | 0.836 | 0.886 | 0.938 |
Random Forest zdecydowanie dominuje pod względem wszystkich metryk — osiąga najwyższą dokładność (Accuracy 0.927), F1 Score (0.926) oraz ROC AUC (0.979), co wskazuje na jego wysoką skuteczność i dobrą równowagę między czułością a specyficznością.
XGBoost także radzi sobie bardzo dobrze, szczególnie pod kątem Recall (0.886) i ROC AUC (0.938), co sugeruje, że dobrze identyfikuje pozytywne przypadki.
Natomiast Naive Bayes ma zdecydowanie najniższą skuteczność ogólną, mimo wysokiej precyzji (0.957), co oznacza, że często pomija prawdziwe przypadki (niski Recall 0.158).
Regresja logistyczna i drzewo decyzyjne wypadają umiarkowanie, z wyższym Recall dla drzewa.
6 Podsumowanie
Celem projektu było stworzenie modelu klasyfikacyjnego przewidującego anulowanie rezerwacji hotelowej.
Dane zostały przygotowane z zachowaniem proporcji klas poprzez 5-krotną walidację krzyżową ze stratyfikacją, co zapewniło rzetelną ocenę modeli.
Przetestowano kilka algorytmów, takich jak regresja logistyczna (Logistic Regression), drzewo decyzyjne (Decision Tree), las losowy (Random Forest), Naive Bayes oraz XGBoost.
Najlepsze wyniki osiągnął las losowy (Random Forest), który uzyskał najwyższą dokładność i zrównoważone metryki oceny, potwierdzając jego skuteczność w przewidywaniu anulacji rezerwacji.
Wyniki pokazują, że zastosowanie zaawansowanych metod, takich jak las losowy czy XGBoost, znacząco poprawia przewidywania w porównaniu do prostszych modeli.
Projekt pokazuje, jak wykorzystanie różnych metod klasyfikacyjnych oraz odpowiednia walidacja mogą znacząco podnieść jakość prognoz w zastosowaniach biznesowych. Uzyskane modele mogą wspierać decyzje operacyjne hotelu, pomagając lepiej zarządzać rezerwacjami i minimalizować straty związane z anulacjami.
7 Bibliografia
https://pl.wikipedia.org/wiki/Wikipedia:Strona_g%C5%82%C3%B3wna
Wiedza teoretyczna i praktyczna zdobyta podczas wykładów i laboratoriów z przedmiotu Eksploracja Danych.