見出し画像

機械学習:R言語(Tidymodels)チュートリアルの和訳①

R言語を普及したい!
機械学習楽しいよ!
Kaggleにも挑戦できるよ!!
という事で、自分の復習もかねてチュートリアルを和訳します。
※細かい文章は省略しています。

対象のチュートリアル


ホテルの宿泊データ

まず、ライブラリを使えるようにします。
library(tidymodels)  
library(readr)       # for importing data
library(vip)         # for variable importance plots

予測タスクの目的:ゲストが子供や赤ちゃんを含むホテルの滞在を予測する

library(readr) hotels <- read_csv("https://tidymodels.org/start/case-study/hotels.csv") %>%
mutate(across(where(is.character), as.factor))

※キャンセルした人のデータには欠損が多いため、キャンセルしていない人のみのデータで分析を行う

glimpse(hotels) は、データセットをざっくりと表示するコマンドです。
hotelから始まる縦列がデータの変数(列)です。
予測するものは「children」です(2値)

glimpse(hotels)
#> Rows: 50,000
#> Columns: 23
#> $ hotel                          <fct> City_Hotel, City_Hotel, Resort_Hotel, R…
#> $ lead_time                      <dbl> 217, 2, 95, 143, 136, 67, 47, 56, 80, 6…
#> $ stays_in_weekend_nights        <dbl> 1, 0, 2, 2, 1, 2, 0, 0, 0, 2, 1, 0, 1, …
#> $ stays_in_week_nights           <dbl> 3, 1, 5, 6, 4, 2, 2, 3, 4, 2, 2, 1, 2, …
#> $ adults                         <dbl> 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 1, 2, …
#> $ children                       <fct> none, none, none, none, none, none, chi…
#> $ meal                           <fct> BB, BB, BB, HB, HB, SC, BB, BB, BB, BB,…
#> $ country                        <fct> DEU, PRT, GBR, ROU, PRT, GBR, ESP, ESP,…
#> $ market_segment                 <fct> Offline_TA/TO, Direct, Online_TA, Onlin…
#> $ distribution_channel           <fct> TA/TO, Direct, TA/TO, TA/TO, Direct, TA…
#> $ is_repeated_guest              <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
#> $ previous_cancellations         <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
#> $ previous_bookings_not_canceled <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
#> $ reserved_room_type             <fct> A, D, A, A, F, A, C, B, D, A, A, D, A, …
#> $ assigned_room_type             <fct> A, K, A, A, F, A, C, A, D, A, D, D, A, …
#> $ booking_changes                <dbl> 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
#> $ deposit_type                   <fct> No_Deposit, No_Deposit, No_Deposit, No_…
#> $ days_in_waiting_list           <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
#> $ customer_type                  <fct> Transient-Party, Transient, Transient, …
#> $ average_daily_rate             <dbl> 80.75, 170.00, 8.00, 81.00, 157.60, 49.…
#> $ required_car_parking_spaces    <fct> none, none, none, none, none, none, non…
#> $ total_of_special_requests      <dbl> 1, 3, 2, 1, 4, 1, 1, 1, 1, 1, 0, 1, 0, …
#> $ arrival_date                   <date> 2016-09-01, 2017-08-25, 2016-11-19, 20…
childrenの中身を見ます。
hotels %>% count(children) %>% 
mutate(prop = n/sum(n))

 #> # A tibble: 2 × 3 #> 
children n prop #> <fct> <int> <dbl> #> 1 
children 4038 0.0808 #> 2 
none 45962 0.919

childrenは0.8%しかなく、かなり不均衡(偏った)データです。
※偏りが大きいままでモデリングすると、うまく予測ができません。

滞在の25%をテストセットにします。偏りが大きいので「層化抽出※」をします。※指定した列について、データ分割時に同じ割合になるように分割
strata = childrenが対象箇所

set.seed(123) 

splits <- initial_split(hotels, strata = children) 

hotel_other <- training(splits) 
hotel_test <- testing(splits) 

hotel_other %>% count(children) %>% 
mutate(prop = n/sum(n)) 

 A tibble: 2 × 3 #> 
children n prop #> <fct> <int> <dbl> #> 
1 children 3027 0.0807 #> 
2 none 34473 0.919 

hotel_test %>% count(children) %>% 
mutate(prop = n/sum(n)) 

 A tibble: 2 × 3 #> 
children n prop #> <fct> <int> <dbl> #> 
1 children 1011 0.0809 #> 
2 none 11489 0.919

確かめてみると、trainもtestもchildrenが0.8%程度になっています。


こんなイメージになります
set.seed(234) 
val_set <- validation_split(hotel_other, strata = children, prop = 0.80)

seedは数はなんでもいいです(設定しないと毎回違う結果になる)
validation_splitを実施していますが、分割方法は他にもたくさんあります。
vfold_cvなど

最初のモデル:ペナルティ付きロジスティック回帰

parsnipパッケージをglmnetを使います。

lr_mod <-
logistic_reg(penalty = tune(), mixture = 1) %>%
set_engine("glmnet")

※penalty = tune()は、ハイパラを後で調整しますという意味です。

前処理
データを予測しやすい形に変換します。
step_date() は、年、月、曜日の予測子を作成します。

step_holiday() は、特定の休日のインジケータ変数のセットを生成します。これらの2つのホテルがどこに位置しているかはわかりませんが、ほとんどの滞在の出発国がヨーロッパに基づいていることはわかっています。

step_rm() は変数を削除します。ここでは、モデルに含めたくない元の日付変数を削除するために使用します。

step_dummy() は、文字列またはファクター(つまり、名義尺度の変数)を元のデータのレベルに対応する1つ以上の数値のバイナリモデル用語に変換します。

step_zv() は、単一の一意の値(例:すべてがゼロのインジケータ変数)のみを含む予測子を削除します。ペナルティ付きモデルでは、予測子は中心化およびスケーリングされている必要があるため、これは重要です。

step_normalize() は、数値変数を中心化およびスケーリングします。

holidays <- c("AllSouls", "AshWednesday", "ChristmasEve", "Easter",
"ChristmasDay", "GoodFriday", "NewYearsDay", "PalmSunday")
lr_recipe <-
recipe(children ~ ., data = hotel_other) %>%
step_date(arrival_date) %>%
step_holiday(arrival_date, holidays = holidays) %>%
step_rm(arrival_date) %>%
step_dummy(all_nominal_predictors()) %>%
step_zv(all_predictors()) %>%
step_normalize(all_predictors())

※children ~ .,の”.”はデータセット内の説明変数をすべて含むという省略記号です。
※%>%は、左の処理をして、右(もしくは下に)に渡すパイプラインです。

workflow()オブジェクトにまとめて可視性を向上させ、この後の処理を楽にします。

lr_workflow <- workflow() %>% add_model(lr_mod) %>% add_recipe(lr_recipe)

lr_mod <-
logistic_reg(penalty = tune()・・・の、tuneをします。
範囲を決めて、30通りの中から探索します。

lr_reg_grid <- tibble(penalty = 10^seq(-4, -1, length.out = 30))
lr_reg_grid %>% top_n(-5) # lowest penalty values
#> Selecting by penalty
#> # A tibble: 5 × 1
#>    penalty
#>      <dbl>
#> 1 0.0001
#> 2 0.000127
#> 3 0.000161
#> 4 0.000204
#> 5 0.000259
lr_reg_grid %>% top_n(5)  # highest penalty values
#> Selecting by penalty
#> # A tibble: 5 × 1
#>   penalty
#>     <dbl>
#> 1  0.0386
#> 2  0.0489
#> 3  0.0621
#> 4  0.0788
#> 5  0.1
以下のコードでTUNEを実行します。
lr_res <-
lr_workflow %>%
tune_grid(val_set,
grid = lr_reg_grid,
control = control_grid(save_pred = TRUE),
metrics = metric_set(roc_auc))
lr_plot <- lr_res %>% collect_metrics() %>%
 ggplot(aes(x = penalty, y = mean)) + geom_point() + geom_line() + 
ylab("Area under the ROC Curve") + scale_x_log10(labels = scales::label_number()) 

lr_plot


ROCカーブでTUNE結果を確認

penaltyは小さいほうがよさそうですね。
縦が0.5になると、ほぼ予測できていないことになります。

BESTを選びます

top_models <-
lr_res %>%
show_best(metric = "roc_auc", n = 15) %>%
arrange(penalty)

top_models
#> # A tibble: 15 × 7
#>     penalty .metric .estimator  mean     n std_err .config
#>       <dbl> <chr>   <chr>      <dbl> <int>   <dbl> <chr>
#>  1 0.000127 roc_auc binary     0.872     1      NA Preprocessor1_Model02
#>  2 0.000161 roc_auc binary     0.872     1      NA Preprocessor1_Model03
#>  3 0.000204 roc_auc binary     0.873     1      NA Preprocessor1_Model04
#>  4 0.000259 roc_auc binary     0.873     1      NA Preprocessor1_Model05
#>  5 0.000329 roc_auc binary     0.874     1      NA Preprocessor1_Model06
#>  6 0.000418 roc_auc binary     0.874     1      NA Preprocessor1_Model07
#>  7 0.000530 roc_auc binary     0.875     1      NA Preprocessor1_Model08
#>  8 0.000672 roc_auc binary     0.875     1      NA Preprocessor1_Model09
#>  9 0.000853 roc_auc binary     0.876     1      NA Preprocessor1_Model10
#> 10 0.00108  roc_auc binary     0.876     1      NA Preprocessor1_Model11
#> 11 0.00137  roc_auc binary     0.876     1      NA Preprocessor1_Model12
#> 12 0.00174  roc_auc binary     0.876     1      NA Preprocessor1_Model13
#> 13 0.00221  roc_auc binary     0.876     1      NA Preprocessor1_Model14
#> 14 0.00281  roc_auc binary     0.875     1      NA Preprocessor1_Model15
#> 15 0.00356  roc_auc binary     0.873     1      NA Preprocessor1_Model16

ただし、モデルの性能が低下し始める地点に近いx軸上でペナルティ値を選択したいかもしれません。
性能がほぼ同じであれば、より高いペナルティ値を選択したいと考えるでしょう。この値を選択して、検証セットのROC曲線を可視化しましょう。

lr_best <-
lr_res %>%
collect_metrics() %>%
arrange(penalty) %>%
slice(12)

lr_best
#> # A tibble: 1 × 7
#>   penalty .metric .estimator  mean     n std_err .config
#>     <dbl> <chr>   <chr>      <dbl> <int>   <dbl> <chr>
#> 1 0.00137 roc_auc binary     0.876     1      NA Preprocessor1_Model12
lr_auc <-
lr_res %>%
collect_predictions(parameters = lr_best) %>%
roc_curve(children, .pred_children) %>%
mutate(model = "Logistic Regression")
autoplot(lr_auc)


結果

次のステップとして、木ベースのアンサンブル法を使用して生成される非常に非線形なモデルを考慮することができます。

2つ目のモデル:木ベースのアンサンブル

ランダムフォレストを試します。ただ、tuneの計算コストが高く時間がかかります。そのようなときは、以下のように並列処理ができます。

cores <- parallel::detectCores()

cores
#> [1] 10 ※CPUのコアが10個あるPCということ

※今回は並列処理を実行しないので省略します。

rf_mod <-
rand_forest(mtry = tune(), min_n = tune(), trees = 1000) %>%
set_engine("ranger", num.threads = cores) %>%
set_mode("classification")

※ハイパラの補足
mtry
内容: mtryは、各決定木の分岐ごとに考慮する変数の数を指定します。デフォルトでは、ランダムフォレストはすべての予測変数のうちランダムに選ばれたサブセットの中から最適な分割を選びます。

目的: モデルのバリアンス(過剰適合)とバイアス(適合不足)を調整します。小さなmtryはバリアンスを減少させ、バイアスを増加させる可能性があり、大きなmtryはその逆です。


min_n
内容: min_nは、各ノードがさらに分割されるために必要な最小のデータポイント数を指定します。このパラメータは、決定木の深さと複雑さを制御します。

目的: モデルの複雑さを管理し、過剰適合を防ぎます。小さなmin_nは非常に細かい分割を許可し、木が深くなる傾向があります。一方、大きなmin_nは木を浅くし、過剰適合を防ぐのに役立ちます。

前処理

rf_recipe <-
recipe(children ~ ., data = hotel_other) %>%
step_date(arrival_date) %>%
step_holiday(arrival_date) %>%
step_rm(arrival_date) 

ワークフロー

rf_workflow <-
workflow() %>%
add_model(rf_mod) %>%
add_recipe(rf_recipe)

TRAINとTUNE

rf_mod
#> Random Forest Model Specification (classification)
#>
#> Main Arguments:
#>   mtry = tune()
#>   trees = 1000
#>   min_n = tune()
#>
#> Engine-Specific Arguments:
#>   num.threads = cores
#>
#> Computational engine: ranger
extract_parameter_set_dials(rf_mod)
#> Collection of 2 parameters for tuning
#>
#>  identifier  type    object
#>        mtry  mtry nparam[?]
#>       min_n min_n nparam[+]
#>
#> Model parameters needing finalization:
#>    # Randomly Selected Predictors ('mtry')
#>
#> See `?dials::finalize` or `?dials::update.parameters` for more information.
set.seed(345)
rf_res <-
rf_workflow %>%
tune_grid(val_set,
grid = 25,
control = control_grid(save_pred = TRUE),
metrics = metric_set(roc_auc))
#> i Creating pre-processing data to finalize unknown parameter: mtry

grid = 25で25個の候補からTUNEしています。

一番いいものを表示します。

rf_res %>%
show_best(metric = "roc_auc")

#> # A tibble: 5 × 8
#>    mtry min_n .metric .estimator  mean     n std_err .config
#>   <int> <int> <chr>   <chr>      <dbl> <int>   <dbl> <chr>
#> 1     8     7 roc_auc binary     0.926     1      NA Preprocessor1_Model13
#> 2    12     7 roc_auc binary     0.926     1      NA Preprocessor1_Model01
#> 3    13     4 roc_auc binary     0.925     1      NA Preprocessor1_Model05
#> 4     9    12 roc_auc binary     0.924     1      NA Preprocessor1_Model19
#> 5     6    18 roc_auc binary     0.924     1      NA Preprocessor1_Model24

ロジスティック回帰では、ROC AUCは0.876だったので、すでにそれを超えています。※数値が1に近いほうが良いと判断します。

autoplot(rf_res)


結果
rf_best <-
rf_res %>%
select_best(metric = "roc_auc")

rf_best
#> # A tibble: 1 × 3
#>    mtry min_n .config
#>   <int> <int> <chr>
#> 1     8     7 Preprocessor1_Model13

ROC曲線をプロットするために必要なデータを計算するには、collect_predictions()を使用します。これは、control_grid(save_pred = TRUE)を使用してチューニングした後にのみ可能です。出力には、子供を含むか含まないかを予測するクラス確率を保持する2つの列が表示されます。

rf_res %>%
collect_predictions()

#> # A tibble: 187,500 × 8
#>    .pred_children .pred_none id          .row  mtry min_n children .config
#>             <dbl>      <dbl> <chr>      <int> <int> <int> <fct>    <chr>
#>  1        0.152        0.848 validation    13    12     7 none     Preprocessor…
#>  2        0.0302       0.970 validation    20    12     7 none     Preprocessor…
#>  3        0.513        0.487 validation    22    12     7 children Preprocessor…
#>  4        0.0103       0.990 validation    23    12     7 none     Preprocessor…
#>  5        0.0111       0.989 validation    31    12     7 none     Preprocessor…
#>  6        0            1     validation    38    12     7 none     Preprocessor…
#>  7        0            1     validation    39    12     7 none     Preprocessor…
#>  8        0.00325      0.997 validation    50    12     7 none     Preprocessor…
#>  9        0.0241       0.976 validation    54    12     7 none     Preprocessor…
#> 10        0.0441       0.956 validation    57    12     7 children Preprocessor…
#> # ℹ 187,490 more rows
rf_auc <-
rf_res %>%
collect_predictions(parameters = rf_best) %>%
roc_curve(children, .pred_children) %>%
mutate(model = "Random Forest")
bind_rows(rf_auc, lr_auc) %>% 
ggplot(aes(x = 1 - specificity, y = sensitivity, col = model)) + 
geom_path(lwd = 1.5, alpha = 0.8) + geom_abline(lty = 3) + coord_equal() +
 scale_color_viridis_d(option = "plasma", end = .6)


2モデルの比較

最終FIT

TRAIN全体で再度モデルの学習を行います。
importance = "impurity" を追加します。これにより、この最後のモデルに対して変数重要度スコアが提供され、どの予測子がモデルの性能に寄与しているかの洞察が得られます。

the last model

last_rf_mod <-
rand_forest(mtry = 8, min_n = 7, trees = 1000) %>%
set_engine("ranger", num.threads = cores, importance = "impurity") %>%
set_mode("classification")

the last workflow

last_rf_workflow <-
rf_workflow %>%
update_model(last_rf_mod)

the last fit

set.seed(345)
last_rf_fit <-
last_rf_workflow %>%
last_fit(splits)
last_rf_fit
#> # Resampling results
#> # Manual resampling
#> # A tibble: 1 × 6
#>   splits                id             .metrics .notes   .predictions .workflow
#>   <list>                <chr>          <list>   <list>   <list>       <list>
#> 1 <split [37500/12500]> train/test sp… <tibble> <tibble> <tibble>     <workflow>
last_rf_fit %>%
collect_metrics()
#> # A tibble: 3 × 4
#>   .metric     .estimator .estimate .config
#>   <chr>       <chr>          <dbl> <chr>
#> 1 accuracy    binary        0.946  Preprocessor1_Model1
#> 2 roc_auc     binary        0.923  Preprocessor1_Model1
#> 3 brier_class binary        0.0423 Preprocessor1_Model1

どの変数が予測に効いているかを可視化します。

last_rf_fit %>%
extract_fit_parsnip() %>%
vip(num_features = 20)


可視化

重要な変数は以下の通り
部屋の1日あたりの費用
予約された部屋のタイプ
予約の作成日から到着日までの時間
最終的に割り当てられた部屋のタイプ

最後のROC曲線の生成

最後に、ROC曲線を生成して視覚化しましょう。予測するイベントが children 要因の最初のレベル(「子供がいる」)であるため、roc_curve() に適切なクラス確率 .pred_children を提供します。

last_rf_fit %>%
collect_predictions() %>%
roc_curve(children, .pred_children) %>%
autoplot()


結果

後は予測するのみ!
ここからはチュートリアルに書いていませんが、
testという結果がわからないデータがあるなら以下のコードで予測ができます。

predict(last_rf_fit,test)

END


この記事が気に入ったらサポートをしてみませんか?