Rで多重forを回避するTips

多重forが発生するケース

例えば3個のパラメータについて、いくつかのパターンの総組合せを試したいということ があるかと思います。

f <- function(x, y, z) {
  # x, y, zを使って何かするコード
  sprintf("x: %d, y: %d, z: %d", x, y, z)
}

x_list <- c(1, 2)
y_list <- c(100, 50, 20)
z_list <- c(1, 0)

results<- list()
for (x in x_list) {
  for (y in y_list) {
    for (z in z_list) {
      results <- c(results, f(x, y, z))
    }
  }
}
head(results)
##> [[1]]
##> [1] "x: 1, y: 100, z: 1"
##> 
##> [[2]]
##> [1] "x: 1, y: 100, z: 0"
##> 
##> [[3]]
##> [1] "x: 1, y: 50, z: 1"
##> 
##> [[4]]
##> [1] "x: 1, y: 50, z: 0"
##> 
##> [[5]]
##> [1] "x: 1, y: 20, z: 1"
##> 
##> [[6]]
##> [1] "x: 1, y: 20, z: 0"

このコードの嫌なところは例えば関数fに新しいパラメータwを追加してw_list <- c("a", "b", "c")というベクトルをパラメータの組合せに加えるということをしようとしたとき、変更しなければならない箇所が多いことが挙げられます。

また、見栄えも悪い気がします。

f <- function(x, y, z, w) {
  # x, y, z, wを使って何かするコード
  sprintf("x: %d, y: %d, z: %d, w: %s", x, y, z, w)
}

x_list <- c(1, 2)
y_list <- c(100, 50, 20)
z_list <- c(1, 0)
w_list <- c("a", "b", "c")

results<- list()
for (x in x_list) {
  for (y in y_list) {
    for (z in z_list) {
      for (w in w_list) {
        results <- c(results, f(x, y, z, w))
      }
    }
  }
}
head(results)
##> [[1]]
##> [1] "x: 1, y: 100, z: 1, w: a"
##> 
##> [[2]]
##> [1] "x: 1, y: 100, z: 1, w: b"
##> 
##> [[3]]
##> [1] "x: 1, y: 100, z: 1, w: c"
##> 
##> [[4]]
##> [1] "x: 1, y: 100, z: 0, w: a"
##> 
##> [[5]]
##> [1] "x: 1, y: 100, z: 0, w: b"
##> 
##> [[6]]
##> [1] "x: 1, y: 100, z: 0, w: c"

そんなときに私ならどうするかを紹介しようと思います。

あくまで最近の私がどうしているかでこれが最善ではありません。

expand.gridとpurrrを使う

以下にコードを示します。もし関数f の引数が増えたらその対応はwrapper_fの内部とparam_listの要素の追加になります。新しいコードブロックが増えるようなことはないので多少はシンプルかなと思っています。

欠点としては全組合せがいったんデータフレームに展開されるので組合せ数が爆発するとメモリに乗らなくなるかもしれないという危険があるので結局のところ一長一短ではあります。

f <- function(x, y, z, w) {
  # x, y, z, wを使って何かするコード
  sprintf("x: %d, y: %d, z: %d, w: %s", x, y, z, w)
}

wrapper_f <- function(p) {
  f(p$x, p$y, p$z, p$w)  #### fの引数の数が変わったら変更すべき場所(1) #####
}

param_list <- list(
  x = c(1, 2),
  y = c(100, 50, 20),
  z = c(1, 0),
  w = c("a", "b", "c") #### fの引数の数が変わったら変更すべき場所(2) #####
)
param_list |>
  expand.grid(stringsAsFactors = FALSE) |> # 全ての組合せを網羅するデータフレーム
  purrr::transpose() |> # データフレームを行ごとにイテレートする(直感的でないのがマイナスポイント)
  purrr::map(wrapper_f) |>
  head()
##> [[1]]
##> [1] "x: 1, y: 100, z: 1, w: a"
##> 
##> [[2]]
##> [1] "x: 2, y: 100, z: 1, w: a"
##> 
##> [[3]]
##> [1] "x: 1, y: 50, z: 1, w: a"
##> 
##> [[4]]
##> [1] "x: 2, y: 50, z: 1, w: a"
##> 
##> [[5]]
##> [1] "x: 1, y: 20, z: 1, w: a"
##> 
##> [[6]]
##> [1] "x: 2, y: 20, z: 1, w: a"