purrr 0.1.0を使ってみる(1) map

purrrがついに出ました!!! 思ったより早かったです。

blog.rstudio.org

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ファミリーといった趣です。直観的にわかることが多いので積極的に使っていこうと思います。