Rのパッケージpurrr 1.0.2の添付文書和訳です。purrrはHadley Wickham氏の作成した関数型プログラミングの素晴らしいパッケージで、base Rよりも一貫した方法、手軽な方法でデータを操作できます。
この文書はpurrr 1.0.2のbase.Rmdの和訳及び改変です。
添付文書のライセンスはMITライセンスですが、原文の権利はHadley Wickham氏と貢献者方にあります。翻訳文の使用は自己責任です。
purrrとbase Rの対応
はじめに
このビネット(文章)ではマップ関数群と関連する関数を中心にpurrrとbase Rを比較します。base Rに慣れた人にはpurrrの働きを理解する助けとなり、purrrユーザーにはbase Rにおける記述を示します。それでは主な違いの大まかな概要からはじめて、大まかな翻訳ガイド、そしていくつかの具体例をお示しします。
library(purrr) library(tibble)
重要な違い
purrrのmap関数群とbaseのapply関数群には2つの重要な違いがあります。purrrの関数は名前がより一貫していて、入出力版の空間をより完全に探索することです。
purrrの関数は不注意による引数の一致を防ぐために一貫して
.
接頭辞を使用します。baseの関数は大文字(例:lapply(X, FUN, ...)
)や匿名関数(例えばMap()
)等の様々なテクニックを用います。全てのmap関数は型が安定しています。入力のわずかな情報から出力の型を推測できます。一方、baseの
sapply()
とmapply()
は自動で出力を単純化するため、出力型の推測は困難です。全てのmap関数は入力の最初がデータ、次に関数、以降に付加的な定数が続きます。baseのapply関数は多くが同様のパターンですが、
mapply()
は最初の引数が関数で、Map()
は付加的な定数を渡せません。purrrの関数は入出力版の全ての組み合わせと2つの共通引数で特定される版を提供します。
直訳
以下のセクションではbase Rとpurrrの高水準な翻訳を提供します。詳細は関数ドキュメントを参照してください。
map
関数
ここではx
でベクトル、f
で関数を表します。
出力 | 入力 | base R | purrr |
---|---|---|---|
リスト | 1ベクトル | lapply() |
map() |
リスト | 2ベクトル | mapply() 、Map() |
map2() |
リスト | >2ベクトル | mapply() 、Map() |
pmap() |
指定した型のアトミックベクトル | 1ベクトル | vapply() |
map_lgl() (logical)、map_int() (integer)、map_dbl() (double)、map_chr() (character)、map_raw() (raw) |
指定した型のアトミックベクトル | 2ベクトル | mapply() 、Map() とis.*() による型チェック |
map2_lgl() (logical)、map2_int() (integer)、map2_dbl() (double)、map2_chr() (character)、map2_raw() (raw) |
指定した型のアトミックベクトル | >2ベクトル | mapply() 、Map() とis.*() による型チェック |
pmap_lgl() (logical)、pmap_int() (integer)、pmap_dbl() (double)、pmap_chr() (character)、pmap_raw() (raw) |
副作用のみ | 1ベクトル | ループ構文 | walk() |
副作用のみ | 2ベクトル | ループ構文 | walk2() |
副作用のみ | >2ベクトル | ループ構文 | pwalk() |
データフレーム(rbind 出力) |
1ベクトル | lapply() 、rbind() |
map_dfr() |
データフレーム(rbind 出力) |
2ベクトル | mapply() またはMap() とrbind() |
map2_dfr() |
データフレーム(rbind 出力) |
>2ベクトル | mapply() またはMap() と[rbind() |
pmap_dfr() |
データフレーム(cbind 出力) |
1ベクトル | lapply() とcbind() |
map_dfc() |
データフレーム(cbind 出力) |
2ベクトル | mapply() またはMap() とcbind() |
map2_dfc() |
データフレーム(cbind 出力) |
>2ベクトル | mapply() またはMap() とcbind() |
pmap_dfc() |
任意 | ベクトルと名前 | l/s/vapply(X, function(x) f(x, names(x))) またはmapply/Map(f, x, names(x)) |
imap() 、imap_*() (lgl 、dbl 、dfr 等。同様にmap() 、map2() 、pmap() ) |
任意 | ベクトルの選択要素 | l/s/vapply(X[index], FUN, ...) |
map_if() 、map_at() |
リスト | リストのリストへの再帰的適用 | rapply() |
map_depth() |
リスト | リストのみ | lapply() |
lmap() 、lmap_at() 、lmap_if() |
抽出の短絡表記
map関数はリスト要素の抽出によく使われるため、purrrは[[
の様々な使用方法に対して使いやすい短絡関数を提供しています。
入力 | base R | purrr |
---|---|---|
名前による抽出 | lapply(x, `[[`, "a") |
map(x, "a") |
位置による抽出 | lapply(x, `[[`, 3) |
map(x, 3) |
深い抽出 | lapply(x, \(y) y[[1]][["x"]][[3]]) |
map(x, list(1, "x", 3)) |
既定値を用いた抽出 | lapply(x, function(y) tryCatch(y[[3]], error = function(e) NA)) |
map(x, 3, .default = NA) |
述語
ここではp
を述語、即ちオブジェクトが基準を満たすかをTRUE
かFALSE
で返す関数とします。具体例はis.character()
です。
説明 | base R | purrr |
---|---|---|
一致要素の検索 | Find(p, x) |
detect(x, p) |
一致要素の位置の検索 | Position(p, x) |
detect_index(x, p) |
ベクトルの全要素が基準を満たす? | all(sapply(x, p)) |
every(x, p) |
ベクトルのある要素が基準を満たす? | any(sapply(x, p)) |
some(x, p) |
リストはオブジェクトを含む? | any(sapply(x, identical, obj)) |
has_element(x, obj) |
基準を満たす要素を選ぶ。 | x[sapply(x, p)] |
keep(x, p) |
基準を満たす要素を除外する。 | x[!sapply(x, p)] |
discard(x, p) |
基準関数を反転する。 | function(x) !p(x) |
negate(p) |
その他のベクトル変換
説明 | base R | purrr |
---|---|---|
ベクトルをリダクションした中間結果を集積する。 | Reduce(f, x, accumulate = TRUE) |
accumulate(x, f) |
2つのリストを再帰的に合体する。 | c(X, Y) 、ただし複雑な再帰的結合が必要 |
list_merge() 、list_modify() |
リストに2引数を取る関数を再帰適用して単一の値に要約(リダクション)する。 | Reduce(f, x) |
reduce(x, f) |
具体例
様々な入力
単一の値
次のコードでは平均値の異なる正規分布から5標本のリストを作成します。
means <- 1:4
サンプルの生成がちょっと違います。
base Rでは
lapply()
を使います。set.seed(2020) samples <- lapply(means, rnorm, n = 5, sd = 1) str(samples) #> List of 4 #> $ : num [1:5] 1.377 1.302 -0.098 -0.13 -1.797 #> $ : num [1:5] 2.72 2.94 1.77 3.76 2.12 #> $ : num [1:5] 2.15 3.91 4.2 2.63 2.88 #> $ : num [1:5] 5.8 5.704 0.961 1.711 4.058
purrrでは
map()
を使います。set.seed(2020) samples <- map(means, rnorm, n = 5, sd = 1) str(samples) #> List of 4 #> $ : num [1:5] 1.377 1.302 -0.098 -0.13 -1.797 #> $ : num [1:5] 2.72 2.94 1.77 3.76 2.12 #> $ : num [1:5] 2.15 3.91 4.2 2.63 2.88 #> $ : num [1:5] 5.8 5.704 0.961 1.711 4.058
2つの入力
標準偏差も異なる値にして少し複雑にしてみます。
means <- 1:4 sds <- 1:4
set.seed(2020) samples <- mapply( rnorm, mean = means, sd = sds, MoreArgs = list(n = 5), SIMPLIFY = FALSE ) str(samples) #> List of 4 #> $ : num [1:5] 1.377 1.302 -0.098 -0.13 -1.797 #> $ : num [1:5] 3.44 3.88 1.54 5.52 2.23 #> $ : num [1:5] 0.441 5.728 6.589 1.885 2.63 #> $ : num [1:5] 11.2 10.82 -8.16 -5.16 4.23
Map()
も使えますが、簡単にはなりません。任意の定数引数を与えられないため、匿名関数を使う必要があります。
samples <- Map(function(...) rnorm(..., n = 5), mean = means, sd = sds)
R 4.1以降では匿名関数の短縮形も使えます。
samples <- Map(\(...) rnorm(..., n = 5), mean = means, sd = sds)
prrrでは2ベクトルの処理という一般的な状況に対して
map2()
関数群を使えます。set.seed(2020) samples <- map2(means, sds, rnorm, n = 5) str(samples) #> List of 4 #> $ : num [1:5] 1.377 1.302 -0.098 -0.13 -1.797 #> $ : num [1:5] 3.44 3.88 1.54 5.52 2.23 #> $ : num [1:5] 0.441 5.728 6.589 1.885 2.63 #> $ : num [1:5] 11.2 10.82 -8.16 -5.16 4.23
任意個の入力
標本数も異なる値のもっと複雑な例にも挑戦してみましょう。
ns <- 4:1
base Rの
Map()
は先ほどより素直に書けます。定数がないためです。set.seed(2020) samples <- Map(rnorm, mean = means, sd = sds, n = ns) str(samples) #> List of 4 #> $ : num [1:4] 1.377 1.302 -0.098 -0.13 #> $ : num [1:3] -3.59 3.44 3.88 #> $ : num [1:2] 2.31 8.28 #> $ : num 4.47
purrrでは
map2()
をpmap()
に書き換える必要があります。pmap()
は任意個の引数のリストを受け取ります。set.seed(2020) samples <- pmap(list(mean = means, sd = sds, n = ns), rnorm) str(samples) #> List of 4 #> $ : num [1:4] 1.377 1.302 -0.098 -0.13 #> $ : num [1:3] -3.59 3.44 3.88 #> $ : num [1:2] 2.31 8.28 #> $ : num 4.47
出力
標本の平均を計算する場合を考えてみましょう。平均は単一の値なので、出力はリストよりも数値ベクトルであって欲しいです。
base Rには
vapply()
とsapply()
の2つの選択肢があります。vapply()
は(比較的冗長な方法で)出力型の指定が必要ですが、指定すれば常に数値ベクトルを返せます。sapply()
は簡潔ですが、空リストを与えると数値ベクトルではなくリストを返します。# 型が安定 medians <- vapply(samples, median, FUN.VALUE = numeric(1L)) medians #> [1] 0.6017626 3.4411470 5.2946304 4.4694671 # 型が不安定(空リストの場合がある) medians <- sapply(samples, median)
purrrでは
map_dbl()
を使えるので少しだけコンパクトになります。medians <- map_dbl(samples, median) medians #> [1] 0.6017626 3.4411470 5.2946304 4.4694671
副作用だけ欲しい場合、例えばプロットやファイル出力だけしたい場合はどうでしょうか?
- base Rではループや
lapply
と結果の隠蔽を使います。
# for loop
for (s in samples) {
hist(s, xlab = "value", main = "")
}
# lapply
invisible(lapply(samples, function(s) {
hist(s, xlab = "value", main = "")
}))
purrrでは
walk()
を使います。walk(samples, ~ hist(.x, xlab = "value", main = ""))
パイプ
複数ステップをmagrittrパイプで連結できます。
set.seed(2020) means %>% map(rnorm, n = 5, sd = 1) %>% map_dbl(median) #> [1] -0.09802317 2.72057350 2.87673977 4.05830349
base Rのパイプも使えます。
set.seed(2020) means |> lapply(rnorm, n = 5, sd = 1) |> sapply(median) #> [1] -0.09802317 2.72057350 2.87673977 4.05830349
(もちろん、base Rとpurrrのパイプスタイルは混ぜたり一致させて使えます)
パイプは長い変換と使う場合に強力です。例えば、次のコードはmtcars
をcyl
で分割して線形モデルにフィットさせ、係数を抽出して、最初の計数(傾き)を抽出します。
mtcars %>% split(mtcars$cyl) %>% map(\(df) lm(mpg ~ wt, data = df)) %>% map(coef) %>% map_dbl(1) #> 4 6 8 #> 39.57120 28.40884 23.86803
purrrの開発者:Hadley Wickham, Lionel Henry, Posit (RStudio)