dplyr::mutate内でベクトル化されていない関数を使う - purrr::pmapでのカラム名の対応付け

Rのデータフレームの各行に対して処理を行いたいが、関数をベクトル化することが難しい場合*1には、下で簡単に書くようにmutatemapを組み合わせると非常に便利です。しかし、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関数にfromtoの2つの引数を与えたいので、map2関数を使います。

df2 <- df1 %>% mutate(d = map2(a, b, seq))

print(df2$d)

下にこの処理がどのように行われているかのイメージ図を貼ってみました。

f:id:yoshidk6:20180806153334p:plain

もちろん、使用したい処理が一つの引数のみを取る場合はmapを使えばいいですし、作成する列をdoublecharacter型にしたければmap_dblmap_chrなどを使います。 また、mapmap2では、関数内で明示的に引数を指定したい場合、.x.yを使います。例えば、上の例でdf2 <- mutate(df1, d = map2(a, b, ~seq(.y, .x)))を実行してみてください。

各行について別々に処理、入力値が3つ以上の場合

この様にmap系関数を使うと、ベクトル化されていない処理をmutate内で簡潔に書くことが出来ます。 3つ以上のカラムを使用したい場合、同様にpmapを使うことになるのですが、カラム名と関数の引数名の対応付けが少し複雑になります。

以下の例を用いて、どのように対応付けを行うことができるのか見ていきます。

使用する例

複数の平均値・標準偏差の組み合わせについて、それぞれ異なった数の乱数をrnorm関数を用いて算出したい。

ここではまず、meansdnに対応する3つのカラムを持ったデータフレームを作成し、各行ごとにrnorm関数を適用して乱数を発生させています。新しく生成されたカラムdataには、各行ごとに乱数値を保持したベクトルが格納されます。
結果を見やすくするため、この例では更にunnnest()関数を用いて縦長のデータフレームに変換していますが、変換せずにそのままmap系関数を使って処理を続けていくことも可能です。
ネスト化周りに関してはR4DSなど色々と解説があると思うので、馴染みのない方はそちらも参考にしてみてください。

単純な例

データフレーム内のカラムが、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系関数の使い方について参考になるサイト

*1:特にnested dafa frameを扱っているときに頻発します