---
title: "Hotel Booking Cancellation Prediction"
subtitle: "Machine Learning & Predictive Analysis"
author: "Hamid Yameen"
date: last-modified
format:
html:
output-file: quarto_report.html
toc: true
toc-depth: 3
toc-location: left
number-sections: true
smooth-scroll: true
code-fold: true
code-tools: true
embed-resources: true
theme:
light: cosmo
dark: darkly
jupyter: python3
execute:
echo: false
warning: false
message: false
error: false
cache: false
# This report is intentionally read-only with respect to project artifacts.
# It reads existing CSV/JSON/model files but does not save, retrain, or overwrite them.
---
# Introduction and Business Problem
Hotel booking cancellations create operational and financial challenges because rooms reserved for customers who later cancel may remain unsold. Identifying bookings with a high cancellation risk can support better reservation management, confirmation follow-up, inventory planning, and revenue-related decisions.
The objective of this project is to develop a machine-learning model that estimates the probability that a hotel booking will be cancelled. Historical booking records from two hotels were combined with guest information, while payment-event data was investigated separately to determine whether it could provide valid predictors at the booking-time prediction point.
A non-machine-learning heuristic was first established as a baseline. Logistic Regression, Random Forest, and HistGradientBoosting classifiers were then developed and compared using a chronological train-test strategy. The final selected model was a tuned HistGradientBoostingClassifier, and a classification threshold of **0.40** was selected to improve the identification of actual cancellations while maintaining acceptable precision.
The final model was subsequently integrated into a lightweight production scoring workflow. A simple HTML/JavaScript frontend sends booking information to a Python prediction service, which loads the saved model and returns a cancellation probability, risk classification, and any reliability warnings.
## Project Objective
The primary objective is to develop a reliable cancellation-risk model using information available at or before the prediction point. Performance is assessed using accuracy, precision, recall, F1-score, ROC-AUC, and Average Precision.
::: {.callout-note}
## Prediction Point
Only information that is available at or before the booking-time prediction point should be used. Variables that depend on events occurring after the booking may create data leakage and therefore produce unrealistically optimistic model performance.
:::
## Reproducible Report Setup
The report reads existing project artifacts and recomputes selected statistics directly from them. It does **not** retrain the model or write back to the project.
```{python}
#| label: setup
#| echo: true
#| code-fold: true
from pathlib import Path
import json
import joblib
import pandas as pd
from sklearn.metrics import (
accuracy_score,
precision_score,
recall_score,
f1_score,
roc_auc_score,
confusion_matrix,
)
PROJECT_ROOT = Path("..")
PROCESSED_DIR = PROJECT_ROOT / "data" / "processed"
MODEL_DIR = PROJECT_ROOT / "models"
MODEL_DATA_PATH = PROCESSED_DIR / "bookings_model.csv"
MODEL_PATH = MODEL_DIR / "cancellation_model.joblib"
METADATA_PATH = MODEL_DIR / "model_metadata.json"
# Read-only loads
df = pd.read_csv(MODEL_DATA_PATH)
model = joblib.load(MODEL_PATH)
with open(METADATA_PATH, "r") as f:
metadata = json.load(f)
print("Rows:", len(df))
print("Model:", metadata["model"])
print("Threshold:", metadata["threshold"])
```
::: {.callout-tip}
## Read-only Design
The executable cells in this report use operations such as `pd.read_csv()`, `joblib.load()`, and `json.load()`. No `to_csv()`, `joblib.dump()`, file writes, or model fitting are performed.
:::
# Dataset and Data Sources
## Booking Data
The primary booking data consists of `H1.csv` and `H2.csv`. The two files share a common structure and contain booking-level information such as lead time, arrival date, stay duration, number of guests, meal type, country, market segment, distribution channel, room type, deposit type, agent, customer type, ADR, parking spaces, special requests, and cancellation status.
A `Hotel` variable was added before the files were combined so that the source hotel remained identifiable.
`BookingID` was used as the booking identifier, while `GuestID` was used to link bookings to guest-level information.
## Guest Data
The `guests.csv` file contains guest-level information including guest ID, name, age, country, email, and notes.
Duplicate `GuestID` records were investigated during data preparation. Many represented the same guest with differences such as capitalisation or missing values. Guest records were therefore consolidated using `GuestID`.
Age was retained as a model feature. Personally identifying fields such as names and email addresses were not used as predictive variables.
## Payment Event Data
The `payments.csv` file contains payment-related events such as `payment_received`, `card_declined`, `deposit_authorised`, and `refund_issued`.
These events were investigated because they appeared potentially useful for predicting cancellations. However, timing analysis showed that payment events could not reliably be proven to be available before the booking-time prediction point.
Most payment events occurred after the calculated booking date, while same-day events could not be reliably ordered because an exact booking timestamp was unavailable.
{#fig-payment-timing fig-align="center" width="85%"}
As shown in @fig-payment-timing, payment-event variables were excluded from the final predictive model to reduce the risk of data leakage.
::: {.callout-warning}
## Why Payment Features Were Excluded
A payment event may appear highly predictive of cancellation, but if it occurred after the booking-time prediction point, using it as an input would give the model future information. This would make evaluation results look stronger than they would be in real production use.
:::
## Target Variable
The prediction target is `IsCanceled`:
- `0` = booking not cancelled
- `1` = booking cancelled
Cancellation (`1`) is treated as the positive class.
# Exploratory Data Analysis
Exploratory Data Analysis (EDA) was conducted to understand the structure and quality of the data, examine cancellation behaviour, identify potential predictors, and detect variables that might introduce leakage.
## Cancellation Distribution
The final modelling dataset contained **119,210 bookings**, of which **75,011** were not cancelled and **44,199** were cancelled.
```{python}
#| label: cancellation-counts
#| echo: true
#| code-fold: true
cancellation_counts = (
df["IsCanceled"]
.value_counts()
.sort_index()
)
cancellation_summary = pd.DataFrame({
"Status": ["Not Cancelled", "Cancelled"],
"Bookings": [
int(cancellation_counts.get(0, 0)),
int(cancellation_counts.get(1, 0)),
]
})
cancellation_summary
```
{#fig-cancellation-distribution fig-align="center" width="80%"}
The cancelled class represented approximately **37.1%** of all bookings. The classes were therefore moderately imbalanced rather than perfectly balanced.
Accuracy alone would not be sufficient to evaluate the model because a model could achieve reasonable accuracy while still failing to identify many cancellations. Precision, recall, F1-score, ROC-AUC, and Average Precision were therefore included in the final evaluation.
## Lead Time and Cancellation
Lead time represents the number of days between booking and arrival.
Cancellation rates increased across longer lead-time groups, indicating that bookings made further in advance tended to carry greater cancellation risk.
{#fig-lead-time fig-align="center" width="85%"}
The relationship in @fig-lead-time suggested that lead time was potentially useful both as a model feature and as part of a simple non-ML baseline.
A lead-time threshold of **150 days** was used in the heuristic baseline. This threshold was intended to create a simple and interpretable benchmark rather than an optimised machine-learning decision boundary.
## Deposit Type and Cancellation
Deposit type showed one of the strongest relationships with cancellation behaviour.
{#fig-deposit fig-align="center" width="80%"}
Historical data showed that `Non Refund` bookings had an exceptionally high cancellation rate. This strong association explains why deposit type became an influential feature in the final model.
::: {.callout-important}
## Important EDA Finding
`DepositType = "Non Refund"` was strongly associated with cancellation in the historical data. This is a predictive association observed in the dataset and should not automatically be interpreted as a causal relationship.
:::
## Average Daily Rate
ADR was examined for missing, negative, zero, and unusually large observations.
Negative ADR values were treated as invalid. High ADR observations were investigated rather than automatically removed using an arbitrary upper threshold.
ADR was retained because booking price may contain useful information about cancellation behaviour.
## Guest Data Quality
The guest dataset contained duplicate guest IDs and invalid age values.
Observed age-quality issues included missing ages, ages below zero, and ages above 100.
Invalid ages were treated as missing. Where a valid age was unavailable, the median valid age of **46 years** was used.
Duplicate guest records were consolidated using `GuestID`.
## Chronological Behaviour
Booking dates ranged from June 2013 to August 2017.
A chronological train-test split was used because the intended production scenario involves predicting future bookings from historical data.
| Dataset | Period | Rows |
|---|---|---:|
| Training | 24 Jun 2013 – 11 Jan 2017 | 95,278 |
| Testing | 12 Jan 2017 – 31 Aug 2017 | 23,932 |
The training cancellation rate was approximately **38.46%**, while the testing cancellation rate was approximately **31.56%**.
This difference indicates that cancellation behaviour changed over time and supports the decision to use chronological evaluation instead of randomly mixing earlier and later bookings.
The chronological split can also be reconstructed directly from the modelling dataset:
```{python}
#| label: chronological-split
#| echo: true
#| code-fold: true
df_dates = df.copy()
df_dates["ArrivalDate"] = pd.to_datetime(
df_dates["ArrivalDateYear"].astype(str) + "-" +
df_dates["ArrivalDateMonth"] + "-" +
df_dates["ArrivalDateDayOfMonth"].astype(str),
format="%Y-%B-%d"
)
df_dates["BookingDate"] = (
df_dates["ArrivalDate"]
- pd.to_timedelta(df_dates["LeadTime"], unit="D")
)
df_dates = df_dates.sort_values("BookingDate").reset_index(drop=True)
cutoff_date = pd.Timestamp("2017-01-12")
train_df = df_dates[df_dates["BookingDate"] < cutoff_date].copy()
test_df = df_dates[df_dates["BookingDate"] >= cutoff_date].copy()
pd.DataFrame({
"Dataset": ["Training", "Testing"],
"Rows": [len(train_df), len(test_df)],
"Cancellation Rate": [
train_df["IsCanceled"].mean(),
test_df["IsCanceled"].mean(),
]
})
```
# Data Cleaning and Feature Engineering
The raw data required several cleaning and transformation steps before modelling.
## Data Cleaning
The two hotel datasets were combined after adding a `Hotel` identifier.
The following cleaning tasks were performed:
- Missing values in `Children` were handled.
- Invalid ADR values were investigated and cleaned.
- Guest ages outside a plausible range were treated as invalid.
- Duplicate guest records were consolidated using `GuestID`.
- Personally identifying fields were excluded from modelling.
- Leakage-prone variables and post-booking outcome information were excluded.
- Categorical values were standardised where necessary.
Categorical variables were later processed using one-hot encoding inside the modelling pipeline.
The encoder was configured to handle previously unseen categories safely during transformation, preventing the scoring pipeline from failing when an unfamiliar category appears in new data.
## Date Construction
`ArrivalDate` was reconstructed using `ArrivalDateYear`, `ArrivalDateMonth`, and `ArrivalDateDayOfMonth`.
The approximate booking date was then calculated as:
**BookingDate = ArrivalDate - LeadTime**
`BookingDate` was used for chronological ordering, historical feature construction, and train-test splitting.
## Engineered Booking Features
Two direct booking-level features were created:
**TotalNights**
`TotalNights = StaysInWeekendNights + StaysInWeekNights`
**TotalGuests**
`TotalGuests = Adults + Children + Babies`
These variables provide direct measures of stay duration and occupancy.
## Guest-History Features
Three historical features were engineered:
- `PreviousBookings`
- `PreviousCancellations`
- `PreviousCancellationRate`
The cancellation rate was calculated as:
**PreviousCancellationRate = PreviousCancellations / PreviousBookings**
For guests with no previous bookings, the rate was set to zero.
Only bookings occurring **before the current booking date** were used when creating historical guest features.
::: {.callout-note}
## Leakage Prevention
A guest's future bookings were never used when constructing history for an earlier booking. This ensures that the model only receives information that would realistically have been available at the prediction point.
:::
## Final Model Features
The final model used **29 input features**: 19 numerical and 10 categorical.
### Numerical Features
- LeadTime
- ArrivalDateYear
- ArrivalDateWeekNumber
- ArrivalDateDayOfMonth
- StaysInWeekendNights
- StaysInWeekNights
- Adults
- Children
- Babies
- DaysInWaitingList
- ADR
- RequiredCarParkingSpaces
- TotalOfSpecialRequests
- TotalNights
- TotalGuests
- age
- PreviousBookings
- PreviousCancellations
- PreviousCancellationRate
### Categorical Features
- ArrivalDateMonth
- Meal
- Country
- MarketSegment
- DistributionChannel
- ReservedRoomType
- DepositType
- Agent
- CustomerType
- Hotel
One-hot encoding transformed categorical features inside the pipeline while the original text columns were replaced by their encoded representation before model fitting.
# Non-ML Heuristic Baseline
A simple rule-based classifier was developed before the machine-learning models.
The baseline classified a booking as cancellation risk when either `DepositType == "Non Refund"` or `LeadTime > 150`.
The purpose of this baseline was not to produce an optimal classifier. It provided a simple and interpretable benchmark against which the value added by machine learning could be measured.
| Metric | Heuristic Baseline |
|---|---:|
| Accuracy | 70.95% |
| Precision | 58.76% |
| Recall | 26.66% |
| F1-score | 36.67% |
The baseline achieved reasonable overall accuracy but performed poorly at identifying actual cancellations.
The recall of **26.66%** means that fewer than one third of actual cancellations were identified by the heuristic.
This is important because a simple business rule may appear sensible but still fail to capture the combinations of booking and guest characteristics that influence cancellation behaviour.
The low F1-score of **36.67%** also shows that the baseline did not provide a strong balance between precision and recall.
# Model Development
Three classification algorithms were evaluated:
1. Logistic Regression
2. Random Forest
3. HistGradientBoostingClassifier
Each model was evaluated on the same chronological test period.
## Logistic Regression
Logistic Regression was used as a linear classification benchmark.
Categorical variables were one-hot encoded before modelling.
| Metric | Logistic Regression |
|---|---:|
| Accuracy | 80.25% |
| Precision | 69.22% |
| Recall | 67.36% |
| F1-score | 68.28% |
| ROC-AUC | 87.51% |
Logistic Regression substantially improved on the non-ML baseline, especially in recall and F1-score.
However, the model remains fundamentally linear and may not capture complex nonlinear relationships between booking characteristics.
## Random Forest
Random Forest was evaluated to capture nonlinear relationships and interactions among predictors.
| Metric | Random Forest |
|---|---:|
| Accuracy | 82.54% |
| Precision | 77.33% |
| Recall | 63.19% |
| F1-score | 69.55% |
| ROC-AUC | 89.75% |
Random Forest improved accuracy and precision compared with Logistic Regression, but recall decreased.
This means that its cancellation alerts were more reliable, but it missed a larger proportion of actual cancellations.
## HistGradientBoostingClassifier
HistGradientBoostingClassifier was evaluated as a boosting-based nonlinear model.
| Metric | HistGradientBoosting |
|---|---:|
| Accuracy | 83.26% |
| Precision | 75.61% |
| Recall | 69.32% |
| F1-score | 72.33% |
| ROC-AUC | 90.90% |
The model achieved the strongest overall combination of accuracy, F1-score, and ROC-AUC among the initial candidates.
It was therefore selected for hyperparameter tuning.
# Hyperparameter Tuning
Hyperparameter tuning was performed on HistGradientBoostingClassifier.
Because the dataset is chronological, ordinary random cross-validation was not used. Instead, `TimeSeriesSplit` was used so that validation observations always occurred after their corresponding training observations.
This better reflects real production use, where predictions are made on future bookings from patterns learned from past bookings.
A randomized hyperparameter search was used to explore combinations of `learning_rate`, `max_iter`, `max_leaf_nodes`, `min_samples_leaf`, and `l2_regularization`.
ROC-AUC was used as the tuning score because it evaluates the model's ability to distinguish cancellations from non-cancellations across possible classification thresholds.
The selected hyperparameters were:
```text
min_samples_leaf = 40
max_leaf_nodes = 31
max_iter = 300
learning_rate = 0.1
l2_regularization = 1.0
```
The best cross-validation ROC-AUC was approximately **0.9134**.
At the default threshold of 0.50, the tuned model achieved:
| Metric | Tuned Model (0.50) |
|---|---:|
| Accuracy | 83.52% |
| Precision | 75.71% |
| Recall | 70.35% |
| F1-score | 72.93% |
| ROC-AUC | 90.91% |
The tuned model was therefore selected as the final classifier.
# Classification Threshold Selection
`predict_proba()` returns the estimated probability of cancellation rather than a final class.
For example:
```text
Cancellation probability = 0.73
Threshold = 0.40
0.73 >= 0.40
Prediction = 1
```
A threshold of 0.50 is commonly used as the default binary classification cutoff, but it is not mandatory.
Several thresholds were evaluated:
| Threshold | Accuracy | Precision | Recall | F1-score |
|---:|---:|---:|---:|---:|
| 0.30 | 81.28% | 65.75% | 84.94% | 74.12% |
| **0.40** | 83.15% | 71.52% | 77.45% | **74.37%** |
| 0.50 | **83.52%** | 75.71% | 70.35% | 72.93% |
| 0.60 | 82.81% | 79.42% | 61.47% | 69.30% |
| 0.70 | 81.72% | **84.30%** | 51.69% | 64.09% |
Lowering the threshold causes the model to classify more bookings as cancellation risk. This generally increases recall and false positives while reducing precision.
Raising the threshold generally has the opposite effect.
A threshold of **0.40** was selected because it increased recall from **70.35%** at the default 0.50 threshold to **77.45%**, while also improving the F1-score from **72.93%** to **74.37%**.
::: {.callout-important}
## Selected Operating Threshold
The final model threshold is **0.40**. This does not change the model's underlying probabilities. It changes only the point at which a predicted probability is converted into a cancellation-risk classification.
:::
# Final Model Evaluation
At the selected 0.40 threshold, the final tuned HistGradientBoostingClassifier achieved:
| Metric | Final Result |
|---|---:|
| Accuracy | 83.15% |
| Precision | 71.52% |
| Recall | 77.45% |
| F1-score | 74.37% |
| ROC-AUC | 90.91% |
| Average Precision | 83.39% |
The final metrics below are recomputed directly from the saved model and chronological test set.
```{python}
#| label: final-metrics
#| echo: true
#| code-fold: true
feature_drop = [
"IsCanceled",
"ArrivalDate",
"BookingDate",
"LeadTimeGroup",
]
X_test = test_df.drop(columns=feature_drop, errors="ignore")
y_test = test_df["IsCanceled"]
y_prob = model.predict_proba(X_test)[:, 1]
threshold = float(metadata["threshold"])
y_pred = (y_prob >= threshold).astype(int)
metrics_table = pd.DataFrame({
"Metric": [
"Accuracy",
"Precision",
"Recall",
"F1 Score",
"ROC-AUC",
],
"Value": [
accuracy_score(y_test, y_pred),
precision_score(y_test, y_pred),
recall_score(y_test, y_pred),
f1_score(y_test, y_pred),
roc_auc_score(y_test, y_prob),
],
})
metrics_table
```
## Accuracy
Accuracy of **83.15%** means that approximately 83% of all test bookings were classified correctly.
## Precision
Precision of **71.52%** means that, among bookings classified as cancellation risk, approximately 71.5% actually cancelled.
## Recall
Recall of **77.45%** means that the model successfully identified approximately 77.5% of all actual cancellations.
## F1-score
The F1-score of **74.37%** summarises the balance between precision and recall.
## Confusion Matrix
The final confusion matrix contained:
- 14,051 True Negatives
- 2,329 False Positives
- 1,703 False Negatives
- 5,849 True Positives
```{python}
#| label: confusion-matrix-values
#| echo: true
#| code-fold: true
cm = confusion_matrix(y_test, y_pred)
tn, fp, fn, tp = cm.ravel()
pd.DataFrame({
"Outcome": [
"True Negative",
"False Positive",
"False Negative",
"True Positive",
],
"Count": [tn, fp, fn, tp],
})
```
{#fig-confusion fig-align="center" width="75%"}
The **2,329 false positives** represent bookings predicted to cancel that did not cancel.
The **1,703 false negatives** represent bookings predicted not to cancel that actually cancelled.
## ROC Curve
The final ROC-AUC was **0.9091**.
{#fig-roc fig-align="center" width="80%"}
ROC-AUC evaluates the model's ability to rank cancelled bookings above non-cancelled bookings across possible thresholds.
## Precision-Recall Curve
The model achieved an Average Precision score of approximately **0.8339**.
{#fig-pr fig-align="center" width="80%"}
## Comparison with the Non-ML Baseline
| Metric | Heuristic Baseline | Final ML Model | Improvement |
|---|---:|---:|---:|
| Accuracy | 70.95% | 83.15% | +12.20 pp |
| Precision | 58.76% | 71.52% | +12.76 pp |
| Recall | 26.66% | 77.45% | +50.79 pp |
| F1-score | 36.67% | 74.37% | +37.70 pp |
{#fig-baseline-comparison fig-align="center" width="85%"}
The largest improvement was in recall. The non-ML heuristic detected only **26.66%** of actual cancellations, whereas the final model detected **77.45%**.
# Production Scoring
The final fitted HistGradientBoosting pipeline was saved using Joblib so that new bookings can be scored without retraining the model.
Metadata was stored separately to record the model name, selected classification threshold, prediction target, positive class, and the minimum and maximum lead times observed in training.
```json
{
"model": "HistGradientBoostingClassifier",
"threshold": 0.4,
"target": "IsCanceled",
"positive_class": 1,
"lead_time_min": 0,
"lead_time_max": 737
}
```
## Production Scoring Process
The production scoring script accepts new booking information and prepares the booking in the same structure used during model development.
Required engineered features are calculated automatically, while historical guest information is retrieved where available.
The saved pipeline then uses `predict_proba()` to estimate the cancellation probability.
The 0.40 threshold converts the probability into the final risk classification.
A typical response is:
```json
{
"cancellation_probability": 0.0691,
"prediction": 0,
"decision": "Low cancellation risk",
"threshold": 0.4,
"warnings": []
}
```
## Input Validation and Reliability Checks
Basic input validation was added to identify invalid and out-of-range observations.
The minimum and maximum lead times observed in the training data are stored in model metadata.
```{python}
#| label: production-metadata
#| echo: true
#| code-fold: true
# Already loaded in the setup cell using json.load(..., "r")
metadata
```
The current model was trained on lead times ranging from **0 to 737 days**.
A negative lead time is rejected because it indicates that the arrival date occurs before the booking date.
A booking with a lead time greater than 737 days can still be scored, but the application returns a warning that the value is outside the range observed during training.
The 0–737 day range is not a hotel reservation restriction. It is used only as a model-reliability check.
## Lightweight Web Application
A lightweight web application was developed to demonstrate how the model can be used as a production risk-assessment tool.
The frontend is implemented using HTML and JavaScript.
The prediction service is implemented using standard Python HTTP server functionality. No Python web framework is required.
Controlled inputs are used where appropriate for categorical fields such as meal, deposit type, market segment, distribution channel, room type, customer type, and hotel.
Booking and arrival dates are used to derive date-related model inputs automatically.
## Production Architecture
```text
Laravel HTML / JavaScript
↓
/risk-api/predict
↓
Nginx
↓
Python prediction service
↓
score.py
↓
Saved HistGradientBoosting pipeline
↓
Cancellation probability
↓
0.40 threshold
↓
High / Low Risk
```
The Python prediction service can run independently as a system service, while Nginx forwards prediction requests from the existing web application to the local Python process.
## Operational Interpretation
The prediction should be treated as a decision-support tool rather than an automatic reservation decision.
A high-risk prediction may be used to prioritise booking confirmation, appropriate deposit or payment follow-up, closer monitoring, and inventory planning.
Bookings should not be automatically cancelled solely because the model predicts a high cancellation probability.
# Monitoring and Retraining
Model performance should be monitored as new booking outcomes become available.
The following should be reviewed periodically:
- Accuracy
- Precision
- Recall
- F1-score
- ROC-AUC
- Distribution of predicted probabilities
- False-positive rate
- False-negative rate
Changes in booking behaviour, channels, customer mix, pricing, or market conditions may cause model performance to deteriorate over time.
Retraining should be considered when a sustained decline in predictive performance is observed, important feature distributions change significantly, new booking channels or customer types appear, or sufficient new labelled booking data becomes available.
Any replacement model should be evaluated chronologically before deployment.
# Limitations
The model does not identify every cancellation. At the selected threshold, some actual cancellations remain false negatives.
False cancellation alerts also remain possible. Some bookings predicted as high risk will ultimately stay.
Payment-event variables were excluded because their timing could introduce future information and therefore data leakage.
Guest age required cleaning and imputation. Imputed values contain less information than genuine observed ages.
Previously unseen categorical values may appear over time. Although the preprocessing pipeline can handle unknown categories, the model has not learned their relationship with cancellation.
The selected 0.40 threshold reflects the current balance between precision and recall. If the operational cost of missed cancellations or false alerts changes, the threshold should be reviewed.
New bookings may also contain numerical values outside the range represented in training data. Such predictions may be less reliable, which is why the production tool includes an out-of-range lead-time warning.
::: {.callout-note}
## Reproducibility Safety
Rendering this report does not modify the trained model, metadata, notebooks, datasets, source code, or existing figures. Quarto only creates or refreshes its own rendered output file, `quarto_report.html`.
:::
# Conclusion
This project developed a complete machine-learning workflow for hotel booking cancellation prediction.
Booking records from two hotels were combined with guest information, while payment-event data was investigated and excluded from final predictors because of potential data leakage.
A simple non-ML heuristic was first used as a benchmark. Logistic Regression, Random Forest, and HistGradientBoosting classifiers were then evaluated using a chronological train-test strategy.
The tuned HistGradientBoostingClassifier was selected as the final model.
At the selected 0.40 threshold, it achieved:
- **83.15% accuracy**
- **71.52% precision**
- **77.45% recall**
- **74.37% F1-score**
- **0.9091 ROC-AUC**
The final model substantially outperformed the heuristic baseline, particularly in recall and F1-score.
The trained model was also incorporated into a lightweight production scoring workflow with input validation, historical guest feature construction, risk classification, and a web interface.
The project therefore demonstrates not only predictive model development but also how the model can be used as a practical cancellation-risk decision-support tool.
Continued monitoring and periodic retraining will be necessary to maintain performance as booking behaviour changes.