UPDATE: 2022-12-17 14:07:59

はじめに

tidymodelsパッケージの使い方をいくつかのノートに分けてまとめている。tidymodelsパッケージは、統計モデルや機械学習モデルを構築するために必要なパッケージをコレクションしているパッケージで、非常に色んなパッケージがある。ここでは、今回はrecipesというパッケージの使い方をまとめていく。モデルの数理的な側面や機械学習の用語などは、このノートでは扱わない。

下記の公式ドキュメントやtidymodelsパッケージに関する書籍を参考にしている。

recipesパッケージの目的

recipesパッケージは、dplyrパッケージのようなパイプ演算子を使って特徴量エンジニアリングを行うことを可能にするパッケージ。特徴量エンジニアリングの処理をステップに分けて管理することで、学習や評価の際のモデリング、予測の際のモデリングをスムーズに統一した処理を利用して実行できる。

recipesパッケージの実行例

recipesパッケージの関数群の基本的な利用方法は下記の通り。お役所の書類の決済手続きのような感じでモデルに必要な情報を決定していく。

  • recipe: モデルおよび学習データを定義する
  • step_a: データの特徴量エンジニアリングの設計を行う
  • prep: 処理内容を決定する(必要な統計量などを推定する)
  • bake: 特徴量エンジニアリングや処理内容を適用する
# recipe(y ~ x + z, data = train) %>% 
#   step_a() %>% 
#   step_b() %>% 
#   step_c() %>% 
#   step_d() %>% 
#   prep() %>% 
#   bake(new_data = train)

recipe関数の引数に含まれるdataは学習データである必要はなく、このデータは、変数名や型の情報をカタログ化するためにのみ使用される。そのため、モデルを対数変換したいからといって、この段階で変換してはいけない。

# recipe(log(Sepal.Length) ~ ., data = iris)
# Error in `inline_check()`:
# ! No in-line functions should be used here; use steps to define baking actions.

また、特徴量エンジニアリングをステップに分けて記述していくことになるが、下記の通りかなり多くの特徴量エンジニアリングの関数が用意されている。Function referenceには説明付きで一覧があるので、こちらを見るのが良い。

library(recipes)
grep("step_", ls("package:recipes"), value = TRUE)
##  [1] "step_arrange"            "step_bagimpute"         
##  [3] "step_bin2factor"         "step_BoxCox"            
##  [5] "step_bs"                 "step_center"            
##  [7] "step_classdist"          "step_corr"              
##  [9] "step_count"              "step_cut"               
## [11] "step_date"               "step_depth"             
## [13] "step_discretize"         "step_dummy"             
## [15] "step_dummy_extract"      "step_dummy_multi_choice"
## [17] "step_factor2string"      "step_filter"            
## [19] "step_filter_missing"     "step_geodist"           
## [21] "step_harmonic"           "step_holiday"           
## [23] "step_hyperbolic"         "step_ica"               
## [25] "step_impute_bag"         "step_impute_knn"        
## [27] "step_impute_linear"      "step_impute_lower"      
## [29] "step_impute_mean"        "step_impute_median"     
## [31] "step_impute_mode"        "step_impute_roll"       
## [33] "step_indicate_na"        "step_integer"           
## [35] "step_interact"           "step_intercept"         
## [37] "step_inverse"            "step_invlogit"          
## [39] "step_isomap"             "step_knnimpute"         
## [41] "step_kpca"               "step_kpca_poly"         
## [43] "step_kpca_rbf"           "step_lag"               
## [45] "step_lincomb"            "step_log"               
## [47] "step_logit"              "step_lowerimpute"       
## [49] "step_meanimpute"         "step_medianimpute"      
## [51] "step_modeimpute"         "step_mutate"            
## [53] "step_mutate_at"          "step_naomit"            
## [55] "step_nnmf"               "step_nnmf_sparse"       
## [57] "step_normalize"          "step_novel"             
## [59] "step_ns"                 "step_num2factor"        
## [61] "step_nzv"                "step_ordinalscore"      
## [63] "step_other"              "step_pca"               
## [65] "step_percentile"         "step_pls"               
## [67] "step_poly"               "step_poly_bernstein"    
## [69] "step_profile"            "step_range"             
## [71] "step_ratio"              "step_regex"             
## [73] "step_relevel"            "step_relu"              
## [75] "step_rename"             "step_rename_at"         
## [77] "step_rm"                 "step_rollimpute"        
## [79] "step_sample"             "step_scale"             
## [81] "step_select"             "step_shuffle"           
## [83] "step_slice"              "step_spatialsign"       
## [85] "step_spline_b"           "step_spline_convex"     
## [87] "step_spline_monotone"    "step_spline_natural"    
## [89] "step_spline_nonnegative" "step_sqrt"              
## [91] "step_string2factor"      "step_time"              
## [93] "step_unknown"            "step_unorder"           
## [95] "step_window"             "step_YeoJohnson"        
## [97] "step_zv"

では、実際にレシピを組み立てていく。使用するデータはrsampleパッケージのノートで使用したデータをここでも利用する。数値変換、カテゴリ変換、欠損値補完の例を通じて、receipeパッケージへの理解を深める。

library(tidymodels)
library(tidyverse)
df_past <- read_csv("https://raw.githubusercontent.com/SugiAki1989/statistical_note/main/note_TidyModels00/df_past.csv")
set.seed(1989)
df_initial <- df_past %>% initial_split(prop = 0.8, strata = "Status")
df_train <- df_initial %>% training()
df_test <- df_initial %>% testing()

まずは基本的な数値変換手法である、標準化を行ってみる。step_normalize関数を利用して、prep関数、bake関数とつなげていく。他にも、対数変換を行うstep_log()関数、 ロジット変換を行うstep_logit関数、平方根変換を行うstep_sqrt関数、離散化のためのstep_discretize関数、step_cut関数もある。

recipe(Status ~ ., data = df_train) %>% 
  step_normalize(Age) %>% 
  prep() %>% 
  bake(new_data = df_train) %>% 
  select(Age)
## # A tibble: 3,206 × 1
##        Age
##      <dbl>
##  1  0.354 
##  2 -1.45  
##  3 -0.0977
##  4 -1.09  
##  5 -1.27  
##  6 -1.27  
##  7 -0.0977
##  8 -0.550 
##  9  0.535 
## 10 -0.911 
## # … with 3,196 more rows

複数のカラムを同時に指定することもできる。

recipe(Status ~ ., data = df_train) %>% 
  step_normalize(Age, Income) %>% 
  prep() %>% 
  bake(new_data = df_train) %>% 
  select(Age, Income)
## # A tibble: 3,206 × 2
##        Age Income
##      <dbl>  <dbl>
##  1  0.354  -0.756
##  2 -1.45   -1.13 
##  3 -0.0977 -0.135
##  4 -1.09   -0.421
##  5 -1.27   -0.694
##  6 -1.27   -0.234
##  7 -0.0977 -0.520
##  8 -0.550  -0.632
##  9  0.535  -0.868
## 10 -0.911  -0.160
## # … with 3,196 more rows

ここでは全ての数値型を変換したい。そんな時はall_numeric_predictors関数を利用する。all_**関数も沢山用意されており、特定の型のカラムを処理対象ししてまとめて決定できる。

all_numeric_predictorsall_numericの違いは、前者が数値型の説明変数を選択するのに対し、後者は数値型を選択するという違いがある。その他も同様である。

grep("all_", ls("package:recipes"), value = TRUE)
##  [1] "all_date"                 "all_date_predictors"     
##  [3] "all_datetime"             "all_datetime_predictors" 
##  [5] "all_double"               "all_double_predictors"   
##  [7] "all_factor"               "all_factor_predictors"   
##  [9] "all_integer"              "all_integer_predictors"  
## [11] "all_logical"              "all_logical_predictors"  
## [13] "all_nominal"              "all_nominal_predictors"  
## [15] "all_numeric"              "all_numeric_predictors"  
## [17] "all_ordered"              "all_ordered_predictors"  
## [19] "all_outcomes"             "all_predictors"          
## [21] "all_string"               "all_string_predictors"   
## [23] "all_unordered"            "all_unordered_predictors"

それでは標準化を行う。まずは変換前のdf_trainの中身を見ておく。標準化する前なので、オリジナルのスケールでデータが記録されている。

df_train %>% 
  select_if(is.numeric)
## # A tibble: 3,206 × 9
##    Seniority  Time   Age Expenses Income Assets  Debt Amount Price
##        <dbl> <dbl> <dbl>    <dbl>  <dbl>  <dbl> <dbl>  <dbl> <dbl>
##  1         0    48    41       90     80      0     0   1200  1468
##  2         0    18    21       35     50      0     0    400   500
##  3         0    48    36       45    130    750     0   1100  1511
##  4         2    60    25       46    107      0     0   1500  2189
##  5         3    24    23       75     85   5000     0    600  1600
##  6         0    36    23       45    122   2500     0    400   400
##  7         1    54    36       70     99      0     0    950   950
##  8         5    48    31       44     90      0     0   1300  1700
##  9         2    60    43       75     71   3000     0   1500  1552
## 10         2    36    27       48    128      0     0    450   545
## # … with 3,196 more rows

レシピの手順に従って、データを標準化すると、数値型の列が標準化されていることがわかる。

recipe(Status ~ ., data = df_train) %>% 
  step_normalize(all_numeric_predictors()) %>% 
  prep() %>% 
  bake(new_data = df_train) %>% 
  select_if(is.numeric)
## # A tibble: 3,206 × 9
##    Seniority   Time     Age Expenses Income  Assets   Debt Amount   Price
##        <dbl>  <dbl>   <dbl>    <dbl>  <dbl>   <dbl>  <dbl>  <dbl>   <dbl>
##  1    -0.978  0.112  0.354     1.76  -0.756 -0.504  -0.265  0.353  0.0239
##  2    -0.978 -1.95  -1.45     -1.06  -1.13  -0.504  -0.265 -1.32  -1.48  
##  3    -0.978  0.112 -0.0977   -0.545 -0.135 -0.433  -0.265  0.144  0.0906
##  4    -0.735  0.937 -1.09     -0.494 -0.421 -0.504  -0.265  0.980  1.14  
##  5    -0.613 -1.54  -1.27      0.991 -0.694 -0.0311 -0.265 -0.902  0.229 
##  6    -0.978 -0.712 -1.27     -0.545 -0.234 -0.268  -0.265 -1.32  -1.63  
##  7    -0.857  0.525 -0.0977    0.735 -0.520 -0.504  -0.265 -0.170 -0.779 
##  8    -0.369  0.112 -0.550    -0.596 -0.632 -0.504  -0.265  0.562  0.384 
##  9    -0.735  0.937  0.535     0.991 -0.868 -0.220  -0.265  0.980  0.154 
## 10    -0.735 -0.712 -0.911    -0.392 -0.160 -0.504  -0.265 -1.22  -1.41  
## # … with 3,196 more rows

次はカテゴリ変換についてもいくつかの関数をまとめておく。ワンホットエンコーディングはstep_dummy(one_hot = TRUE)で実行できる。FALSEで使用すると、ベースカテゴリをおいた上で、ダミー変数化が行われる。下記の例ではfixedがベースラインカテゴリとなっている。

recipe(Status ~ ., data = df_train) %>% 
  step_dummy(Job, one_hot = FALSE) %>% 
  prep() %>% 
  bake(new_data = df_train) %>% 
  select(starts_with("Job")) %>% 
  bind_cols(df_train %>% select(Job))
## # A tibble: 3,206 × 4
##    Job_freelance Job_others Job_partime Job    
##            <dbl>      <dbl>       <dbl> <chr>  
##  1             0          0           1 partime
##  2             0          0           1 partime
##  3             0          0           1 partime
##  4             0          0           0 fixed  
##  5             0          0           0 fixed  
##  6             0          0           1 partime
##  7             0          0           0 fixed  
##  8             0          0           0 fixed  
##  9             0          0           1 partime
## 10             0          0           0 fixed  
## # … with 3,196 more rows

ワンホットエンコーディングの例はこちら。

recipe(Status ~ ., data = df_train) %>% 
  step_dummy(Job, one_hot = TRUE) %>% 
  prep() %>% 
  bake(new_data = df_train) %>% 
  select(starts_with("Job")) %>% 
  bind_cols(df_train %>% select(Job))
## # A tibble: 3,206 × 5
##    Job_fixed Job_freelance Job_others Job_partime Job    
##        <dbl>         <dbl>      <dbl>       <dbl> <chr>  
##  1         0             0          0           1 partime
##  2         0             0          0           1 partime
##  3         0             0          0           1 partime
##  4         1             0          0           0 fixed  
##  5         1             0          0           0 fixed  
##  6         0             0          0           1 partime
##  7         1             0          0           0 fixed  
##  8         1             0          0           0 fixed  
##  9         0             0          0           1 partime
## 10         1             0          0           0 fixed  
## # … with 3,196 more rows

お次はラベルエンコーディングを行う。ラベルエンコーディングはラベルを数値にする変換。因子型を数値にスコア化するstep_ordinalscore関数も用意されているが、ここでは文字型を数値に直し、文字型を因子型にしてから数値型に戻す。まずは、文字型を選択できるall_nominal_predictors関数で文字を因子型に変換する。

recipe(Status ~ ., data = df_train) %>% 
  step_string2factor(all_nominal_predictors()) %>% 
  prep() %>% 
  bake(new_data = df_train) %>% 
  str()
## tibble [3,206 × 14] (S3: tbl_df/tbl/data.frame)
##  $ Seniority: num [1:3206] 0 0 0 2 3 0 1 5 2 2 ...
##  $ Home     : Factor w/ 6 levels "ignore","other",..: 4 2 1 6 3 3 6 6 3 6 ...
##  $ Time     : num [1:3206] 48 18 48 60 24 36 54 48 60 36 ...
##  $ Age      : num [1:3206] 41 21 36 25 23 23 36 31 43 27 ...
##  $ Marital  : Factor w/ 5 levels "divorced","married",..: 2 4 2 4 2 4 2 4 2 3 ...
##  $ Records  : Factor w/ 2 levels "no","yes": 1 2 1 1 1 1 1 1 1 1 ...
##  $ Job      : Factor w/ 4 levels "fixed","freelance",..: 4 4 4 1 1 4 1 1 4 1 ...
##  $ Expenses : num [1:3206] 90 35 45 46 75 45 70 44 75 48 ...
##  $ Income   : num [1:3206] 80 50 130 107 85 122 99 90 71 128 ...
##  $ Assets   : num [1:3206] 0 0 750 0 5000 2500 0 0 3000 0 ...
##  $ Debt     : num [1:3206] 0 0 0 0 0 0 0 0 0 0 ...
##  $ Amount   : num [1:3206] 1200 400 1100 1500 600 400 950 1300 1500 450 ...
##  $ Price    : num [1:3206] 1468 500 1511 2189 1600 ...
##  $ Status   : Factor w/ 2 levels "bad","good": 1 1 1 1 1 1 1 1 1 1 ...

これにstep_mutate_at関数で数値型への変換を追加する。

recipe(Status ~ ., data = df_train) %>% 
  step_string2factor(all_nominal_predictors()) %>% 
  step_mutate_at(Job, fn = ~ as.numeric(.)) %>% 
  prep() %>% 
  bake(new_data = df_train) %>% 
  select(starts_with("Job")) %>% 
  bind_cols(df_train %>% select(Job))
## # A tibble: 3,206 × 2
##    Job...1 Job...2
##      <dbl> <chr>  
##  1       4 partime
##  2       4 partime
##  3       4 partime
##  4       1 fixed  
##  5       1 fixed  
##  6       4 partime
##  7       1 fixed  
##  8       1 fixed  
##  9       4 partime
## 10       1 fixed  
## # … with 3,196 more rows

最後は、欠損値補完の方法をまとめおく。学習データには下記の通り、欠損値がいくつかあることがわかる。

map_int(.x = df_train, .f = function(x){sum(is.na(x))})
##    Status Seniority      Home      Time       Age   Marital   Records       Job 
##         0         0         3         0         0         1         0         1 
##  Expenses    Income    Assets      Debt    Amount     Price 
##         0       265        33        13         0         0

欠損値が埋まっているかを確認するために、欠損しているレコードをいくつかサンプリングしておく。

df_train_imp <- df_train %>% 
  mutate(idx = row_number()) %>% 
  filter(idx %in% c(350, 385, 391, 413, 497, 512, 539, 590, 1007, 1040)) %>% 
  select(Status, Marital, Home, Expenses, Income, Assets)
df_train_imp
## # A tibble: 10 × 6
##    Status Marital   Home  Expenses Income Assets
##    <chr>  <chr>     <chr>    <dbl>  <dbl>  <dbl>
##  1 bad    married   <NA>        45     NA     NA
##  2 bad    separated rent        41     58     NA
##  3 bad    married   priv        45    102     NA
##  4 bad    married   owner      150     NA   8000
##  5 bad    married   owner       75     60     NA
##  6 bad    married   owner       45     66     NA
##  7 bad    married   owner       35     NA     NA
##  8 bad    married   owner       60     NA     NA
##  9 good   married   owner       45    110  15000
## 10 good   single    <NA>        35    337     NA

まずは基本的な代表値補間方法である平均値補完から始める。平均値補間はstep_impute_mean関数を利用する。step_impute_median関数であれば中央値補間、step_impute_mode関数であれば最頻値補間となる。

recipe(Status ~ ., data = df_train_imp) %>% 
  step_impute_mean(Assets) %>% 
  step_impute_median(Income) %>% 
  step_impute_mode(Home) %>% 
  prep() %>% 
  bake(new_data = df_train_imp) 
## # A tibble: 10 × 6
##    Marital   Home  Expenses Income Assets Status
##    <fct>     <fct>    <dbl>  <dbl>  <dbl> <fct> 
##  1 married   owner       45     84  11500 bad   
##  2 separated rent        41     58  11500 bad   
##  3 married   priv        45    102  11500 bad   
##  4 married   owner      150     84   8000 bad   
##  5 married   owner       75     60  11500 bad   
##  6 married   owner       45     66  11500 bad   
##  7 married   owner       35     84  11500 bad   
##  8 married   owner       60     84  11500 bad   
##  9 married   owner       45    110  15000 good  
## 10 single    owner       35    337  11500 good

予測補間を行う関数もいくつか用意されているので、ここでは線形回帰予測補間(step_impute_linear)、決定木予測補間(step_impute_bag)、k近傍予測補間(step_impute_knn)の例をまとめておく。説明変数に欠損値が含まれるとエラーになるので注意。

ここではIncomeの欠損値をMarital, Expensesを説明変数としてモデルを作成し、予測値で補間している。

recipe(Status ~ ., data = df_train_imp) %>% 
  step_impute_linear(Income, impute_with = imp_vars(Marital, Expenses)) %>% 
  prep() %>% 
  bake(new_data = df_train_imp) 
## # A tibble: 10 × 6
##    Marital   Home  Expenses Income Assets Status
##    <fct>     <fct>    <dbl>  <dbl>  <dbl> <fct> 
##  1 married   <NA>        45   92.7     NA bad   
##  2 separated rent        41   58       NA bad   
##  3 married   priv        45  102       NA bad   
##  4 married   owner      150  -21.7   8000 bad   
##  5 married   owner       75   60       NA bad   
##  6 married   owner       45   66       NA bad   
##  7 married   owner       35  104.      NA bad   
##  8 married   owner       60   76.3     NA bad   
##  9 married   owner       45  110    15000 good  
## 10 single    <NA>        35  337       NA good

step_impute_linear関数でIncomeの欠損値を埋めたので、step_impute_bag関数では、Incomeを説明変数として利用できる。

recipe(Status ~ ., data = df_train_imp) %>% 
  step_impute_linear(Income, impute_with = imp_vars(Marital, Expenses)) %>% 
  step_impute_bag(Assets, impute_with = imp_vars(Marital, Expenses, Income)) %>% 
  prep(stringsAsFactors = TRUE) %>% 
  bake(new_data = df_train_imp) 
## # A tibble: 10 × 6
##    Marital   Home  Expenses Income Assets Status
##    <fct>     <fct>    <dbl>  <dbl>  <dbl> <fct> 
##  1 married   <NA>        45   92.7  11556 bad   
##  2 separated rent        41   58    11556 bad   
##  3 married   priv        45  102    11556 bad   
##  4 married   owner      150  -21.7   8000 bad   
##  5 married   owner       75   60    11556 bad   
##  6 married   owner       45   66    11556 bad   
##  7 married   owner       35  104.   11556 bad   
##  8 married   owner       60   76.3  11556 bad   
##  9 married   owner       45  110    15000 good  
## 10 single    <NA>        35  337    11556 good

k近傍予測補間(step_impute_knn)であればカテゴリ変数の欠損値も埋めることができる。

recipe(Status ~ ., data = df_train_imp) %>% 
  step_impute_linear(Income, impute_with = imp_vars(Marital, Expenses)) %>% 
  step_impute_bag(Assets, impute_with = imp_vars(Marital, Expenses, Income)) %>% 
  step_impute_knn(Home, impute_with = imp_vars(Expenses, Income, Assets)) %>% 
  prep() %>% 
  bake(new_data = df_train_imp)
## # A tibble: 10 × 6
##    Marital   Home  Expenses Income Assets Status
##    <fct>     <fct>    <dbl>  <dbl>  <dbl> <fct> 
##  1 married   owner       45   92.7  11920 bad   
##  2 separated rent        41   58    11920 bad   
##  3 married   priv        45  102    11920 bad   
##  4 married   owner      150  -21.7   8000 bad   
##  5 married   owner       75   60    11920 bad   
##  6 married   owner       45   66    11920 bad   
##  7 married   owner       35  104.   11920 bad   
##  8 married   owner       60   76.3  11920 bad   
##  9 married   owner       45  110    15000 good  
## 10 single    owner       35  337    11920 good

他にもcaretパッケージにもあったフィルター系の関数もある。相関が高い列を削除するstep_corr関数、分散がほぼ0の列を削除するstep_nzv関数、線形結合を除くstep_lincomb関数などもある。

まとめきれないので、下記recipesパッケージの公式サイトを参照のこと。