Project Report

Introduction

Hotel booking cancellations create challenges for forecasting occupancy, managing room availability, and planning staffing. A hotel may receive many reservations but still face uncertainty about how many guests will actually arrive. Understanding cancellation patterns can help hotel managers identify booking groups that require closer attention when planning operations. This project, “Hotel Booking Cancellations: Exploring Lead Time and Market Segments Across Two Hotels,” explores how cancellations vary between a City Hotel and a Resort Hotel, with particular attention to booking lead time and market segments.

This study draws on the Hotel Booking Demand data set, which contains 119,390 reservations for two Portuguese hotels, a Resort Hotel and a City Hotel, with scheduled arrival dates from July 2015 to August 2017. Each record represents a single booking. The analysis centres on four key variables: hotel type, cancellation status, lead time and market segment. Lead time is the number of days between when a booking was made and the scheduled arrival date. Market segment classifies how the booking was sourced, such as Direct, Corporate, Groups or Online Travel Agents.

Problem Statement:

“This project examines how hotel booking cancellation rates differ between the City Hotel and the Resort Hotel, and how those rates vary by booking lead time and market segment within each hotel.”

For the midterm, we concentrate on data preparation and a proposed exploratory data analysis (EDA) approach. We will assess data quality, prepare the relevant variables and create lead-time groups to support comparisons. The analysis will rely on descriptive statistics, summary tables and visualizations to compare cancellation patterns. Cancellation rate is defined as the number of canceled bookings divided by the total number of bookings in a group. Rates allow fair comparison between groups with different booking volumes, while reporting group sizes helps readers judge how much weight to give results from smaller segments.

The intended audience is hotel managers and revenue management teams. The findings can help them identify groups with higher observed cancellation rates and guide further evaluation of demand forecasts, customer communication and booking policies. The results describe associations rather than establish the causes of cancellations. Because the data covers only two hotels over a specific historical period, findings should be interpreted within that context.

Packages Required

The following packages are used to prepare the hotel booking data and present the results in this report.

  • tidyverse: Imports the CSV file, selects and transforms variables, and calculates summary statistics. Its plotting tools will also support the proposed exploratory analysis.
  • knitr: Formats tables for the HTML report.
  • scales: Formats booking counts and percentages to improve readability.
# Display all analysis code in the HTML report.
knitr::opts_chunk$set(echo = TRUE, fig.width = 8, fig.height = 5)

# Load all packages used in this report.
library(tidyverse)
library(knitr)
library(scales)

Data Preparation

Data Source

We used the hotel booking dataset from TidyTuesday’s February 11, 2020 release.

The dataset contains 119,390 bookings: 79,330 from City Hotel and 40,060 from Resort Hotel. Each row represents a booking, including both canceled and non-canceled reservations. Scheduled arrival dates range from July 2015 through August 2017. February 2020 is the TidyTuesday release date, not the period when the bookings were scheduled to arrive.

The hotel datasets each had 31 variables. TidyTuesday combined them and added a hotel identifier, giving the combined file 32 variables.

For our project, we focus on four variables.

Variable Meaning
hotel City Hotel or Resort Hotel
is_canceled 1 means canceled; 0 means not canceled
lead_time Days between recording the booking and the scheduled arrival
market_segment Market category, such as Direct, Corporate, Groups, or Online TA

In the market-segment labels, TA means Travel Agents and TO means Tour Operators.

Importing the Data

We imported the CSV and named it hotels_raw. We kept this original version so we could compare it with the prepared data later.

Some columns contain NULL entries. We read these, along with blanks and "NA", as missing values in R. A NULL value does not always mean the same thing across variables. For example, a NULL agent identifier may mean that no travel agent was associated with the booking.

# Read the CSV file.
hotels_raw <- read_csv("hotels.csv", na = c("", "NA", "NULL"), show_col_types = FALSE)

# Check whether any values could not be read correctly.
stopifnot(nrow(problems(hotels_raw)) == 0)

# Show the number of rows and columns.
data_size <- data.frame(Bookings = nrow(hotels_raw), Variables = ncol(hotels_raw))

kable(data_size, caption = "Size of the original dataset")
Size of the original dataset
Bookings Variables
119390 32

The file contains 119390 bookings and 32 variables.

Checking Missing Values

We first counted missing values in each column. The table below shows only columns with missing entries.

# Count missing values in each column.
missing_counts <- colSums(is.na(hotels_raw))

# Put the results into a table.
missing_table <- data.frame(Variable = names(missing_counts),
  Missing = as.numeric(missing_counts))

# Display only variables with missing entries.
missing_table <- missing_table[missing_table$Missing > 0, ]

kable(missing_table, caption = "Missing or NULL entries")
Missing or NULL entries
Variable Missing
11 children 4
14 country 488
24 agent 16340
25 company 112593

Missing entries appear in children, country, agent, and company. These variables are not part of our three research questions, so we did not remove bookings because of these entries.

Next, we selected the four variables we need and checked them separately.

# Keep the four variables used in this project.
hotels <- select(hotels_raw, hotel, is_canceled, lead_time, market_segment)

# Count missing values in the selected variables.
selected_missing <- data.frame(Variable = names(hotels), Missing = as.numeric(colSums(is.na(hotels))))

kable(selected_missing, caption = "Missing values in selected variables")
Missing values in selected variables
Variable Missing
hotel 0
is_canceled 0
lead_time 0
market_segment 0

None of these four variables has missing values. We therefore did not need to fill in missing values or remove incomplete records from the selected data.

However, market_segment contains two bookings labeled Undefined. We handle this category in the preparation step below.

Checking Repeated Records

We checked whether any rows matched an earlier row across all 32 original columns.

# Count repeated rows in the full source dataset.
repeated_rows <- sum(duplicated(hotels_raw))

repeated_table <- data.frame(Repeated_rows = repeated_rows)

kable(repeated_table, caption = "Repeated full-row values")
Repeated full-row values
Repeated_rows
31994

We found 31994 repeated rows. We kept them because the file does not contain a unique booking ID. Without that ID, we cannot confirm whether matching rows are accidental copies or different bookings with the same recorded details.

We checked the full dataset rather than only our four selected variables. Different bookings can easily share the same hotel, cancellation status, lead time, and market segment.

Checking the Selected Values

Before preparing the categories, we checked that the hotel names were valid, cancellation status contained only 0 and 1, and lead time had no missing, negative, or infinite values.

# Confirm that the selected variables have no missing values.
stopifnot(!anyNA(hotels))

# Check the two hotel names.
stopifnot(all(hotels$hotel %in% c("City Hotel", "Resort Hotel")))

# Check the cancellation codes.
stopifnot(all(hotels$is_canceled %in% c(0, 1)))

# Check lead time.
stopifnot(all(is.finite(hotels$lead_time)))
stopifnot(all(hotels$lead_time >= 0))

These checks stop the report if an unexpected value is found. If they pass, R continues without printing a result.

We kept is_canceled numeric. Because it is coded as 0 and 1, its mean will give the cancellation rate when we calculate group summaries later.

Preparing Categories

We renamed the Undefined market segment as Unknown. This keeps the two bookings in the dataset while making their category easier to understand.

We also converted hotel and market segment to factors because they represent categories.

# Rename the Undefined market segment.
hotels$market_segment[hotels$market_segment == "Undefined"] <- "Unknown"

# Set categorical variables as factors.
hotels$hotel <- factor(hotels$hotel, levels = c("City Hotel", "Resort Hotel"))

hotels$market_segment <- factor(hotels$market_segment)

Creating Lead-Time Groups

We divided lead time into five groups to make the planned comparisons easier to read:

  • 0–7 days
  • 8–30 days
  • 31–90 days
  • 91–180 days
  • 181 or more days

These intervals separate bookings made close to arrival from those made several months ahead. They are choices for this analysis, not established risk categories. We kept the original numeric lead time so we can use it again if we decide to change the groups.

# Create ordered lead-time categories.
hotels$lead_time_group <- cut(hotels$lead_time, breaks = c(-Inf, 7, 30, 90, 180, Inf), labels = c("0–7 days", "8–30 days", "31–90 days", "91–180 days", "181+ days"), ordered_result = TRUE)

# Check that every booking has a lead-time group.
stopifnot(!anyNA(hotels$lead_time_group))

# Check that no booking records were lost.
stopifnot(nrow(hotels) == nrow(hotels_raw))

We did not remove long lead times just because they were large. They are relevant to our second research question, and we do not have evidence that they are recording errors.

Preview of the Prepared Data

After preparation, the dataset contains 119390 bookings and 5 variables. The additional variable is lead_time_group.

The first six rows are shown below.

# Display a small sample of the prepared data.
kable(head(hotels, 6), caption = "First six rows of the prepared dataset")
First six rows of the prepared dataset
hotel is_canceled lead_time market_segment lead_time_group
Resort Hotel 0 342 Direct 181+ days
Resort Hotel 0 737 Direct 181+ days
Resort Hotel 0 7 Direct 0–7 days
Resort Hotel 0 13 Corporate 8–30 days
Resort Hotel 0 14 Online TA 8–30 days
Resort Hotel 0 14 Online TA 8–30 days

Summary of the Selected Variables

The following tables describe the prepared data before we begin comparing cancellation rates.

First, we counted the bookings for each hotel.

# Count bookings by hotel.
hotel_counts <- as.data.frame(table(hotels$hotel))
names(hotel_counts) <- c("Hotel", "Bookings")

kable(hotel_counts, caption = "Bookings by hotel")
Bookings by hotel
Hotel Bookings
City Hotel 79330
Resort Hotel 40060

City Hotel has more bookings than Resort Hotel. This is why we will compare cancellation rates rather than only the number of cancellations.

Next, we summarized cancellation status and lead time.

# Count canceled and non-canceled bookings.
status_counts <- data.frame(Status = c("Not canceled", "Canceled"), Bookings = c(sum(hotels$is_canceled == 0), sum(hotels$is_canceled == 1)))

kable(status_counts, caption = "Booking status")
Booking status
Status Bookings
Not canceled 75166
Canceled 44224
# Calculate basic lead-time statistics.
lead_summary <- data.frame(Minimum = min(hotels$lead_time), Median = median(hotels$lead_time), Mean = round(mean(hotels$lead_time), 1), Maximum = max(hotels$lead_time))

kable(lead_summary, caption = "Lead time in days")
Lead time in days
Minimum Median Mean Maximum
0 69 104 737

Lead time ranges from 0 to 737 days. The mean is 104 days, while the median is 69 days. The difference shows why it is useful to look at more than one summary measure.

Finally, we counted bookings in each market segment and lead-time group.

# Count bookings in each market segment.
segment_counts <- as.data.frame(table(hotels$market_segment))
names(segment_counts) <- c("Market_segment", "Bookings")

kable(segment_counts, caption = "Bookings by market segment")
Bookings by market segment
Market_segment Bookings
Aviation 237
Complementary 743
Corporate 5295
Direct 12606
Groups 19811
Offline TA/TO 24219
Online TA 56477
Unknown 2

The market segments differ in size. The Unknown category has only two bookings, so we will treat it separately when interpreting segment comparisons.

# Count bookings in each lead-time group.
lead_group_counts <- as.data.frame(table(hotels$lead_time_group))
names(lead_group_counts) <- c("Lead_time_group", "Bookings")

kable(lead_group_counts, caption = "Bookings by lead-time group")
Bookings by lead-time group
Lead_time_group Bookings
0–7 days 19746
8–30 days 18960
31–90 days 29553
91–180 days 26439
181+ days 24692

These counts help us check the size of the groups that will be used in the proposed analysis.

Visual Overview

We included two plots to show the composition of the data before examining cancellation rates.

Booking Counts by Hotel

ggplot(hotel_counts, aes(x = Hotel, y = Bookings, fill = Hotel)) + geom_col(width = 0.6, show.legend = FALSE) + scale_y_continuous(labels = scales::label_comma()) + labs(title = "Number of Bookings by Hotel", x = NULL, y = "Number of bookings") + theme_minimal()

The bar chart shows the difference in booking volume between the two hotels. This provides context for the rate comparisons we plan to make.

Distribution of Lead Time

ggplot(hotels, aes(x = lead_time)) + geom_histogram(binwidth = 30, boundary = 0, fill = "steelblue", color = "white") + scale_y_continuous(labels = scales::label_comma()) + labs(title = "Distribution of Booking Lead Time", x = "Lead time (days)", y = "Number of bookings") + theme_minimal()

The histogram shows the distribution across the full range of lead times. Its bars represent 30-day intervals. These intervals are used for the plot only and are separate from the five lead-time groups created for our research question.

Proposed Exploratory Data Analysis

Planned Analytical Approach

We plan to explore cancellation rates between the two hotels and across lead-time groups and market segments within each hotel. These grouped comparisons may reveal patterns that are not apparent from the overall dataset.

For each group, we will calculate total bookings, canceled bookings, and the cancellation rate:

\[ \text{Cancellation rate} = \frac{\text{Canceled bookings}}{\text{Total bookings}} \]

Cancellation rates will be displayed as percentages. The denominator will include all canceled and non-canceled bookings in the corresponding group.

We will report group sizes alongside rates because percentages based on a few bookings require more cautious interpretation than percentages based on many bookings. No external data joins are currently planned because the variables required for the three questions are available in the prepared dataset.

Question 1: How Do Cancellation Rates Differ Between City Hotel and Resort Hotel?

We will group bookings by hotel and calculate total bookings, canceled bookings, and cancellation rates. A summary table will display these measures, and a bar chart will compare the two rates using the same percentage scale.

Using rates will account for the different booking volumes. We will describe the difference between hotels in percentage points. This comparison will provide a starting point for examining lead time and market segments, rather than establish why any difference exists.

Question 2: How Does Booking Lead Time Relate to Cancellation Rates Within Each Hotel?

We will group bookings by hotel and the five lead-time categories created during data preparation. For each combination, we will calculate the number of bookings and the cancellation rate.

A summary table will report the counts and rates. A line chart with a separate line for each hotel will show how cancellation rates vary across the ordered categories. We will examine whether the patterns are similar between the hotels and whether rates increase, decrease, or follow a more complex pattern.

The categories have unequal widths, so the connecting lines will indicate their order rather than daily changes in cancellation risk. We may compare alternative group boundaries to assess whether the observed pattern depends on the selected intervals.

Question 3: How Do Cancellation Rates Vary Across Market Segments Within Each Hotel?

We will calculate booking counts and cancellation rates for each combination of hotel and market segment. A summary table and horizontal bar charts with separate panels for the hotels will support comparisons within and across hotels.

The panels will use the same percentage scale. We will consider the number of bookings in each segment when interpreting differences.

The Unknown category will remain in the table but will be treated separately from identified market segments. A segment with no bookings in a hotel will be treated as unavailable rather than assigned a cancellation rate of zero.

If useful, we will also examine lead time within selected market segments to explore whether differences in advance-booking behavior provide additional context.

Alternate Option

Analytical Approach

To address the overarching problem, the analysis proceeds in three steps, moving from the broadest comparison (hotel type) to more detailed ones (lead time and market segment within each hotel).

Step 1: Cancellation Rates by Hotel

We will group bookings by hotel and calculate total bookings, canceled bookings and cancellation rates. A summary table will present these measures, and a bar chart will compare the two rates on the same percentage scale. Because rates account for differing booking volumes, the difference between hotels will be described in percentage points. This comparison is a starting point for the lead-time and market-segment analysis, not an explanation of why any difference exists.

Cancellation Rates by Lead Time

Within each hotel, we will group bookings into the five lead-time categories created during data preparation and calculate the number of bookings and cancellation rate for each category. A summary table will report these figures, and a line chart with one line per hotel will show how cancellation rates change across the ordered categories. We will examine whether the two hotels follow similar patterns and whether rates rise, fall or follow a more complex path. Because the categories have unequal widths, the connecting lines indicate order rather than daily changes in cancellation risk. We may also test alternative group boundaries to check that the pattern does not depend on the intervals chosen.

Cancellation Rates by Market Segment

Within each hotel, we will calculate booking counts and cancellation rates for each market segment. A summary table and horizontal bar charts, with a separate panel for each hotel on the same percentage scale, will support comparisons within and across hotels. Segment size will be considered when interpreting differences. The Unknown category will remain in the table but be treated separately from identified segments, and a segment with no bookings in a hotel will be reported as unavailable rather than given a rate of zero. If useful, we will also examine lead time within selected segments to see whether differences in advance-booking behavior add context.

Planned Tables and Visualizations

Research question Planned table Planned visualization
Cancellation rates between hotels Total bookings, canceled bookings, and rates by hotel Bar chart comparing the two hotels
Lead time and cancellations within hotels Booking counts and rates by hotel and lead-time group Line chart across ordered lead-time categories
Market segments and cancellations within hotels Booking counts and rates by hotel and market segment Horizontal bar charts with separate hotel panels

Skills to Develop

To complete the proposed analysis, we need to strengthen our understanding of how to choose lead-time group boundaries and evaluate whether different boundaries change the interpretation.

We also need to learn more about interpreting cancellation rates for small groups and constructing confidence intervals for proportions. These skills would help us communicate uncertainty rather than rely only on differences between percentages.

If we include a statistical model in the final project, we will need to develop our ability to fit and interpret logistic regression.

Plans for Machine Learning or Statistical Modeling

This midterm submission does not include a machine learning model. Descriptive statistics, grouped summaries and visualizations are the main methods proposed for answering the research questions.

For the final project, we may consider logistic regression, since cancellation status is a binary outcome. A possible model would examine cancellation status in relation to hotel, lead time and market segment simultaneously. This extension has not yet been implemented, and the decision to include it will depend on the final project requirements and on whether it adds useful information beyond the descriptive comparisons. Any model would be interpreted as describing associations, not proving causation.

Limitations and Next Steps

The dataset covers only two hotels over a specific historical period, so the findings may not generalize to other properties or to current booking behavior. The analysis is also observational, which means it can identify associations but cannot establish the causes of cancellations.

Repeated full-row values are retained because the dataset has no unique booking identifier, so identical records cannot be reliably distinguished from duplicates. Results for small market segments, and the choice of lead-time boundaries, will require careful interpretation.

At the midterm stage, the data have been imported, checked, prepared and summarized. The next steps are to produce the proposed tables and visualizations, evaluate the resulting patterns and decide whether further analysis would add value.

References

TidyTuesday. (2020). Hotels 2020. Dataset documentation and preparation code.