Heavy Watal

purrr — ループ処理やapply系関数の決定版

forループやlistの処理などをより簡潔に書けるようにしてくれるパッケージ。 標準のapply系関数よりも覚えやすく読みやすい。 dplyrtidyr と組み合わせて使う。 いまのところ並列化する機能はないので、 それに関してはforeach/parallelページを参照。

tidyverse に含まれているので、 install.packages('tidyverse') で一括インストール、 library(tidyverse) で一括ロード。

list, vector操作

各要素に関数を適用するapply系関数

v = list(1, 2L, "3")
check_class = function(x) {paste0(x, " is ", class(x))}

# 自分でfor文を書くと結構大変
results = vector("list", length(v))
for (i in seq_along(v)) {
  results[[i]] = check_class(v[[i]])
}

# 1行で簡潔に記述でき、意図も明確
results = lapply(v, check_class)
results = purrr::map(v, check_class)
results = purrr::map(v, function(x) {paste0(x, " is ", class(x))})
results = purrr::map(v, ~ {paste0(.x, " is ", class(.x))})
purrr::map(.x, .f, ...)
list/vector .x の各要素に関数 .f を適用した結果をlistに詰めて返す。 base::lapply()とほぼ同義。
.f にformulaや数値などを渡すと関数に変換した上で処理してくれる。(後述)
purrr::map_lgl(), map_int(), map_dbl(), map_chr()
型の決まったvectorを返すmap亜種。 base::sapply()base::vapply()よりも覚えやすく読みやすい。
purrr::map_dfr(.x, .f, ..., .id=NULL)
結果をdplyr::bind_rows()で結合したdata.frameとして返すmap亜種。 例えば、同じ形式のCSVファイルを一気に読んで結合、みたいなときに便利:
files = fs::dir_ls("path/to/data/", glob = "*.csv")
combined_df = purrr::map_dfr(files, readr::read_csv)
purrr::map2(.x, .y, .f, ...)
2変数バージョン。map2_int()などの型指定vector版もある。 3変数以上渡したいときはlistかdata.frameにまとめて次のpmap()を使う。
purrr::pmap(.l, .f, ...)
listの中身をparallelに処理するmap。 関数 .fの引数はlistの要素名と一致させるか ... で受け流す必要がある。 e.g., pmap(list(a=1:3, b=4:6), function(a, b) {a * b})
data.frameの正体はlist of columnsなので、 そのまま.lとして渡せる。
pmap_int など出力型指定の亜種もある。
purrr::map_if(.x, .p, .f, ...)
.pTRUEになる要素のみ.f()を適用し、残りはそのまま出力。 .pはlogical vectorでもいいし、 .x[[i]]を受け取るpredicate関数でもよい。
番号か名前で選ぶにはpurrr::map_at(.x, .at, .f, ...)
purrr::walk(.x, .f, ...)
map()同様に関数を適用しつつ元の値をそのままinvisible返しする亜種。
purrr::lmap(.x, .f, ...)
.x[[i]]ではなく.x[i]を参照する亜種。
purrr::imap(.x, .f, ...)
名前や整数インデックスを第二引数で受け取れる亜種。 iwalk() などの派生もある。
purrr::modify(.x, .f, ...)
入力と同じ型で出力する亜種。 つまりdata.frameを入れたらlistじゃなくてdata.frameが出てくる。 e.g., modify_if(iris, is.numeric, round)
purrr::reduce(.x, .f, ..., .init)
二変数関数を順々に適用して1つの値を返す。 C++でいうstd::accumulate()。 例えば reduce(1:3, `+`) の結果は6。
purrr::accumulate(.x, .f, ..., .init)
二変数関数を順々に適用し、過程も含めてvectorで返す。 C++でいうstd::partial_sum()。 例えば accumulate(1:3, sum) の結果は 1 3 6

list作成・変形・解体

purrr::list_along(x)
x と同じ長さの空listを作る vector("list", length(x)) のショートカット。
purrr::flatten(.x)
階層性のあるlistを一段階解消する。 階層なしlistをvector化するには明示的に型指定できる flatten_lgl(), flatten_int(), flatten_dbl(), flatten_chr(), flatten_dfr() のほうが標準のunlist()よりも安心。
purrr::keep(.x, .p, ...), discard(), compact()
listやvectorの要素を .p に応じて取捨選択。 .p に関数を渡した場合の挙動は .x[.p(.x)] じゃなくて .x[map_lgl(.x, .p, ...)] となることに注意。
purrr::pluck(.x, ..., .default=NULL)
オブジェクト .x 内の要素を引っ張り出す [[ の強力版。 ... には整数、文字列、関数、listで複数指定できる。 例えば accessor(x[[1]])$foo だと読む順が左右に振られるが、 pluck(x, 1, accessor, "foo") だと左から右に読み流せる。
purrr::cross2(.x, .y, .filter=NULL)
listの各要素の組み合わせを作る。 .filter に渡した関数が TRUE となるものは除外される。 名前付きlistを渡す purrr::cross()purrr::cross_df() のほうが便利かも。 vectorなら tidyr::crossing() とか tidyr::expand() が使える。
purrr::transpose(.l)
行列転置関数t()のlist版。 例えば、pair of lists <=> list of pairs。
data.frameに適用するとlist of rowsが得られる。

その他

purrr::invoke(.f, .x=NULL, ..., .env=NULL)
list .x の中身を引数として関数 .f を呼び出す。
関数に渡す引数があらかじめlistにまとまってるときに使うdo.call()の改良版。
params = list(n = 6L, size = 10L, replace = TRUE)
purrr::invoke(sample.int, params)
purrr::invoke_map(.f, .x=list(NULL), ..., .env=NULL)
関数listを順々に実行してlistで返す。 引数.xは同じ長さのlist of listsか、list of a listをリサイクル。
e.g., invoke_map(list(runif, rnorm), list(c(n=3, 0, 1)))
purrr::has_element(.x, .y)
list .x は要素 .y を持っている。
purrr::set_names(x, nm=x)
標準のsetNames(x=nm, nm)は第二引数のほうが省略不可という気持ち悪い定義だったが、 この改良版ではその心配が解消されている。 長さや型のチェックもしてくれる。

無名関数

apply/map系関数は、名前のついた関数だけでなく、その場で定義された無名関数も受け取れる。 ごく短い関数や一度しか使わない関数に名前をつけずに済むので便利。 さらにpurrrのmap系関数はformula (チルダで始まる ~ x + y のようなもの) や数値を受け取って関数として処理してくれる。

# named function
ord = function(x) {strtoi(charToRaw(x), 16L)}
map_int(letters, ord)

# unnamed function
map_int(letters, function(x) {strtoi(charToRaw(x), 16L)})

# formula
map_int(letters, ~ strtoi(charToRaw(.x), 16L))

# integer/character
li = list(lower = letters, upper = LETTERS)
map_chr(li, 3L)
map_chr(li, function(x) {x[[3L]]})

formula内部では、第一引数を.xまたは.として、第二引数を.yとして参照する。 ..1, ..2, ..3 のような形で三つめ以降も参照できる。

purrr::as_mapper(.f, ...)
map() 内部で関数への変換機能を担っている関数。
formulaを受け取ると function (.x, .y, . = .x) のような関数に変換する。
数値や文字列を受け取ると [[ による抽出関数に変換する。 参照先が存在しない場合の値はmap関数の .default 引数で指定できる。
purrr::partial(...f, ..., .env, .lazy, .first)
引数を部分的に埋めてある関数を作る。C++でいう std::bind()

purrrlyr

data.frame を引数にとるものは purrr 0.2.2.1 から切り離され、 purrrlyr に移動された。 これらは今のところdeprecatedではないが、 近いうちにそうなるので早くほかのアプローチに移行せよ 、とのこと。 https://github.com/hadley/purrrlyr/blob/master/NEWS.md

tidyr でネストして、 purrr でその list of data.frames に処理を施し、 dplyr でその変更を元の data.frame に適用する、 というのがtidyverse流のモダンなやり方らしい。

パイプ演算子 %>% についてはdplyrを参照。

## OLD
iris %>%
  purrrlyr::slice_rows('Species') %>%
  purrrlyr::by_slice(head, .collate='rows')

## NEW
iris %>%
  tidyr::nest(-Species) %>%
  dplyr::mutate(data= purrr::map(data, head)) %>%
  tidyr::unnest()
purrrlyr::dmap(.d, .f, ...)
data.frameかgrouped_dfを受け取り、列ごとに.fを適用してdata.frameを返す。 対象列を選べるdmap_if()dmap_at()もある。 全列で長さが揃っていれば怒られないので dplyr::mutate_all()的にもdplyr::summarise_all()的にも使える。 ただし.fに渡せるのは単一の関数のみでdplyr::funs()は使えない。 パッケージ作者は dplyrmutate_*()summarise_*() の利用を推奨。
purrrlyr::slice_rows(.d, .cols=NULL)
指定した列でグループ化してgrouped_dfを返す。 dplyr::group_by_(.dots=.cols) と同じ。
purrrlyr::by_slice(.d, ..f, ..., .collate=c('list', 'rows', 'cols'), .to='.out', .labels=TRUE)
grouped_dfを受け取ってグループごとに関数を適用する。 dplyr::do() とほぼ同じ役割で、一長一短。 こちらは出力形式をより柔軟に指定できるが、 中の関数からgrouping variableを参照できないという弱点を持つ。
purrrlyr::by_row(.d, ..f, ..., .collate=c('list', 'rows', 'cols'), .to='.out', .labels=TRUE)
data.frame 1行ごとに関数を適用する。 dplyr::rowwise() %>% dplyr::do()的な処理を一撃で書ける。
.to: 結果listの列名
.labels: 元の.dの列をラベルとして結果に残すか
.collate: 結果列の展開方法(下記例)。 とりあえずlistにしておいて後でunnest()するのが無難か。
purrrlyr::invoke_rows()はかなり似ているが、 データと関数の順序が逆になっている点と、 .f.dの列名で引数を取るという点で異なる。 (by_row()では1行のdata.frameとして受け取って.$colのように参照する)
purrrlyr::invoke_rows(.f, .d, ..., .collate, .to, .labels)
第一引数が関数になってるところはinvoke()っぽくて、 結果の収納方法を.collateなどで調整できるところはby_row()っぽい。
iris[1:3, 1:2] %>% by_row(~data_frame(x=0:1, y=c('a', 'b')), .collate='list')
## Source: local data frame [3 x 3]
##
##   Sepal.Length Sepal.Width           .out
##          (dbl)       (dbl)          (chr)
## 1          5.1         3.5 <tbl_df [2,2]>
## 2          4.9         3.0 <tbl_df [2,2]>
## 3          4.7         3.2 <tbl_df [2,2]>
iris[1:3, 1:2] %>% by_row(~data_frame(x=0:1, y=c('a', 'b')), .collate='rows')
## Source: local data frame [6 x 5]
##
##   Sepal.Length Sepal.Width  .row     x     y
##          (dbl)       (dbl) (int) (int) (chr)
## 1          5.1         3.5     1     0     a
## 2          5.1         3.5     1     1     b
## 3          4.9         3.0     2     0     a
## 4          4.9         3.0     2     1     b
## 5          4.7         3.2     3     0     a
## 6          4.7         3.2     3     1     b
iris[1:3, 1:2] %>% by_row(~data_frame(x=0:1, y=c('a', 'b')), .collate='cols')
## Source: local data frame [3 x 6]
##
##   Sepal.Length Sepal.Width    x1    x2    y1    y2
##          (dbl)       (dbl) (int) (int) (chr) (chr)
## 1          5.1         3.5     0     1     a     b
## 2          4.9         3.0     0     1     a     b
## 3          4.7         3.2     0     1     a     b

関連書籍