R言語のdo.callとReduceでlist型の扱いが簡単になる
はじめに
R言語を使うとき、繰り返し処理はみなさんはどのように書いていますか?
筆者はR言語で繰り返し処理を lapply や Map 関数を利用することが多いです。
そもそもですが、筆者が扱うデータは配列やデータフレームよりも list で扱うことに適しているため、繰り返し処理は必然として lapply (Map) を利用することになります。
例えば、気象データの (netCDF) のように、属性情報 (気温、湿度など) は異なるが 位置情報 (long, lat) や時間情報が同じである構造化データの値を取り出す場合には list 型を利用する方法が適しているように感じます。
また、以前のバージョンのRでは for だと計算速度が出なかった、という話もありますが、 for だと、値の受け渡しを毎度書く必要があるため、正直面倒です。
今回は lapply (Map) の結果をまとめて data.frame にしたり、集合の計算をまとめて行う方法を記録しておきます。
実行環境
Linuxで実行していますが、Rの初期導入関数を利用するのでどの環境でも結果に変わりはありません。
sessionInfo()
R version 4.2.2 Patched (2022-11-10 r83330)
Platform: x86_64-pc-linux-gnu (64-bit)
Running under: Debian GNU/Linux 12 (bookworm)
Matrix products: default
BLAS: /usr/lib/x86_64-linux-gnu/blas/libblas.so.3.11.0
LAPACK: /usr/lib/x86_64-linux-gnu/lapack/liblapack.so.3.11.0
locale:
[1] LC_CTYPE=ja_JP.UTF-8 LC_NUMERIC=C LC_TIME=ja_JP.UTF-8 LC_COLLATE=ja_JP.UTF-8 LC_MONETARY=ja_JP.UTF-8
[6] LC_MESSAGES=ja_JP.UTF-8 LC_PAPER=ja_JP.UTF-8 LC_NAME=C LC_ADDRESS=C LC_TELEPHONE=C
[11] LC_MEASUREMENT=ja_JP.UTF-8 LC_IDENTIFICATION=C
attached base packages:
[1] stats graphics grDevices utils datasets methods base
loaded via a namespace (and not attached):
[1] compiler_4.2.2 tools_4.2.2
サンプルデータの作成
まずはテスト用のデータを作成します。
id 列がキー列で、ランダムに選択されるようになっています。
これを data.frame として各listの要素に格納します。
d <- lapply(1:5, function(x) {
data.frame(id = sample(1:10, size = 5),
value = rnorm(5)
)
})
str(d)
List of 5
$ :'data.frame': 5 obs. of 2 variables:
..$ id : int [1:5] 5 6 8 1 10
..$ value: num [1:5] 1.296 0.417 1.594 0.453 -1.607
$ :'data.frame': 5 obs. of 2 variables:
..$ id : int [1:5] 6 2 9 7 4
..$ value: num [1:5] 0.487 -0.283 -1.227 -0.304 -0.825
$ :'data.frame': 5 obs. of 2 variables:
..$ id : int [1:5] 10 9 2 3 6
..$ value: num [1:5] -1.3959 0.0378 0.4035 1.2866 0.0731
$ :'data.frame': 5 obs. of 2 variables:
..$ id : int [1:5] 2 4 8 6 1
..$ value: num [1:5] -0.429 -0.548 -0.569 0.899 -0.687
$ :'data.frame': 5 obs. of 2 variables:
..$ id : int [1:5] 6 2 8 10 1
..$ value: num [1:5] 1.207 -0.73 -0.587 0.897 0.14
do.call関数について
筆者は、 do.call をlist型のデータをベクトル (c)、データフレーム化 (data.frame)、行方向の結合(rbind)、列方向の結合(cbind) など変換する作業時に利用しています。
例えば、列方向の結合(cbind) は以下の例のとおりです。
複数のデータフレームの列が結合されています。
当然ながら、データの長さ (nrow) が揃っていないと成功しませんが、揃ってさえいればこのように一括で結合できます。
この操作感は cbind を単体で利用する場合と全く同じです。
do.call(cbind, d)
id value id value id value id value id value
1 5 1.2964818 6 0.4867769 10 -1.39592327 2 -0.4290121 6 1.2068753
2 6 0.4167234 2 -0.2830809 9 0.03782643 4 -0.5477464 2 -0.7298629
3 8 1.5944528 9 -1.2270937 2 0.40350864 8 -0.5687036 8 -0.5872229
4 1 0.4528981 7 -0.3041781 3 1.28658397 6 0.8988091 10 0.8972524
5 10 -1.6067214 4 -0.8245866 6 0.07312669 1 -0.6867530 1 0.1397295
続いて、行方向に結合する場合は、 rbind を使うように、列名が同じであることが条件です。
do.call(rbind, d) |> str()
'data.frame': 25 obs. of 2 variables:
$ id : int 5 6 8 1 10 6 2 9 7 4 ...
$ value: num 1.296 0.417 1.594 0.453 -1.607 ...
関数型プログラミングを実行する場合、 x <- do.call(rbind, lapply(y, function(m) {...}) のように複数の要素 y を lapply で並列計算し、その結果返る data.frame を rbind で行方向に結合して一つのデータセットにしてしまうことができます。
見た目は複雑になりますが、筆者はこの方法をよく利用しています。
do.callでその他関数実行方法
do.call では、第一引数に関数を指定しますが、その書き方は、 do.call(FUN, list(...))) -> FUN(...) と同じ意味になります。
つまり、ある関数 (FUN) の引数を名前付きlist(list(...))として指定することができるということです。
sum(1:5, 3:20, NA, na.rm = TRUE)
do.call(sum, list(1:5, 3:20, NA, na.rm = TRUE))
[1] 222
[1] 222
例えば、新たな関数を定義する際、関数の引数値の指定方法には、名前付き引数を設定する、三点ドット (...) として渡す、メタプログラミングで expression として渡す、文字列で渡してパース (parse(text = ...)) するかの方法があります。
しかしながら、do.callを使うと、ある処理だけに利用する設定 (引数) を制限なく指定することができるため、とても便利になります。
#' @param x データ
#' @param fun 関数
#' @param opt 関数の引数値
testfun <- function(..., sum_opt = list(), mean_opt = list()) {
do.call(sum, c(list(...), sum_opt)) |> print()
do.call(mean, c(list(c(...)), mean_opt)) |> print()
}
message("==== test 1 ====")
testfun(1:5, 3:20, NA, sum_opt = list(na.rm = TRUE), mean_opt = list(na.rm = FALSE))
message("==== test 2 ====")
testfun(1:5, 3:20, NA, sum_opt = list(na.rm = TRUE), mean_opt = list(na.rm = TRUE))
==== test 1 ====
[1] 222
[1] NA
==== test 2 ====
[1] 222
[1] 9.652174
Reduce関数について
Reduce 関数は do.call と同じように、第一引数に関数を指定し、第二引数に指定した list データに適用する挙動をしますが、その対象範囲が異なります。
和集合
和集合を考える場合、 do.call では失敗するが Reduce では成功します。
do.call(union, lapply(d, function(x) x$id))
(function (x, y) でエラー:
使われていない引数 (c(10, 9, 2, 3, 6), c(2, 4, 8, 6, 1), c(6, 2, 8, 10, 1))
Reduce(union, lapply(d, function(x) x$id))
[1] 5 6 8 1 10 2 9 7 4 3
結合 (merge)
Reduce(function(x, y) merge(x, y, by = "id", all = TRUE), d)
id value.x value.y value.x value.y value
1 1 NA NA 0.3462431 1.0542735 0.5674269
2 2 -1.4828797 0.2642513 -0.2009429 -1.4347203 NA
3 3 NA -0.9356619 NA -1.0988169 -1.5468162
4 4 NA NA NA NA -1.2303792
5 5 0.9144786 NA NA -1.4594198 NA
6 6 NA 0.5397016 NA NA 0.4838808
7 7 0.1393513 -0.1823246 -0.4386959 NA 3.1292710
8 8 NA NA -1.3072114 NA NA
9 9 -0.9732506 NA NA 0.1738573 NA
10 10 -0.2555061 0.8463972 -0.2037884 NA NA
警告メッセージ:
1: merge.data.frame(x, y, by = "id", all = TRUE) で:
column names ‘value.x’, ‘value.y’ are duplicated in the result
2: merge.data.frame(x, y, by = "id", all = TRUE) で:
column names ‘value.x’, ‘value.y’ are duplicated in the result
do.callとReduceの比較
では、 do.call で試した cbind を Reduce で試すとどうでしょうか?
Reduce(cbind, d)
id value id value id value id value id value
1 5 1.2964818 6 0.4867769 10 -1.39592327 2 -0.4290121 6 1.2068753
2 6 0.4167234 2 -0.2830809 9 0.03782643 4 -0.5477464 2 -0.7298629
3 8 1.5944528 9 -1.2270937 2 0.40350864 8 -0.5687036 8 -0.5872229
4 1 0.4528981 7 -0.3041781 3 1.28658397 6 0.8988091 10 0.8972524
5 10 -1.6067214 4 -0.8245866 6 0.07312669 1 -0.6867530 1 0.1397295
他にも、 c, data.frame, rbind, list, ... 等の関数が利用できました。
この違いについてAI (gemma3n_e2b) に質問してみると、 do.call は 引数をまとめて関数に渡す ために利用し、 Reduce は 繰り返し処理を簡単にする ために利用するそうです。知りませんでした。
つまり、 x <- do.call(rbind, lapply(y, function(m) {...}) ではなく x <- Reduce(rbind, lapply(y, function(m) {...}) が正しい使い方のようです。
おわりに
do.call と Reduce について見てきましたが、正直どちらも同じような挙動であるため筆者は do.call を多用してきました。
しかしながら、 do.call は do.callでその他関数実行方法 の使い方が本質であり、 list型のデータをまとめて処理する場合には Reduce を使うべきということがわかりました。
LISP のように再帰関数を使ってデータの処理をしているところがありましたが、 Reduce では簡単にできそうです。
長年使っていても知らないことがある、改めて学習し直す必要性を感じました。
Discussion