Hotel reservations are called off either due to cancellations or no-shows as a result of some common reasons such as a change of plans, scheduling conflicts and many more. Typically, hotels provide an option to do so at no or low cost which would be beneficial for guests but not ideal for hotels as it affects the planning of number of available rooms - which ends up with potentially lower rates of room utilisation.
The problem statement: Predict if a customer is going to honor the reservation or cancel it.
hotel_raw = read.csv("Hotel Reservations.csv")
## Booking_ID no_of_adults
## 0 0
## no_of_children no_of_weekend_nights
## 0 0
## no_of_week_nights type_of_meal_plan
## 0 0
## required_car_parking_space room_type_reserved
## 0 0
## lead_time arrival_year
## 0 0
## arrival_month arrival_date
## 0 0
## market_segment_type repeated_guest
## 0 0
## no_of_previous_cancellations no_of_previous_bookings_not_canceled
## 0 0
## avg_price_per_room no_of_special_requests
## 0 0
## booking_status
## 0
No NA values are found after checking through ‘is.na’. Next, we found no duplicated in our data set with ‘get_dupes’.
## [1] Booking_ID no_of_adults
## [3] no_of_children no_of_weekend_nights
## [5] no_of_week_nights type_of_meal_plan
## [7] required_car_parking_space room_type_reserved
## [9] lead_time arrival_year
## [11] arrival_month arrival_date
## [13] market_segment_type repeated_guest
## [15] no_of_previous_cancellations no_of_previous_bookings_not_canceled
## [17] avg_price_per_room no_of_special_requests
## [19] booking_status dupe_count
## <0 rows> (or 0-length row.names)
## 'data.frame': 36275 obs. of 19 variables:
## $ Booking_ID : chr "INN00001" "INN00002" "INN00003" "INN00004" ...
## $ no_of_adults : int 2 2 1 2 2 2 2 2 3 2 ...
## $ no_of_children : int 0 0 0 0 0 0 0 0 0 0 ...
## $ no_of_weekend_nights : int 1 2 2 0 1 0 1 1 0 0 ...
## $ no_of_week_nights : int 2 3 1 2 1 2 3 3 4 5 ...
## $ type_of_meal_plan : chr "Meal Plan 1" "Not Selected" "Meal Plan 1" "Meal Plan 1" ...
## $ required_car_parking_space : int 0 0 0 0 0 0 0 0 0 0 ...
## $ room_type_reserved : chr "Room_Type 1" "Room_Type 1" "Room_Type 1" "Room_Type 1" ...
## $ lead_time : int 224 5 1 211 48 346 34 83 121 44 ...
## $ arrival_year : int 2017 2018 2018 2018 2018 2018 2017 2018 2018 2018 ...
## $ arrival_month : int 10 11 2 5 4 9 10 12 7 10 ...
## $ arrival_date : int 2 6 28 20 11 13 15 26 6 18 ...
## $ market_segment_type : chr "Offline" "Online" "Online" "Online" ...
## $ repeated_guest : int 0 0 0 0 0 0 0 0 0 0 ...
## $ no_of_previous_cancellations : int 0 0 0 0 0 0 0 0 0 0 ...
## $ no_of_previous_bookings_not_canceled: int 0 0 0 0 0 0 0 0 0 0 ...
## $ avg_price_per_room : num 65 106.7 60 100 94.5 ...
## $ no_of_special_requests : int 0 1 0 0 0 1 1 1 1 3 ...
## $ booking_status : chr "Not_Canceled" "Not_Canceled" "Canceled" "Canceled" ...
We then converted the relevant variables as factors.
## 'data.frame': 36275 obs. of 19 variables:
## $ Booking_ID : chr "INN00001" "INN00002" "INN00003" "INN00004" ...
## $ no_of_adults : int 2 2 1 2 2 2 2 2 3 2 ...
## $ no_of_children : int 0 0 0 0 0 0 0 0 0 0 ...
## $ no_of_weekend_nights : int 1 2 2 0 1 0 1 1 0 0 ...
## $ no_of_week_nights : int 2 3 1 2 1 2 3 3 4 5 ...
## $ type_of_meal_plan : Factor w/ 4 levels "Meal Plan 1",..: 1 4 1 1 4 2 1 1 1 1 ...
## $ required_car_parking_space : Factor w/ 2 levels "0","1": 1 1 1 1 1 1 1 1 1 1 ...
## $ room_type_reserved : Factor w/ 7 levels "Room_Type 1",..: 1 1 1 1 1 1 1 4 1 4 ...
## $ lead_time : int 224 5 1 211 48 346 34 83 121 44 ...
## $ arrival_year : int 2017 2018 2018 2018 2018 2018 2017 2018 2018 2018 ...
## $ arrival_month : int 10 11 2 5 4 9 10 12 7 10 ...
## $ arrival_date : int 2 6 28 20 11 13 15 26 6 18 ...
## $ market_segment_type : Factor w/ 5 levels "Aviation","Complementary",..: 4 5 5 5 5 5 5 5 4 5 ...
## $ repeated_guest : Factor w/ 2 levels "0","1": 1 1 1 1 1 1 1 1 1 1 ...
## $ no_of_previous_cancellations : int 0 0 0 0 0 0 0 0 0 0 ...
## $ no_of_previous_bookings_not_canceled: int 0 0 0 0 0 0 0 0 0 0 ...
## $ avg_price_per_room : num 65 106.7 60 100 94.5 ...
## $ no_of_special_requests : int 0 1 0 0 0 1 1 1 1 3 ...
## $ booking_status : Factor w/ 2 levels "Canceled","Not_Canceled": 2 2 1 1 1 1 2 2 2 2 ...
We also removed booking ID as it is not relevant for our analysis
## 'data.frame': 36275 obs. of 18 variables:
## $ no_of_adults : int 2 2 1 2 2 2 2 2 3 2 ...
## $ no_of_children : int 0 0 0 0 0 0 0 0 0 0 ...
## $ no_of_weekend_nights : int 1 2 2 0 1 0 1 1 0 0 ...
## $ no_of_week_nights : int 2 3 1 2 1 2 3 3 4 5 ...
## $ type_of_meal_plan : Factor w/ 4 levels "Meal Plan 1",..: 1 4 1 1 4 2 1 1 1 1 ...
## $ required_car_parking_space : Factor w/ 2 levels "0","1": 1 1 1 1 1 1 1 1 1 1 ...
## $ room_type_reserved : Factor w/ 7 levels "Room_Type 1",..: 1 1 1 1 1 1 1 4 1 4 ...
## $ lead_time : int 224 5 1 211 48 346 34 83 121 44 ...
## $ arrival_year : int 2017 2018 2018 2018 2018 2018 2017 2018 2018 2018 ...
## $ arrival_month : int 10 11 2 5 4 9 10 12 7 10 ...
## $ arrival_date : int 2 6 28 20 11 13 15 26 6 18 ...
## $ market_segment_type : Factor w/ 5 levels "Aviation","Complementary",..: 4 5 5 5 5 5 5 5 4 5 ...
## $ repeated_guest : Factor w/ 2 levels "0","1": 1 1 1 1 1 1 1 1 1 1 ...
## $ no_of_previous_cancellations : int 0 0 0 0 0 0 0 0 0 0 ...
## $ no_of_previous_bookings_not_canceled: int 0 0 0 0 0 0 0 0 0 0 ...
## $ avg_price_per_room : num 65 106.7 60 100 94.5 ...
## $ no_of_special_requests : int 0 1 0 0 0 1 1 1 1 3 ...
## $ booking_status : Factor w/ 2 levels "Canceled","Not_Canceled": 2 2 1 1 1 1 2 2 2 2 ...
To check for anomalies, we plotted variables (lead_time, arrival_date, avg_price_per_room, no_of_previous_bookings_not_canceled))
We observed that lead_time also has outliers, we’ll normalise it by logging the variable
To create a correlation table, we recoded certain variables to categorical values namely:
## no_of_adults no_of_children no_of_weekend_nights
## no_of_adults 1.00 -0.02 0.10
## no_of_children -0.02 1.00 0.03
## no_of_weekend_nights 0.10 0.03 1.00
## no_of_week_nights 0.11 0.02 0.18
## type_of_meal_plan 0.00 0.04 -0.02
## required_car_parking_space 0.01 0.03 -0.03
## no_of_week_nights type_of_meal_plan
## no_of_adults 0.11 0.00
## no_of_children 0.02 0.04
## no_of_weekend_nights 0.18 -0.02
## no_of_week_nights 1.00 0.03
## type_of_meal_plan 0.03 1.00
## required_car_parking_space -0.05 -0.02
## required_car_parking_space room_type_reserved
## no_of_adults 0.01 0.27
## no_of_children 0.03 0.36
## no_of_weekend_nights -0.03 0.06
## no_of_week_nights -0.05 0.09
## type_of_meal_plan -0.02 0.09
## required_car_parking_space 1.00 0.04
## lead_time arrival_year arrival_month arrival_date
## no_of_adults 0.10 0.08 0.02 0.03
## no_of_children -0.05 0.05 0.00 0.03
## no_of_weekend_nights 0.05 0.06 -0.01 0.03
## no_of_week_nights 0.15 0.03 0.04 -0.01
## type_of_meal_plan 0.23 -0.19 0.02 0.02
## required_car_parking_space -0.07 0.02 -0.02 0.00
## market_segment_type repeated_guest
## no_of_adults -0.10 -0.19
## no_of_children 0.07 -0.04
## no_of_weekend_nights -0.02 -0.07
## no_of_week_nights -0.07 -0.10
## type_of_meal_plan -0.22 0.01
## required_car_parking_space 0.12 0.11
## no_of_previous_cancellations
## no_of_adults -0.05
## no_of_children -0.02
## no_of_weekend_nights -0.02
## no_of_week_nights -0.03
## type_of_meal_plan -0.01
## required_car_parking_space 0.03
## no_of_previous_bookings_not_canceled
## no_of_adults -0.12
## no_of_children -0.02
## no_of_weekend_nights -0.03
## no_of_week_nights -0.05
## type_of_meal_plan 0.01
## required_car_parking_space 0.06
## avg_price_per_room no_of_special_requests
## no_of_adults 0.30 0.19
## no_of_children 0.34 0.12
## no_of_weekend_nights 0.00 0.06
## no_of_week_nights 0.02 0.05
## type_of_meal_plan 0.13 -0.09
## required_car_parking_space 0.06 0.09
## booking_status
## no_of_adults 0.09
## no_of_children 0.03
## no_of_weekend_nights 0.06
## no_of_week_nights 0.09
## type_of_meal_plan 0.05
## required_car_parking_space -0.09
## Var1 Var2 value
## 1 no_of_adults no_of_adults 1.00
## 2 no_of_children no_of_adults -0.02
## 3 no_of_weekend_nights no_of_adults 0.10
## 4 no_of_week_nights no_of_adults 0.11
## 5 type_of_meal_plan no_of_adults 0.00
## 6 required_car_parking_space no_of_adults 0.01
Upon viewing the correlation heatmap, we have identified several relationships that we want to delve deeper into, namely:
We’ll look into the first 3 pairwise relationships below:
Lead time vs booking_status: As seen above, the lead time for cancelled rooms is higher than that for rooms that were not cancelled. This means that customers who book their rooms early are more likely to cancel their rooms.
No. of special requests vs booking status: From this pair, it can be seen that if the number of special requests is more than 2, the booking will not be cancelled. It is also observed that in general, people usually make 1 or no special requests, whether or not the booking is cancelled.
No of weekend nights vs booking status
It can be seen that it is more likely for the booking to be cancelled when there is a higher number of weekend nights. This may be due to change in plans seeing that there is a long vacation.
It can be seen that the average price per room of cancelled rooms is
slightly higher than that of not cancelled.
A large proportion of customers are from the online segment. Also, it can be seen that no one from the complimentary segment cancelled their booking.
In general, the years do not affect the customer’s decision to cancel their booking or not.
A large proportion of customers who did not cancel their bookings did not require a car parking space,but it’s interesting to note that a significant proportion of customers who canceled did not require a car parking space as well.
Models used are CART, logistic regression, and random forest.
Rationale for not using other models:
We first split the data into Train and Test respectively and normalised the data using preProcess function before proceeding with our analysis.
## no_of_adults no_of_children no_of_weekend_nights no_of_week_nights
## Min. :-3.5472 Min. :-0.2633 Min. :-0.9321 Min. :-1.5649
## 1st Qu.: 0.3003 1st Qu.:-0.2633 1st Qu.:-0.9321 1st Qu.:-0.8571
## Median : 0.3003 Median :-0.2633 Median : 0.2164 Median :-0.1493
## Mean : 0.0000 Mean : 0.0000 Mean : 0.0000 Mean : 0.0000
## 3rd Qu.: 0.3003 3rd Qu.:-0.2633 3rd Qu.: 1.3650 3rd Qu.: 0.5585
## Max. : 4.1478 Max. :24.4107 Max. : 7.1076 Max. :10.4679
##
## type_of_meal_plan required_car_parking_space room_type_reserved
## Meal Plan 1 :19518 0:24618 Room_Type 1:19685
## Meal Plan 2 : 2319 1: 775 Room_Type 2: 480
## Meal Plan 3 : 4 Room_Type 3: 6
## Not Selected: 3552 Room_Type 4: 4219
## Room_Type 5: 190
## Room_Type 6: 699
## Room_Type 7: 114
## lead_time arrival_year arrival_month arrival_date
## Min. :-0.9946 Min. :-2.1434 Min. :-2.0942 Min. :-1.66387
## 1st Qu.:-0.7973 1st Qu.: 0.4665 1st Qu.:-0.7890 1st Qu.:-0.86375
## Median :-0.3214 Median : 0.4665 Median : 0.1899 Median : 0.05068
## Mean : 0.0000 Mean : 0.0000 Mean : 0.0000 Mean : 0.00000
## 3rd Qu.: 0.4910 3rd Qu.: 0.4665 3rd Qu.: 0.8425 3rd Qu.: 0.85080
## Max. : 4.1468 Max. : 0.4665 Max. : 1.4950 Max. : 1.76522
##
## market_segment_type repeated_guest no_of_previous_cancellations
## Aviation : 86 0:24756 Min. :-0.06115
## Complementary: 293 1: 637 1st Qu.:-0.06115
## Corporate : 1409 Median :-0.06115
## Offline : 7412 Mean : 0.00000
## Online :16193 3rd Qu.:-0.06115
## Max. :36.05056
##
## no_of_previous_bookings_not_canceled avg_price_per_room no_of_special_requests
## Min. :-0.08837 Min. :-2.9341 Min. :-0.7873
## 1st Qu.:-0.08837 1st Qu.:-0.6481 1st Qu.:-0.7873
## Median :-0.08837 Median :-0.1102 Median :-0.7873
## Mean : 0.00000 Mean : 0.0000 Mean : 0.0000
## 3rd Qu.:-0.08837 3rd Qu.: 0.4801 3rd Qu.: 0.4791
## Max. :35.05977 Max. : 7.6963 Max. : 5.5448
##
## booking_status
## Canceled : 8320
## Not_Canceled:17073
##
##
##
##
##
To begin building the Logistic Regression model, we first run the most basic model with all the variables to identify significant variables.
##
## Call:
## glm(formula = booking_status ~ ., family = "binomial", data = Train)
##
## Deviance Residuals:
## Min 1Q Median 3Q Max
## -3.0671 -0.6067 0.3315 0.6573 2.8576
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) 9.674e+02 1.221e+02 7.922 2.34e-15 ***
## no_of_adults -8.753e-02 3.759e-02 -2.329 0.01988 *
## no_of_children -1.635e-01 5.907e-02 -2.767 0.00565 **
## no_of_weekend_nights -1.408e-01 1.981e-02 -7.105 1.20e-12 ***
## no_of_week_nights -3.507e-02 1.224e-02 -2.865 0.00417 **
## type_of_meal_planMeal Plan 2 -9.719e-02 6.655e-02 -1.460 0.14419
## type_of_meal_planMeal Plan 3 -1.195e+01 1.608e+02 -0.074 0.94076
## type_of_meal_planNot Selected -2.193e-01 5.340e-02 -4.106 4.02e-05 ***
## required_car_parking_space1 1.572e+00 1.413e-01 11.128 < 2e-16 ***
## room_type_reservedRoom_Type 2 4.214e-01 1.343e-01 3.138 0.00170 **
## room_type_reservedRoom_Type 3 2.613e-01 1.366e+00 0.191 0.84828
## room_type_reservedRoom_Type 4 2.738e-01 5.336e-02 5.132 2.86e-07 ***
## room_type_reservedRoom_Type 5 5.604e-01 1.992e-01 2.814 0.00490 **
## room_type_reservedRoom_Type 6 1.032e+00 1.489e-01 6.930 4.21e-12 ***
## room_type_reservedRoom_Type 7 1.323e+00 3.144e-01 4.207 2.59e-05 ***
## lead_time -1.569e-02 2.665e-04 -58.880 < 2e-16 ***
## arrival_year -4.781e-01 6.051e-02 -7.901 2.76e-15 ***
## arrival_month 4.578e-02 6.484e-03 7.061 1.66e-12 ***
## arrival_date -9.968e-04 1.940e-03 -0.514 0.60728
## market_segment_typeComplementary 1.961e+01 2.234e+02 0.088 0.93005
## market_segment_typeCorporate 1.127e+00 2.702e-01 4.170 3.05e-05 ***
## market_segment_typeOffline 1.939e+00 2.579e-01 7.519 5.52e-14 ***
## market_segment_typeOnline 2.110e-01 2.549e-01 0.828 0.40765
## repeated_guest1 2.754e+00 6.576e-01 4.188 2.82e-05 ***
## no_of_previous_cancellations -2.333e-01 8.914e-02 -2.617 0.00886 **
## no_of_previous_bookings_not_canceled 4.461e-02 8.980e-02 0.497 0.61938
## avg_price_per_room -1.849e-02 7.356e-04 -25.135 < 2e-16 ***
## no_of_special_requests 1.490e+00 3.020e-02 49.330 < 2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 32122 on 25392 degrees of freedom
## Residual deviance: 21494 on 25365 degrees of freedom
## AIC: 21550
##
## Number of Fisher Scoring iterations: 16
Next, we prepare for cross validation.
fitControl = trainControl(method = "cv", number = 10, classProbs = TRUE, summaryFunction = twoClassSummary)
First, we tried using only variables with *** significance, giving an ROC of 0.8631703.
## Generalized Linear Model
##
## 25393 samples
## 11 predictor
## 2 classes: 'Canceled', 'Not_Canceled'
##
## No pre-processing
## Resampling: Cross-Validated (10 fold)
## Summary of sample sizes: 22853, 22854, 22854, 22853, 22854, 22854, ...
## Resampling results:
##
## ROC Sens Spec
## 0.8631703 0.6282452 0.8916417
Second, we tried using only variables with *** and ** significance, giving an ROC of 0.8633096.
Lastly, we also tried one model with variables of three, two, and one star significance, as well as another model with all the variables. However, we found that the model with only *** and ** significance produced the highest ROC. Hence, we will use this model to move forward with data transformation.
For data transformation, we tested the linearity assumption for each of the variables. We determined that the following variables required transformation: 1. no_of_children
By making a table of no_of_children against booking_status, we see possible right skewness, hence we perform sqrt(no_of_children).
From the plot, we see that data is not completely linear, hence we tried
sqrt and log transformations. sqrt(lead_time) gave a better ROC
result.
By making a table of no_of_previous_cancellations against booking_status, we see possible right skewness, hence we perform sqrt(no_of_previous_cancellations).
From the plot, we see possible right skewness, hence we perform
sqrt(avg_price_per_room).
By making a table of no_of_special_requests against booking_status, we see possible changing rate of decrease, hence we performed log1p(no_of_special_requests).
Finally, we have the resulting model with an ROC of 0.8653404.
set.seed(1)
train(booking_status ~ sqrt(no_of_children) + no_of_weekend_nights + no_of_week_nights + type_of_meal_plan + required_car_parking_space + room_type_reserved + sqrt(lead_time) + arrival_year + arrival_month + market_segment_type + repeated_guest + sqrt(no_of_previous_cancellations) + sqrt(avg_price_per_room) + log1p(no_of_special_requests), data = Train, method = "glm", family = "binomial", trControl = fitControl, metric = "ROC")
Out of Sample Testing: gives an ROC of 0.8650141
We will start off the CART model by creating a model with all independent variables apart from BookingID that was removed previously. According to the CART model, the important variables are lead time; number of special requests; average price per room; market segment type; and the number of adults. The AUC of this model is 82.18%.
## [1] 0.8217722
From this, we will utilise 10-fold cross validation on our model to obtain the optimal cp value achieve higher prediction accuracy.
library(e1071)
fitControl = trainControl(method = "cv", number = 10)
cpGrid = expand.grid(.cp = (0:100)*0.0000005)
set.seed(123)
cvResults = train(booking_status ~., data = normtrain, method = "rpart", trControl = fitControl, tuneGrid = cpGrid)
cvResults
predictCARTCV1 = predict(hotelTreeCv, data = normtrain)
predCARTCV1 = prediction(predictCARTCV1[,2], normtrain$booking_status)
ROCRaucCARTcv1 = performance(predCARTCV1,"auc")
ROCRaucCARTcv1@y.values[[1]]
The in sample AUC is 96.14%
The optimal cp value is 5e-05 or 0.0000005. We will now proceed to train the model using the optimal CP. The accuracy of the new CART model is 92.34%.As seen above, the CART model is very large, and this is due to the small data set which has resulted in overfitting.
We will start off the default Random Forest model by creating a model with all independent variables without BookingID that was removed previously. The In-Sample-AUC of this default model is 0.954774.
We will then apply full cartesian grid search to obtain top 10 models that are ranked based on RSME for optimal mtry and nodesize which results in mtry in the range of 4,5 & 6 and nodesize = 1. We will then try to see the optimal ntree using randomForest package which lies between 100 to 3000. As there might be some iterations between optimal ntree, nodesize and mtry, we will build models based on these range and find the best model with optimal ntree nodesize and mtry. During the try and error phase, we will set the seed at 333. The In-Sample-ROC of the optimised model is 0.9559477. The out-of-sample AUC is 0.9552154.
Based on permutation-based and imputation-based variane check, we can
learn the importance of the variables.
According to the code and plot, lead_time is the most important, along
with special requests and average price per room which affects the
booking status.
Propensity Score Matching will be used to confirm if there are any selection bias with the observational data such as confounding factors in our analysis. In our analysis, we want to determine if lead time is really a causal factor to booking status seeing that it has been used substantially in our analysis.
Proportion of booking: 0 refers to not cancelled and 1, to cancelled.
We can check if there is any significant difference between bookings that are cancelled and bookings that are not cancelled in terms of average price per room, percentage of weekend nights, percentage of children. From the analysis done shows similar findings to that found in visualisation - there is significant difference.
We conducted t-test to confirm whether the differences between the above 3 independent variables are statistically significant. It is observed that all the variables selected are significant with a p-value below 0.05.
From the above tables, we see that there’s quite a bit of difference between the booking statuses. Particularly, bookings that were canceled had booked rooms with higher prices (110.59 vs 99.93) and mostly during weekend nights (88.75% vs. 77.33%), and with children in the booking(12.44% vs. 9.6%)
We will now use the Propensity Score Matching to find appropriate matching between Bookings that were cancelled and not cancelled.
matching = matchit(booking_status~avg_price_per_room+no_of_weekend_nights+no_of_children,data = hotel2,method = "nearest",ratio = 1)
result = summary(matching)
result
##
## Call:
## matchit(formula = booking_status ~ avg_price_per_room + no_of_weekend_nights +
## no_of_children, data = hotel2, method = "nearest", ratio = 1)
##
## Summary of Balance for All Data:
## Means Treated Means Control Std. Mean Diff. Var. Ratio
## distance 0.3434 0.3199 0.3345 0.8906
## avg_price_per_room 110.5900 99.9314 0.3304 0.8090
## no_of_weekend_nights 0.8875 0.7733 0.1242 1.1879
## no_of_children 0.1244 0.0960 0.0644 1.3302
## eCDF Mean eCDF Max
## distance 0.0822 0.1754
## avg_price_per_room 0.0799 0.1773
## no_of_weekend_nights 0.0143 0.0544
## no_of_children 0.0048 0.0149
##
## Summary of Balance for Matched Data:
## Means Treated Means Control Std. Mean Diff. Var. Ratio
## distance 0.3434 0.3432 0.0034 1.0198
## avg_price_per_room 110.5900 110.1945 0.0123 0.9583
## no_of_weekend_nights 0.8875 0.8943 -0.0074 1.1369
## no_of_children 0.1244 0.1125 0.0269 1.1929
## eCDF Mean eCDF Max Std. Pair Dist.
## distance 0.0006 0.0231 0.0041
## avg_price_per_room 0.0182 0.0548 0.2655
## no_of_weekend_nights 0.0060 0.0169 0.4835
## no_of_children 0.0019 0.0081 0.3422
##
## Sample Sizes:
## Control Treated
## All 24390 11885
## Matched 11885 11885
## Unmatched 12505 0
## Discarded 0 0
According to the results, the matching worked very well for our data set, considering the 3 chosen independent variables. We were able to find similar customers that did not cancel their rooms. Prior to matching, the cancelled rooms had a higher average price per room, on a higher percentage of weekend nights, and consisting of a higher percentage of children. After matching, the average price per room for cancelled and non-cancelled rooms are similar, with slightly higher number of weekend nights for non-cancelled rooms and a slightly higher number of children for cancelled rooms.
For better visualisation, we plotted the distribution of propensity scores to further check the matching performance. The matching was successful with similar histograms for the matched data points and the jitter plot also points at the close match between the treatment and matched control units.
plot(matching,type = "hist")
plot(matching,type = "jitter")
## [1] "To identify the units, use first mouse button; to stop, use second."
We have 11,885 pairings through this PSM model. There are the same number of cancelled and non-cancelled rooms in our new data frame.
bookingmatched = match.data(matching)
str(bookingmatched)
## Classes 'matchdata' and 'data.frame': 23770 obs. of 21 variables:
## $ no_of_adults : int 2 2 1 2 2 2 3 2 1 2 ...
## $ no_of_children : int 0 0 0 0 0 0 0 0 0 0 ...
## $ no_of_weekend_nights : int 1 2 2 0 1 0 0 0 1 2 ...
## $ no_of_week_nights : int 2 3 1 2 1 2 4 5 0 1 ...
## $ type_of_meal_plan : num 1 0 1 1 0 2 1 1 0 0 ...
## $ required_car_parking_space : int 0 0 0 0 0 0 0 0 0 0 ...
## $ room_type_reserved : num 1 1 1 1 1 1 1 4 1 1 ...
## $ lead_time : int 224 5 1 211 48 346 121 44 0 30 ...
## $ arrival_year : int 2017 2018 2018 2018 2018 2018 2018 2018 2018 2018 ...
## $ arrival_month : int 10 11 2 5 4 9 7 10 9 11 ...
## $ arrival_date : int 2 6 28 20 11 13 6 18 11 26 ...
## $ market_segment_type : num 0 1 1 1 1 1 0 1 1 1 ...
## $ repeated_guest : int 0 0 0 0 0 0 0 0 0 0 ...
## $ no_of_previous_cancellations : int 0 0 0 0 0 0 0 0 0 0 ...
## $ no_of_previous_bookings_not_canceled: int 0 0 0 0 0 0 0 0 0 0 ...
## $ avg_price_per_room : num 65 106.7 60 100 94.5 ...
## $ no_of_special_requests : int 0 1 0 0 0 1 1 3 0 0 ...
## $ booking_status : num 0 0 1 1 1 1 0 0 0 1 ...
## $ distance : num 0.259 0.376 0.28 0.292 0.315 ...
## $ weights : num 1 1 1 1 1 1 1 1 1 1 ...
## $ subclass : Factor w/ 11885 levels "1","2","3","4",..: 4094 4864 7313 9726 10076 10445 8117 5498 11266 1073 ...
## - attr(*, "distance")= chr "distance"
## - attr(*, "weights")= chr "weights"
## - attr(*, "subclass")= chr "subclass"
table(bookingmatched$booking_status)
##
## 0 1
## 11885 11885
We will now compare the lead time after matching. For a room of similar price and percentages of weekend nights and children, the lead time remains largely different compared to that of cancelled rooms. Hence, lead time truly does play a part in the cancellation of rooms.
tapply(bookingmatched$lead_time, bookingmatched$booking_status, mean)
## 0 1
## 62.28128 139.21548
with(bookingmatched, t.test(lead_time~booking_status))
##
## Welch Two Sample t-test
##
## data: lead_time by booking_status
## t = -71.872, df = 19944, p-value < 2.2e-16
## alternative hypothesis: true difference in means between group 0 and group 1 is not equal to 0
## 95 percent confidence interval:
## -79.03234 -74.83607
## sample estimates:
## mean in group 0 mean in group 1
## 62.28128 139.21548
Provide Recommendations Based on Other Analytics: As an alternative to bagging/ Random Forest, we also tried using boosting with the C5.0 method, which grows trees in succession. At each successive iteration, the new model is built and improved based on past models. We obtained an optimal in-sample ROC of 0.9466660, using the optimised arguments of trials = 20, model = tree, and winnow = TRUE.
Optimising Logistic Regression Models - stochastic gradient descent (SGD): To optimise the logistic regression model, we can use SGD, which minimises the loss function, and seeks to find an optimal model.The advantage of SGD in our logistic regression model is that since the loss function is convex and there is only one minimum, the gradient descent is guaranteed to find the global minimum loss. Another advantage of SGD is that unlike gradient descent, it does not run into the issue of too many computations with large datasets, which will be beneficial if we want to scale our project.
Firstly, our CART model has pointed out that lead time, average price per room, number of special requests, market segment, and number of adults are significant variables. Meanwhile, the findings from confounding factors in our logistic model is that room bookings with higher room prices, and booked during weekend nights along with children in the bookings have a high likelihood of getting cancelled
Based on the analysis done, our team has the following recommendations:
Besides increasing the number of data points through data collection, collecting data on new and different variables such as more time series data (helps to determine seasonality), purpose of stay (i.e. leisure, business etc. will help segment and cluster different groups of customers), any upcoming events happening nearby (i.e. huge events will affect hotel bookings).
Next, it is to expand on the details collected for Market segment variable - bookings are not only made through the hotel’s own website but through online travel agency (OTA) websites as well. In our analysis, a bulk of customers who cancelled their bookings belong to the online segment but there was no way to find the reason behind its huge proportion of cancellations.
Having more information may better explain the proportion of bookings cancelled by new users. Booking through OTA websites could mean having little to no relationship with the hotels that they booked and users could possibly have booked several hotels in advance and cancel a few closer to the reservation date.