potisanのプログラミングメモ

プログラミング素人です。昔の自分を育ててくれたネット情報に少しでも貢献できるよう、情報を貯めていこうと思っています。Windows環境のC++やC#がメインです。

R purrr 1.0.2 記事「purrr <-> base R」の和訳・改変

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_*() (lgldbldfr等。同様に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を述語、即ちオブジェクトが基準を満たすかをTRUEFALSEで返す関数とします。具体例は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
  • base Rでは比較的トリッキーになります。mapply()の既定値は要素数を合わせる必要があるからです(下の例ではmeansdは同じ要素数nは1要素です。)。
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のパイプスタイルは混ぜたり一致させて使えます)

パイプは長い変換と使う場合に強力です。例えば、次のコードはmtcarscylで分割して線形モデルにフィットさせ、係数を抽出して、最初の計数(傾き)を抽出します。

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)