purrrを使って、関数の引数を変えて適用したDFを結合していく
やりたいこと
関数の引数を変えて適用したDFを結合したい。
以下のように、指定した列col
に対してunder_value
以下となるdfを抽出して、そのunder_value
をfiltered_values
列に追加する関数filtered_under_value
で考える。なお、見た目上わかりやすいためhead
で2行のみ抜いてきている。
library(tidyverse) data("diamonds") # under_value以下のcolを抽出 filtered_under_value = function(df, col, under_value) { col = enquo(col) df_filtered = df %>% filter(!!col <= under_value)%>% mutate(filtered_values = under_value) %>% head(2) return(df_filtered) } filtered_under_value(diamonds, price, 350)
このunder_value
を変えたdfを結合するには3つの方法がある。
ひたすらコピペする
以下のように、dfを結合するbind_rows
をひたすらコピペして貼り付けていく。
filtered_under_value(diamonds, price, 350) %>% bind_rows(filtered_under_value(diamonds, price, 400)) %>% bind_rows(filtered_under_value(diamonds, price, 450))
for文で回す
コピペはできるだけ避けた方が保守など色々な面で良いので、空のdfに対してfor
で回したdfを結合していく。
values = c(350, 400, 450) df = data.frame() for (v in values) { tmp_df = filtered_under_value(diamonds, price, v) df = df %>% bind_rows(tmp_df) }
reduceとmapを組み合わせる
for
のようなiterateはpurrr::map
で置き換えることができる。こちらの方がシンプルなので可読性が上がる。また、上述の空dfに結合していく方法は空dfを作成する部分を走らせ損なうと値が2重で入るなど、ミスを生みやすい。
df = reduce( map(c(350, 400, 450), ~ filtered_under_value(diamonds, price, .)), bind_rows )
map
とreduce
を使っていて、それぞれちゃんと理解していないと難しいのでそれぞれ解説する。
map
map(.x, .f, ...)
関数は .x
のlistの中身を1つずつ.f
に適用してlistで返す関数となる(list以外で返す関数もそれぞれ用意されている)。
つまり、前述のコードのmap
部分では以下のようにlistc(350, 400, 450)
の3つの値がfiltered_under_value(diamonds, price, .)
の.
部分に前から入っていき結果をそれぞれlistとして格納する。
m_list = map(c(350, 400, 450), ~ filtered_under_value(diamonds, price, .))
reduce
reduce(.x, .f, ...)
は、.x
のlistの中身を前から適用し、自分と1つ前の結果を2変数関数(引数を2つ取る関数).f
に適用して最終的な結果を返す。
「自分と1つ前の結果を2変数関数(引数を2つ取る関数).f
に適用」とは、例えば下記のようなコードの場合、c(1, 2, 3)
の1回目は1つ前が無いため.f
である+
の結果は1
、2回目は2
と1回目の結果を.f
に適用して1+2
、3回目は3
と2回目の結果1+2(3)
の結果を.f
に適用して(1+2)+3
となり、最後の3回目の結果(1+2)+3
を返す。
reduce(c(1, 2, 3), `+`) # => 6
なお、余談だがこの過程をまとめてvectorで返すaccumurate
という関数もある。
accumulate(1:3, `+`) # => [1] 1 3 6
今までの説明を踏まえてわかりやすく書き直すと以下のようになる。
m_list = map(c(350, 400, 450), ~ filtered_under_value(diamonds, price, .)) df = reduce(m_list, bind_rows)
つまり、map
でc(350, 400, 450)
を順にfiltered_under_value
に適用した結果がlist m_list
として格納され、reduce
でlistm_list
を2変数関数bind_rows
を用いて、map
内でのloopの1回目はlist m_list
の1つ目の結果のみ、2回目はlist m_list
の2つ目と1回目の結果をbind_rows
した結果、3回目はlist m_list
の3つ目と2回目の結果とbind_rows
してこの3回めの結果を返すことになる。
追記
Each argument can either be a data frame, a list that could be a data frame, or a list of data frames.
bind_rows
はdfのlistを受け取った場合、list内を縦結合するため前述のコードはreduce
を使わないでも問題ないようです。
# old df = reduce( map(c(350, 400, 450), ~ filtered_under_value(diamonds, price, .)), bind_rows ) # new df = map(c(350, 400, 450), ~ filtered_under_value(diamonds, price, .)) %>% bind_rows()