dplyr::mutate内でベクトル化されていない関数を使う - purrr::pmapでのカラム名の対応付け
Rのデータフレームの各行に対して処理を行いたいが、関数をベクトル化することが難しい場合*1には、下で簡単に書くようにmutate
とmap
を組み合わせると非常に便利です。しかし、map
内で定義する関数の入力値が3つ以上になると話が少しややこしくなります。納得するまで結構混乱したので、自分の備忘録も兼ねて記事にしておくことにしました。
通常のmutate
を使った処理
殆どの場合、mutate内で行う処理(例えば+
、*
、ifelse
等)はベクトル化されているので、出力値(下のケースではc
)が他の列と同じ長さになり、元のデータフレームに追加されます。
library(tidyverse) df0 <- tibble(a = 1:3, b = 4:6) df0 %>% mutate(c = a + b)
各行について別々に処理、入力値が2つまでの場合
こちらにあるように、データフレームの各行に対して等差数列を作成する例を考えてみます。
このとき、seq
関数はベクトル化されていないため、以下のように書くとエラーになります。
df1 <- tibble(a = c(1, 2), b = c(3, 6), c = c(8, 10)) df %>% mutate(d = seq(x, y)) # Error in mutate_impl(.data, dots) : Evaluation error: 'from' must be of length 1.
ここでmap
系関数の出番です。
map
系関数は、リストを引数とし、リスト内の各要素に対して与えられた関数を適用した結果を返すという働きを持っています。
Rのデータフレームは各列がそれぞれリストとなっているので、map
系巻数と相性が良くなっています。
今回の例ではseq
関数にfrom
とto
の2つの引数を与えたいので、map2
関数を使います。
df2 <- df1 %>% mutate(d = map2(a, b, seq)) print(df2$d)
下にこの処理がどのように行われているかのイメージ図を貼ってみました。
もちろん、使用したい処理が一つの引数のみを取る場合はmap
を使えばいいですし、作成する列をdouble
やcharacter
型にしたければmap_dbl
やmap_chr
などを使います。
また、map
やmap2
では、関数内で明示的に引数を指定したい場合、.x
や.y
を使います。例えば、上の例でdf2 <- mutate(df1, d = map2(a, b, ~seq(.y, .x)))
を実行してみてください。
各行について別々に処理、入力値が3つ以上の場合
この様にmap
系関数を使うと、ベクトル化されていない処理をmutate
内で簡潔に書くことが出来ます。
3つ以上のカラムを使用したい場合、同様にpmap
を使うことになるのですが、カラム名と関数の引数名の対応付けが少し複雑になります。
以下の例を用いて、どのように対応付けを行うことができるのか見ていきます。
使用する例
複数の平均値・標準偏差の組み合わせについて、それぞれ異なった数の乱数を
rnorm
関数を用いて算出したい。
ここではまず、mean
、sd
、n
に対応する3つのカラムを持ったデータフレームを作成し、各行ごとにrnorm
関数を適用して乱数を発生させています。新しく生成されたカラムdata
には、各行ごとに乱数値を保持したベクトルが格納されます。
結果を見やすくするため、この例では更にunnnest()
関数を用いて縦長のデータフレームに変換していますが、変換せずにそのままmap
系関数を使って処理を続けていくことも可能です。
ネスト化周りに関してはR4DSなど色々と解説があると思うので、馴染みのない方はそちらも参考にしてみてください。
- R for Data Science
- Rではじめるデータサイエンス、 20.2.1 入れ子データ
- tidyverse脳になって階層構造のあるデータフレームを使いこなそう
単純な例
データフレーム内のカラムが、pmap内で使用する関数の引数と個数・名前共に一致する場合にのみ、以下のような簡潔な文法が通じます。
df4 <- tribble(~mean, ~sd, ~n, 1, 0.03, 2, 10, 0.1, 4, 5, 0.1, 4) df4 %>% mutate(data = pmap(., rnorm)) %>% unnest()
ちなみに、下のような書き方だと、rnorm内の入力はdf4$n
df4$mean
df4$sd
であると見做されてしまい、想定していない結果となってしまいます。
df4 %>% mutate(data = pmap(., ~rnorm(n, mean, sd))) %>% unnest() # Wrong answer
余分なカラムが含まれている場合
実際には、カラム数が関数の引数の数と一致するという状況は非常に稀だと思います。 余計なカラムが含まれていると、unused argumentがあると言って怒られます。
df5 <- tribble(~mean, ~sd, ~dummy, ~n, 1, 0.03, "a", 2, 10, 0.1, "b", 4, 5, 0.1, "c", 4) df5 %>% mutate(data = pmap(., rnorm)) # Error
これを回避するには、2つの方法があります。
小さいリストを作ってしまう
1つ目は、pmapの第一引数をdf4
のようなリストにその場で変えてしまう方法です。(参照: Dplyr: Alternatives to rowwise - tidyverse - RStudio Community )
df5 %>% mutate(data = pmap(list(n=n, mean=mean, sd=sd), rnorm)) %>% unnest()
注意点として、下のようにリスト内要素に名前をつけないと、rnormへの受け渡しが順序のみによって行われてしまいます。少し面倒ですが、上のようにきちんと名前を割り振る書き方を推奨します。
df5 %>% mutate(data = pmap(list(n, mean, sd), rnorm)) %>% unnest() # Correct but not recommended df5 %>% mutate(data = pmap(list(mean, sd, n), rnorm)) %>% unnest() # Wrong answer
...
で未使用変数を受け流す
2つ目は、pmap
への入力ではデータフレームをそのまま渡す一方で、関数宣言の所で未使用の変数を受け流す方法です(参照: Map over multiple inputs simultaneously. — map2 • purrr, "Use ...
to absorb unused components of input list .l")。
pmap
が、入力したリスト名と関数の引数名を自動で照合してくれる性質を利用しているので、function()
内の変数名とデータフレームのカラム名が一致する必要があります。
引数を二回書く必要が出てきますが、リスト内要素に名前をつけ忘れる心配はありません。
df5 %>% mutate(data = pmap(., function(n, mean, sd, ...) rnorm(n=n, mean=mean, sd=sd))) %>% unnest()
ただしもちろん、普段関数を使う際と同様に、引数を名前で紐付けないと誤った結果となります。
df5 %>% mutate(data = pmap(., function(n, mean, sd, ...) rnorm(n, mean, sd))) %>% unnest() # Correct but not recommended df5 %>% mutate(data = pmap(., function(n, mean, sd, ...) rnorm(mean, sd, n))) %>% unnest() # Wrong answer df5 %>% mutate(data = pmap(., function(mean, sd, n, ...) rnorm(mean, sd, n))) %>% unnest() # Wrong answer
先にdf4
の例で見たように、以下のような書き方も誤った結果となります。
df5 %>% mutate(data = pmap(., function(...) rnorm(n=n, mean=mean, sd=sd))) %>% unnest() # Wrong answer
カラム名がpmap内で使用する関数の引数名と一致しない場合
上2つのいずれの方法を使っても対応できます。
df6 <- tribble(~mean1, ~sd1, ~dummy, ~n1, 1, 0.03, "a", 2, 10, 0.1, "b", 4, 5, 0.1, "c", 4) df6 %>% mutate(data = pmap(list(mean=mean1, sd=sd1, n=n1), rnorm)) %>% unnest() df6 %>% mutate(data = pmap(., function(n1, mean1, sd1, ...) rnorm(n=n1, mean=mean1, sd=sd1))) %>% unnest()
map系関数の使い方について参考になるサイト
- Lessons and Examples
- GitHub - jennybc/row-oriented-workflows: Row-oriented workflows in R with the tidyverse
*1:特にnested dafa frameを扱っているときに頻発します