Hotel Booking Cancellation Prediction

Machine Learning & Predictive Analysis

Author

Hamid Yameen

Published

August 22, 2026

1 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.

1.1 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.

NotePrediction 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.

1.2 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.

Code
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"])
Rows: 119210
Model: HistGradientBoostingClassifier
Threshold: 0.4
TipRead-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.

2 Dataset and Data Sources

2.1 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.

2.2 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.

2.3 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.

Figure 1: Timing of payment events relative to booking date

As shown in Figure 1, payment-event variables were excluded from the final predictive model to reduce the risk of data leakage.

WarningWhy 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.

2.4 Target Variable

The prediction target is IsCanceled:

  • 0 = booking not cancelled
  • 1 = booking cancelled

Cancellation (1) is treated as the positive class.

3 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.

3.1 Cancellation Distribution

The final modelling dataset contained 119,210 bookings, of which 75,011 were not cancelled and 44,199 were cancelled.

Code
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
Status Bookings
0 Not Cancelled 75011
1 Cancelled 44199
Figure 2: Booking cancellation distribution

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.

3.2 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.

Figure 3: Cancellation rate by lead-time group

The relationship in Figure 3 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.

3.3 Deposit Type and Cancellation

Deposit type showed one of the strongest relationships with cancellation behaviour.

Figure 4: Cancellation rate by deposit type

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.

ImportantImportant 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.

3.4 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.

3.5 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.

3.6 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:

Code
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(),
    ]
})
Dataset Rows Cancellation Rate
0 Training 95278 0.384632
1 Testing 23932 0.315561

4 Data Cleaning and Feature Engineering

The raw data required several cleaning and transformation steps before modelling.

4.1 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.

4.2 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.

4.3 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.

4.4 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.

NoteLeakage 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.

4.5 Final Model Features

The final model used 29 input features: 19 numerical and 10 categorical.

4.5.1 Numerical Features

  • LeadTime
  • ArrivalDateYear
  • ArrivalDateWeekNumber
  • ArrivalDateDayOfMonth
  • StaysInWeekendNights
  • StaysInWeekNights
  • Adults
  • Children
  • Babies
  • DaysInWaitingList
  • ADR
  • RequiredCarParkingSpaces
  • TotalOfSpecialRequests
  • TotalNights
  • TotalGuests
  • age
  • PreviousBookings
  • PreviousCancellations
  • PreviousCancellationRate

4.5.2 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.

5 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.

6 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.

6.1 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.

6.2 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.

6.3 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.

7 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:

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.

8 Classification Threshold Selection

predict_proba() returns the estimated probability of cancellation rather than a final class.

For example:

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%.

ImportantSelected 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.

9 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.

Code
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
Metric Value
0 Accuracy 0.831523
1 Precision 0.715212
2 Recall 0.774497
3 F1 Score 0.743675
4 ROC-AUC 0.909096

9.1 Accuracy

Accuracy of 83.15% means that approximately 83% of all test bookings were classified correctly.

9.2 Precision

Precision of 71.52% means that, among bookings classified as cancellation risk, approximately 71.5% actually cancelled.

9.3 Recall

Recall of 77.45% means that the model successfully identified approximately 77.5% of all actual cancellations.

9.4 F1-score

The F1-score of 74.37% summarises the balance between precision and recall.

9.5 Confusion Matrix

The final confusion matrix contained:

  • 14,051 True Negatives
  • 2,329 False Positives
  • 1,703 False Negatives
  • 5,849 True Positives
Code
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],
})
Outcome Count
0 True Negative 14051
1 False Positive 2329
2 False Negative 1703
3 True Positive 5849
Figure 5: Final model confusion matrix

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.

9.6 ROC Curve

The final ROC-AUC was 0.9091.

Figure 6: ROC curve for the final model

ROC-AUC evaluates the model’s ability to rank cancelled bookings above non-cancelled bookings across possible thresholds.

9.7 Precision-Recall Curve

The model achieved an Average Precision score of approximately 0.8339.

Figure 7: Precision-recall curve

9.8 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
Figure 8: Heuristic baseline versus final model

The largest improvement was in recall. The non-ML heuristic detected only 26.66% of actual cancellations, whereas the final model detected 77.45%.

10 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.

{
  "model": "HistGradientBoostingClassifier",
  "threshold": 0.4,
  "target": "IsCanceled",
  "positive_class": 1,
  "lead_time_min": 0,
  "lead_time_max": 737
}

10.1 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:

{
  "cancellation_probability": 0.0691,
  "prediction": 0,
  "decision": "Low cancellation risk",
  "threshold": 0.4,
  "warnings": []
}

10.2 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.

Code
# Already loaded in the setup cell using json.load(..., "r")
metadata
{'model': 'HistGradientBoostingClassifier',
 'threshold': 0.4,
 'target': 'IsCanceled',
 'positive_class': 1,
 'lead_time_min': 0,
 'lead_time_max': 737}

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.

10.3 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.

10.4 Production Architecture

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.

10.5 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.

11 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.

12 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.

NoteReproducibility 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.

13 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.