まずは蝋の翼から。

学んだことを書きながら確認・整理するためのメモブログ。こういうことなのかな?といったことをふわっと書いたりしていますが、理解が浅いゆえに的はずれなことも多々あると思うのでツッコミ歓迎

purrrを使って、関数の引数を変えて適用したDFを結合していく

やりたいこと

関数の引数を変えて適用したDFを結合したい。

以下のように、指定した列colに対してunder_value以下となるdfを抽出して、そのunder_valuefiltered_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)

f:id:chito_ng:20200623082116p:plain:w600

この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))

f:id:chito_ng:20200623082236p:plain:w600

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)
}

f:id:chito_ng:20200623082236p:plain:w600

reduceとmapを組み合わせる

forのようなiterateはpurrr::mapで置き換えることができる。こちらの方がシンプルなので可読性が上がる。また、上述の空dfに結合していく方法は空dfを作成する部分を走らせ損なうと値が2重で入るなど、ミスを生みやすい。

df = reduce(
  map(c(350, 400, 450),
      ~ filtered_under_value(diamonds, price, .)),
  bind_rows
)

f:id:chito_ng:20200623082236p:plain:w600

mapreduceを使っていて、それぞれちゃんと理解していないと難しいのでそれぞれ解説する。

map

map(.x, .f, ...)関数は .xのlistの中身を1つずつ.fに適用してlistで返す関数となる(list以外で返す関数もそれぞれ用意されている)。

purrr.tidyverse.org

つまり、前述のコードのmap部分では以下のようにlistc(350, 400, 450)の3つの値がfiltered_under_value(diamonds, price, .).部分に前から入っていき結果をそれぞれlistとして格納する。

m_list = map(c(350, 400, 450),
    ~ filtered_under_value(diamonds, price, .))

f:id:chito_ng:20200623083603p:plain:w600

reduce

reduce(.x, .f, ...)は、.xのlistの中身を前から適用し、自分と1つ前の結果を2変数関数(引数を2つ取る関数).fに適用して最終的な結果を返す。

purrr.tidyverse.org

「自分と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

purrr.tidyverse.org

今までの説明を踏まえてわかりやすく書き直すと以下のようになる。

m_list = map(c(350, 400, 450),
             ~ filtered_under_value(diamonds, price, .))

df = reduce(m_list, bind_rows)

つまり、mapc(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.

dplyr.tidyverse.org

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()

参考

heavywatal.github.io

qiita.com