purrrがついに出ました!!! 思ったより早かったです。
purrrとは何か
Purrr is a new package that fills in the missing pieces in R’s functional programming tools: it’s designed to make your pure functions purrr.
purrrは、Rで関数型プログラミングをするためのパッケージです。rlistのライバルだと思われがちですが、リストをうまく処理するためのパッケージではありません。
ピュアな心で汚いデータに挑むのは簡単なことではありません。それなりに学習コストがかかる予感がしています。私は関数型脳になれたら楽しい気がするのでpurrrな道を進もうと思いますが、すべての人におすすめできるかはまだ自信がありません。
前のエントリでも書きましたが、餅は餅屋、リストはrlistです。rlistは日本語の情報量も多いし、ドキュメントもしっかりしています。リストの扱いに困っていてpurrrが目についた、という方はぜひそちらを検討してみてください。
と前置きして、さっそく使ってみます。
map
map()
は、リストやベクトルに関数を適用する関数です。applyファミリー(lapply
, sapply
とか)に近いイメージです。
上のブログ記事で紹介されていた例はこんな感じです(少し変えています)。
mtcars %>% split(.$cyl) %>% map(~lm(mpg ~ wt, data = .)) %>% map(summary) %>% map("r.squared") #> $`4` #> [1] 0.5086326 #> #> $`6` #> [1] 0.4645102 #> #> $`8` #> [1] 0.4229655
split()
は、data.frameを分割して、data.frameのリストを作ります。(これはbaseの関数です)
mtcars %>% split(.$cyl) %>% str(max.level = 1) #> List of 3 #> $ 4:'data.frame': 11 obs. of 11 variables: #> $ 6:'data.frame': 7 obs. of 11 variables: #> $ 8:'data.frame': 14 obs. of 11 variables:
map()
は、この3つのdata.frameそれぞれに関数を適用していますが、map()
が取れる引数には3種類あります。
ふつうの関数
関数オブジェクトを渡すと、それを適用してくれます。
x %>% map(summary)
適用する関数に別の引数を渡したければ、続けて指定することができます。
x %>% map(head, n = 3)
ラムダ関数
~
を使ってその場で関数をつくることもできます。これは、関数をつくるpurrr独特の記法です。
たとえば、
x %>% map(~lm(mpg ~ wt, data = .))
は、以下の2つと同じ意味です。
x %>% map(function(x) lm(mpg ~ wt, data = x)) f <- function(x) lm(mpg ~ wt, data = x) x %>% map(f)
要素名
要素の名前を文字列で渡すと、それを取り出してくれます。
たとえば、
x %>% map("r.squared")
は、以下と同じ意味です。
x %>% map(~ .[["r.squared"]]) x %>% map(`[[`, "r.squared")
map_lgl, map_chr, map_int, map_dbl
map_型名()
のような名前の関数群があります。map()
は単にリストを返しますが、これらは各型のベクトルを返す関数です。
たとえばこんな感じ。
list(x = 1, y = 2) %>% map(`+`, 3) #> $x #> [1] 4 #> #> $y #> [1] 5 list(x = 1, y = 2) %>% map_dbl(`+`, 3) #> x y #> 4 5
見て分かるように、map_dbl()
の方はベクトルを返しています。それぞれ次のような型を返します。
map_lgl()
: factor型map_chr()
: character型map_int()
: integer型map_dbl()
: double型
ただし、これは「この型に変換してくれる」わけではなくて、型が違うとエラーになります。(関数型っぽいノリだ!)
list(x = 1, y = 2) %>% map_int(`+`, 1) #> Error in vapply(.x, .f, ..., FUN.VALUE = integer(1)) : #> values must be type 'integer', #> but FUN(X[[1]]) result is type 'double'
integerであるべきところがdoubleだったのでエラーになっています。勝手に変換とかしてくれません。
また、関数を適用した結果はそれぞれ要素数が1つだけである必要があります。複数の要素を返すものがあるとエラーになります。
1:3 %>% map(seq) #> [[1]] #> [1] 1 #> #> [[2]] #> [1] 1 2 #> #> [[3]] #> [1] 1 2 3 1:3 %>% map_dbl(seq) #> Error in vapply(.x, .f, ..., FUN.VALUE = double(1)) : #> values must be length 1, #> but FUN(X[[2]]) result is length 2
つまり、逆にいうと、これがエラーにならないなら以下が保証されていることになります。
- 各結果が指定された型になっている
- 各結果が長さ1の要素
長さが1以上の要素を返すものをベクトルにしたい場合は、flatmap()
を使います。.type
引数で、あるべき型を指定することもできます。
list(x = 1, y = 2, z = 3) %>% flatmap(seq) #> x y1 y2 z1 z2 z3 #> 1 1 2 1 2 3 list(x = 1, y = 2, z = 3) %>% flatmap(seq, .type = "character") #> Error: Results do not conform to .type
map2, map3, map_n
map()
はひとつのリストやベクトルに関数を適用するだけでしたが、2つ、3つ、n個に適用するためにmap2()
、map3()
, map_n()
が用意されています。
map2(1:3, 2:4, `*`) #> [[1]] #> [1] 2 #> #> [[2]] #> [1] 6 #> #> [[3]] #> [1] 12 map_n(list("I", "love", list("apple", "R", "sushi")), paste) #> [[1]] #> [1] "I love apple" #> #> [[2]] #> [1] "I love R" #> #> [[3]] #> [1] "I love sushi"
map_call
map_call()
は、do.call()
のように、関数名を文字列でも渡すこともできます。
map_call(list("I", "love", list("apple", "R", "sushi")), "paste") [1] "I love apple" "I love R" "I love sushi"
map_if, map_at
map_if()
は条件にマッチする要素にだけmapする関数です。これけっこう便利。
たとえば、NAだけ置き換えたいようなときは、以下のように書きます。(~("値")
で、引数に関わらず固定の値を返す関数をつくれます)
set.seed(8) sample(c(NA, "yes"), 4, replace = TRUE) %>% map_if(is.na, ~("Not Available")) #> [[1]] #> [1] "Not Available" #> #> [[2]] #> [1] "Not Available" #> #> [[3]] #> [1] "yes" #> #> [[4]] #> [1] "yes"
map_at()
は、指定したインデックスの要素にだけmapする関数です。
たとえば、上と同じくNA
の要素にだけ関数を適用したいときは、is.na
なインデックスをつくって引数として渡します。
sample(c(NA, "yes"), 4, replace = TRUE) %>% map_at(which(is.na(.)), ~("Not Available"))
map_rows
これはちょっと難しいので別の機会に。たぶんlift()
あたりについて理解しないと使い方が分からなそう...
感想
map()
は、パイプ時代のapply
ファミリーといった趣です。直観的にわかることが多いので積極的に使っていこうと思います。