map2を使ってサブサンプルに分けた各データに対して複数モデルを適用する
表題通り、サブサンプルに分けた各データに対して複数モデルを適用したい。
より具体的に書くと、以下のdropoutさんの記事では
複数のモデルを当てはめる場合
diamond
データに対して、3モデルを適用サブサンプルに分けて分析する場合
diamond
データをclarity
毎にサブサンプルをnest
で作成しそれぞれに対して、1モデルを適用
といったことをnest
とmap
を使ってモダンに処理する方法の紹介をしている。
これらを複合して、「diamondデータをclarity毎にサブサンプルを作成しそれぞれに対して、3モデルを適用」といったことをおこないたい。
複数のモデルを当てはめる場合 サブサンプルに分けて分析する場合
これら処理だけをまとめると
library(tidyverse) library(tidymodels) df = diamonds %>% mutate_if(is.ordered, factor, ordered = FALSE) # 複数model formulas = c(log(price) ~ clarity, log(price) ~ clarity + carat, log(price) ~ clarity + log(carat)) %>% enframe("model_no", "formula") # 1. 1データに対して複数モデルを適応 formulas %>% mutate(model = map(formula, lm, data = df)) # 2. サブサンプルごとに1モデルを適応 # dfをclarityごとにサブサンプルに分ける df_nested = df %>% group_by(clarity) %>% nest() %>% arrange(clarity) df_nested %>% mutate(model = map(data, ~lm(log(price) ~ log(carat), data = .)))
となる。
ちなみに、上記記事の各関数詳細などを自分なりに解釈してトレースした過去記事はこちら
このとき、
lm
で適用するデータをclarity
毎に変えるlm
で適用するモデル式を変える
この2つをおこなう。つまり、上記コードの
# 1.formula毎に適用 formulas %>% mutate(model = map(formula, lm, data = df)) # 2. data毎に適用 df_nested %>% mutate(model = map(data, ~lm(log(price) ~ log(carat), data = .)))
このどちらかの部分に対して、formula
かdata
を動的に変えることが必要になる。
map
は1引数を2引数目のfunction
(モデル式部分)に適用することができる。そのため、formula
とdata
どちらとも変えることはできない。
そのため、引数を2つfunction
に渡すことができるmap2
を使用する。
なお、はじめのコードにあるモデル式は以下となる。
- log(price) ~ clarity,
- log(price) ~ clarity + carat
- log(price) ~ clarity + log(carat))
今回。clarity
毎にしたサブサンプルを使用するため、式の意味を考えると上記3式のclarity部分は必要ない(特に1つ目はlog(price)
だけの式になる)ので、今回のモチベーションに合わせるためclarityサブサンプル毎の場合ほぼ同じ意味になる*1以下の2式に修正して適用する。
- log(price) ~ carat
- log(price) ~ log(carat))
実践
モデル式一覧の作成
まずはモデル式一覧を作成する。
# formula候補一覧のデータフレーム formulas = c(log(price) ~ carat, log(price) ~ log(carat)) %>% enframe("model_no", "formula") ## サブサンプルに分けない場合以下 # formulas = c(log(price) ~ clarity + carat, # log(price) ~ clarity + log(carat)) %>% # enframe("model_no", "formula")
モデル式とclarityのセットを作成
次に、map2
に指定するために上で作成したformulas
に各clarity
を付与した列を作成する。
# サブセット一覧の組み合わせ formulas = formulas %>% expand_grid(tibble(subset = unique(df$clarity))) # model_noごとに各clarityを付け加える。6model * 8clarity
後に記述するが、formulas
オブジェクトに対してmap2
で処理した結果をmutate
で加えるため、map2
に渡す引数をすべてformulas
オブジェクトで完結させるために各clarity
を付与した列を作成している。
なお、expand_grid
は、適用するオブジェクトに対してexpand_grid
内で指定したデータとの全組み合わせを作成する関数となる。
例えば、以下のコードは1,2,3を取るx
と1,2を取るy
の全組み合わせを出力する。
expand_grid(x = 1:3, y = 1:2)
サブサンプルに分けた各データに対して複数モデルを適用する
# map2() で2変数の組み合わせを並列処理 df_result = formulas %>% # 第2引数.y(subset)で絞り込みをしたデータに対して第1引数.x(formula)を適用 mutate(model = map2(formula, subset, ~lm(.x, data = filter(df, clarity == .y))))
モデルの結果を係数とともに示すと以下のようになる。
df_result %>% mutate(tidied = map(model, tidy)) %>% select(model_no, subset, tidied) %>% unnest() %>% mutate_if(is.double, round, digits=3)
おまけ
元のソースのmap
のままfor
を組み合わせると以下のようになる。
df = diamonds %>% mutate_if(is.ordered, factor, ordered = FALSE) # 複数model formulas = c(log(price) ~ carat, log(price) ~ log(carat)) %>% enframe("model_no", "formula") # clarityの一覧 clarities = df %>% distinct(clarity) %>% pull() # 1. 1データに対して複数モデルを適応をloop df_output = data.frame() # 空のDF for (i_clarity in clarities) { # サブサンプルを作成 df_filtered_clarity = df %>% filter(clarity == i_clarity) df_output_ = formulas %>% mutate(subsumple = i_clarity, model = map(formula, lm, data = df_filtered_clarity)) # df_output_を結合していく df_output = df_output %>% bind_rows(df_output_) }
余談
今回も r-wakalangで質問しました。回答してくださったill_identifiedさんありがとうございました!
参考
*1:正確には、上式ではclarity毎に切片が変わるが他の変数の係数は全clarityで共通。下式では他の変数の係数はclarity毎で変わるため同じではない