数据说明:

本分析报告中的数据来源于R社区的 tidytuesday 项目中的数据。这个项目在每周二会发布一份数据,供数据科学社区分析,并在twitter上相互分享分析结果,交流学习。

image

业务问题:

对于酒店行业来说,预订有两方面的意义:

  1. 酒店可以通过预订信息,对未来的需求有充分的准备,这是预订对业务有益的一个方面;
  2. 但是同时,如果之前的预订产生大量的取消的情况,则会对酒店业务产生不利的影响,如果大量被预订的房间被撤销,很多时候,空余出来的酒店房间并不能及时被租出去,则会对酒店的利用率产生不利的影响,因而,如果能够对酒店的预订情况作出比较准确的预测,对于那些取消风险高的客房,酒店则可以提前做出准备,避免或降低对业务损失。

数据获取与概览:

所有tidytuesday的数据都可以通过两个方式获取,可以安装tidytueday的R包或者直接通过read_csv从github上直接下载。如果你对以往的历史数据集感兴趣,可以访问下面的Tidytuesday at Github.

1. 数据集下载:

library(tidyverse)
library(skimr)
library(lubridate)
library(stringr)
library(ggthemr)
ggthemr("flat")
hotel_bookings <- readr::read_csv('https://raw.githubusercontent.com/rfordatascience/tidytuesday/master/data/2020/2020-02-11/hotels.csv')

我们现在已经将数据存储在了hotel_bookings这个DF中,下面我们可以通过skimr包中的skim函数对整个数据集有一个初步的把握:

skim(hotel_bookings)
-- Data Summary ------------------------
                           Values        
Name                       hotel_bookings
Number of rows             119390        
Number of columns          32            
_______________________                  
Column type frequency:                   
  character                13            
  Date                     1             
  numeric                  18            
________________________                 
Group variables            None          

-- Variable type: character ------------------------------------------------------------------------------------------------------
# A tibble: 13 x 8
   skim_variable        n_missing complete_rate   min   max empty n_unique whitespace
 * <chr>                    <int>         <dbl> <int> <int> <int>    <int>      <int>
 1 hotel                        0             1    10    12     0        2          0
 2 arrival_date_month           0             1     3     9     0       12          0
 3 meal                         0             1     2     9     0        5          0
 4 country                      0             1     2     4     0      178          0
 5 market_segment               0             1     6    13     0        8          0
 6 distribution_channel         0             1     3     9     0        5          0
 7 reserved_room_type           0             1     1     1     0       10          0
 8 assigned_room_type           0             1     1     1     0       12          0
 9 deposit_type                 0             1    10    10     0        3          0
10 agent                        0             1     1     4     0      334          0
11 company                      0             1     1     4     0      353          0
12 customer_type                0             1     5    15     0        4          0
13 reservation_status           0             1     7     9     0        3          0

-- Variable type: Date -----------------------------------------------------------------------------------------------------------
# A tibble: 1 x 7
  skim_variable           n_missing complete_rate min        max        median     n_unique
* <chr>                       <int>         <dbl> <date>     <date>     <date>        <int>
1 reservation_status_date         0             1 2014-10-17 2017-09-14 2016-08-07      926

-- Variable type: numeric --------------------------------------------------------------------------------------------------------
# A tibble: 18 x 11
   skim_variable                  n_missing complete_rate       mean       sd      p0    p25    p50   p75  p100 hist 
 * <chr>                              <int>         <dbl>      <dbl>    <dbl>   <dbl>  <dbl>  <dbl> <dbl> <dbl> <chr>
 1 is_canceled                            0          1       0.370     0.483     0       0      0       1     1 ▇▁▁▁▅
 2 lead_time                              0          1     104.      107.        0      18     69     160   737 ▇▂▁▁▁
 3 arrival_date_year                      0          1    2016.        0.707  2015    2016   2016    2017  2017 ▃▁▇▁▆
 4 arrival_date_week_number               0          1      27.2      13.6       1      16     28      38    53 ▅▇▇▇▅
 5 arrival_date_day_of_month              0          1      15.8       8.78      1       8     16      23    31 ▇▇▇▇▆
 6 stays_in_weekend_nights                0          1       0.928     0.999     0       0      1       2    19 ▇▁▁▁▁
 7 stays_in_week_nights                   0          1       2.50      1.91      0       1      2       3    50 ▇▁▁▁▁
 8 adults                                 0          1       1.86      0.579     0       2      2       2    55 ▇▁▁▁▁
 9 children                               4          1.00    0.104     0.399     0       0      0       0    10 ▇▁▁▁▁
10 babies                                 0          1       0.00795   0.0974    0       0      0       0    10 ▇▁▁▁▁
11 is_repeated_guest                      0          1       0.0319    0.176     0       0      0       0     1 ▇▁▁▁▁
12 previous_cancellations                 0          1       0.0871    0.844     0       0      0       0    26 ▇▁▁▁▁
13 previous_bookings_not_canceled         0          1       0.137     1.50      0       0      0       0    72 ▇▁▁▁▁
14 booking_changes                        0          1       0.221     0.652     0       0      0       0    21 ▇▁▁▁▁
15 days_in_waiting_list                   0          1       2.32     17.6       0       0      0       0   391 ▇▁▁▁▁
16 adr                                    0          1     102.       50.5      -6.38   69.3   94.6   126  5400 ▇▁▁▁▁
17 required_car_parking_spaces            0          1       0.0625    0.245     0       0      0       0     8 ▇▁▁▁▁
18 total_of_special_requests              0          1       0.571     0.793     0       0      0       1     5 ▇▁▁▁▁

skim函数的输出结果来看,这个数据集包含了近12万条的酒店预订记录以及32个变量,其中13个为文本变量,18个位数值型变量,以及一个日期型变量。数据集比较整洁完整,只有children这个变量中包含了4个缺失值。这对于我们来说,是一件非常好的事情,可以节省大量的数据整理的时间。毕竟,在数据科学家中间,有一个人尽皆知的笑话:

数据科学家的工作,80%的时间都是用于整理数据,剩下的20%的时间,都用来抱怨数据有多糟糕。

2. 数据的预处理

虽然这个数据集是相对比较整洁的,但是为了建模的需要,我们还是需要对数据进行一些处理。
具体而言,有几个类别型变量的值过多,需要我们对其进行整合,否则则会导致我们的数据模型中每个类别的样本量都比较少。具体而言:

  1. country变量包含了178个不同的值,并且分布很不均匀;
  2. agent变量包含了334个不同的值,也是分布很不均匀;
  3. company变量包含了353个不同的值。

tidyverse中包含了可以对类别型变量进行重组的函数fct_reorder可以很方便地让我们对以上三个变量进行重组:

hotel_bookings <- hotel_bookings %>% 
  mutate(agent=fct_lump(agent,prop = 0.008))
hotel_bookings <- hotel_bookings %>% 
mutate(company=if_else(company=="NULL","Individual","Corporate"))

除此之外,我们还有另外一些变量需要进行一些处理:

hotel_bookings <- hotel_bookings %>% 
  select(is_canceled,everything()) %>% 
  mutate(is_canceled=factor(if_else(is_canceled==1,"Canceled","Not_Canceled"))) %>% 
  select(-reservation_status_date,-arrival_date_year,-arrival_date_day_of_month,-arrival_date_week_number)

hotel_bookings <- hotel_bookings %>% 
  mutate(with_kids=babies+children
  )

hotel_bookings <- hotel_bookings %>% 
  filter(!is.na(with_kids)) %>% 
  select(-babies,-children)

hotel_bookings <- hotel_bookings %>% 
  select(-reservation_status)   ## this is another indicator for reservation, won't use in the model due to data leakage

这里尤其要提到的是我在原始数据中删除了reservation_status这个变量,因为这个变量实际上产生了所谓的data leakage的问题,因为在我第一次的建模过程中,我发现最终的测试预测准确率达到了100%!!作为一个有多年预测建模经验的人,本能的第一反应就是数据中存在信息泄露的问题,实际上这个reservation_status变量和最终我们要预测的是否取消是等同的。因而,为了避免这样的问题导致模型没有最终的实际业务价值,我们必须将其从原始数据中删除。

3. 数据的不均衡问题诊断:

在一些特定的机器学习问题中,比如银行违约这类的数据,往往存在不均衡的问题,毕竟违约的是很少数的。而这个不均衡的问题,对于机器学习模型会产生很大的影响。

hotel_bookings %>% 
  count(is_canceled,sort = TRUE) %>% 
  mutate(Percent_of_Total=n/sum(n)) %>% 
  kableExtra::kable()

image.png
从数据上看,预约取消的比例大概有40%不到(比我想象的高出不少)。但是另一方面,也说明我们没有太大的class imbalance的问题。

4. 数据类型的转换

因为我们需要使用到的是h2o来进行预测建模,因此,我们需要将原始数据中的所有文本类型的数据转换为factor,这很容易通过tidyverse中的mutate_if函数来轻松实现。

hotel_bookings <- hotel_bookings %>% 
  mutate_if(is.character,factor)

通过h2o中的automl来进行预测建模:

h2o简介:

h2o是美国一家公司开发的开源机器学习项目,由斯坦福大学的几位世界知名的统计学教授作为技术指导,同时提供了R和Python的包,以超高的预测准确率闻名。具体信息可以点击h2o官网

h2o的主要优点有以下几个方面:

  1. 为大数据准备,可以利用分布式计算在电脑的多核以及集群上运行;
  2. 速度快,h2o实际是用Java编写,弥补了R在计算速度上的缺陷;
  3. API界面统一,学习周期短;
  4. 数据预处理要求比较低,很多数据预处理的工作都可以自动完成。

1.启动h2o:

library(h2o)
h2o.init(nthreads = 6,max_mem_size = "36g")

h2o的一大优点就是你可以自己定义为本项目可以利用的计算机处理器和内存的使用量,可以保证你在运行模型时,不影响其他电脑正在运行的任务。我的电脑是8核的处理器,有48GB的内存,所以我为其他任务保留了一定的计算能力。

Connection successful!

R is connected to the H2O cluster: 
    H2O cluster uptime:         1 hours 6 minutes 
    H2O cluster timezone:       Asia/Shanghai 
    H2O data parsing timezone:  UTC 
    H2O cluster version:        3.28.0.2 
    H2O cluster version age:    1 month and 14 days  
    H2O cluster name:           H2O_started_from_R_chn-fzj_tff946 
    H2O cluster total nodes:    1 
    H2O cluster total memory:   35.80 GB 
    H2O cluster total cores:    8 
    H2O cluster allowed cores:  6 
    H2O cluster healthy:        TRUE 
    H2O Connection ip:          localhost 
    H2O Connection port:        54321 
    H2O Connection proxy:       NA 
    H2O Internal Security:      FALSE 
    H2O API Extensions:         Amazon S3, Algos, AutoML, Core V3, TargetEncoder, Core V4 
    R Version:                  R version 3.6.2 (2019-12-12) 

如果你看到和以上类似的信息,就说明你的设置已经成功了。

2.数据框的转换与分割:

response <- "is_canceled"
predictors <- setdiff(names(hotel_bookings),response)

bookings_h2o <- as.h2o(hotel_bookings)


data_split <- h2o.splitFrame(bookings_h2o,ratios = c(0.9,0.05))

bookings_train <- data_split[[1]]
bookings_valid <- data_split[[2]]
bookings_test <- data_split[[3]]

在使用h2o进行建模之前我们需要将R中的dataframe转换成h2o的数据框,然后将我们的数据分割成训练集,验证集和测试集。在本案例中,因为我们的原始数据量还是比较大的,因而我是按照90%的训练集,5%的验证集和5%的测试集。

automl_model <- h2o.automl(x=predictors,
                           y=response,
                           training_frame = bookings_train,
                           validation_frame = bookings_valid,
                           nfolds = 20,
                           max_runtime_secs = 600,
                           max_models = 20,
                           stopping_metric = "AUC",
                           stopping_tolerance = 0.005,
                           seed = 1234,
                           project_name = "First_AutoML_Model")

我使用的是automl的算法,本质上来说就是一种ensemble的方法,可以同时随即森林,GBM等方法训练出模型并从中选择出表现最好的模型。
另外,我规定了模型训练的时间为600秒,这是一个非常有用的特性,可以避免我们平时需要等待几个小时才能看到模型的结果这样的情形出现。

3. 模型的表现对比:

前文说过,automl会产生多个不同的算法模型,通过提取leaderboard的信息,我们对模型的结果有一个比较准确的认识:

image.png
从这些模型中,我们直接选择出表现最好的模型,并使用这个模型对测试集的数据进行预测,并验证最终的结果表现,确保没有过拟合现象的出现:

automl_best <- automl_model@leader

h2o.confusionMatrix(automl_best)

image.png

predictions_with_bestmodel <- h2o.predict(automl_best,bookings_test)

test_df <- as.data.frame(bookings_test)
predictions_with_bestmodel <- as.data.frame(predictions_with_bestmodel)
predicted <- predictions_with_bestmodel %>% 
  pull(predict)
testing_results <- data.frame(
  actual=test_df$is_canceled,
  predictions=predicted
  
)
testing_results %>% 
  count(actual,predictions)

image.png

我们花费了10分钟训练出来的模型的预测准确率达到了88%,这个结果是相当不错的。

下面还应该做些什么?

上面的内容只是一个最简单的案例,在实际业务过程中,我们还需要:

  1. 使用更多的模型和参数;
  2. 训练更长的时间;
  3. 探究模型是不是在某一些方面的预测能力存在问题,可能需要对预测变量进行更多的处理;
  4. 最终,模型完成验证之后,需要进行部署。

FelixZhao
1 声望0 粉丝

多年R用户。Tidyverse死忠粉。热衷使用数据分析和解决业务问题,包括描述性分析,析因分析,预测建模(机器学习)等。