行列を行/列ごとのリストに変換する関数の紹介とベンチマーク (base::asplit, purrr::array_tree, purrr::array_branch)

カテゴリ: r

R 3.6.0 では行列や配列を MARGIN に応じたリストに分割する asplit 関数が追加された.既に purrr パッケージが同様の機能として array_treearray_branch を実装していたので,挙動とベンチマーク結果を比較してみる.

これらの使い道は行ごとなどに lapply する時だろうか. apply が返り値をベクトル・行列・配列・リストに勝手に変換してしまうのを嫌う場合に有用かも知れない.

m <- matrix(1:100, nrow = 50)

# 返り値はリスト
lapply(asplit(m, 2), head)
#> [[1]]
#> [1] 1 2 3 4 5 6
#> 
#> [[2]]
#> [1] 51 52 53 54 55 56

# 返り値は行列
apply(m, 2, head)
#>      [,1] [,2]
#> [1,]    1   51
#> [2,]    2   52
#> [3,]    3   53
#> [4,]    4   54
#> [5,]    5   55
#> [6,]    6   56

base

asplit

asplit 関数は第一引数 x に行列ないし配列を,第二引数 MARGIN に数値のベクトルを取る. MARGIN に応じて x を分割するが, MARGIN の長さは x の次元数未満でなければならない.例えば x が3次元配列の場合は MARGIN = 1:2, 2:3, c(1, 3) は可能であるが, 1:3 はエラーを返す.

asplit(行列)

asplit は行列を行ごと (MARGIN = 1) ないし列ごと (MARGIN = 2) のリストに変換する.

(M <- matrix(c(11, 21, 12, 22, 13, 23), 2, 3))
#>      [,1] [,2] [,3]
#> [1,]   11   12   13
#> [2,]   21   22   23

# 行ごとのリストに分割
(x <- asplit(M, 1))
#> [[1]]
#> [1] 11 12 13
#> 
#> [[2]]
#> [1] 21 22 23

# 行と列で分割しようとするとエラー
asplit(M, 1:2)
#> Error in array(newx[, i], d.call, dn.call): 'dims' cannot be of length 0

asplit(配列)

配列の場合は 3 以上の MARGIN を与えることができる.また, MARGIN = 1:2 といった具合に複数の次元で切ることもできる.

# 配列の容易
(A <- array(M, c(2, 2, 2)))
#> , , 1
#> 
#>      [,1] [,2]
#> [1,]   11   12
#> [2,]   21   22
#> 
#> , , 2
#> 
#>      [,1] [,2]
#> [1,]   13   11
#> [2,]   23   21

# 3次元目で分割
asplit(A, 3)
#> [[1]]
#>      [,1] [,2]
#> [1,]   11   12
#> [2,]   21   22
#> 
#> [[2]]
#>      [,1] [,2]
#> [1,]   13   11
#> [2,]   23   21

# 1:2 次元目で分割
(y <- asplit(A, 1:2))
#>      [,1]      [,2]     
#> [1,] Numeric,2 Numeric,2
#> [2,] Numeric,2 Numeric,2

猶, y の出力が一風変わっていることに注目されたい.実は,asplit はリストの配列を返り値に持つ. y の各要素は Numeric, 2 と表示されているので一見ベクトルであるが,実体は配列である.ある変数が配列であるかどうかは is.arrayTRUE を返すことや dim が整数ベクトルを返すことで確認できる.

# y は配列なので,dimが数値を返す
dim(y)
#> [1] 2 2

# y の各要素も配列
lapply(y, dim)
#> [[1]]
#> [1] 2
#> 
#> [[2]]
#> [1] 2
#> 
#> [[3]]
#> [1] 2
#> 
#> [[4]]
#> [1] 2

先の行列の例においても,返り値は数値ベクトルのリストに見えるが,数値の一次元配列を内包したリストの一次元配列である.

# x は配列
dim(x)
#> [1] 2

# x の各要素も配列
lapply(x, dim)
#> [[1]]
#> [1] 3
#> 
#> [[2]]
#> [1] 3

purrr

array_treearray_branchmargin 引数が 長さ1の数値である場合に asplit とよく似た挙動を示す. asplit では大文字な MARGIN 引数が purrr では小文字であることに注意されたい.

この margin 引数に長さ2以上の数値を与えた場合,配列を階層的に切る.このためarray_tree はリストのリストを作る. array_brancharray_tree のフラットリスト版に相当する.

また margin 引数には,入力した配列の次元数と同じ長さのベクトルを取ることができる.何なら,1次元のベクトルを与えるとリスト化して返してくれる.

更に margin 引数は省略すると seq(dim(x)) 相当の値が与えられる.ただし x がベクトルの場合は 1L

array_tree

array_tree(行列)

行列を行ごとに切ると,ベクトルを束ねたリストを返す.

M
#>      [,1] [,2] [,3]
#> [1,]   11   12   13
#> [2,]   21   22   23

str(array_tree(M, 1))
#> List of 2
#>  $ : num [1:3] 11 12 13
#>  $ : num [1:3] 21 22 23

# asplit は配列のリストを返すため,
# 要素の長さに (1d) と記載される
str(asplit(M, 1))
#> List of 2
#>  $ : num [1:3(1d)] 11 12 13
#>  $ : num [1:3(1d)] 21 22 23
#>  - attr(*, "dim")= int 2

2行3列の行列を行→列の順で切ると長さ3のリストを2つ束ねたリストを返す.

また逆順で列→行の順で切ると長さ2のリストを3つ束ねたリストを返す.

str(array_tree(M, 1:2))
#> List of 2
#>  $ :List of 3
#>   ..$ : num 11
#>   ..$ : num 12
#>   ..$ : num 13
#>  $ :List of 3
#>   ..$ : num 21
#>   ..$ : num 22
#>   ..$ : num 23

str(array_tree(M, 2:1))
#> List of 3
#>  $ :List of 2
#>   ..$ : num 11
#>   ..$ : num 21
#>  $ :List of 2
#>   ..$ : num 12
#>   ..$ : num 22
#>  $ :List of 2
#>   ..$ : num 13
#>   ..$ : num 23

# asplit では次元数と同じ長さの MARGIN に対してエラーを返す
asplit(M, 1:2)
#> Error in array(newx[, i], d.call, dn.call): 'dims' cannot be of length 0

margin 引数を省略すると, margin = 1:2 として扱う.

identical(array_tree(M), array_tree(M, 1:2))
#> [1] TRUE

array_branch(配列)

行列での挙動をそのまま配列に拡張しただけなので,特筆することはない,

A
#> , , 1
#> 
#>      [,1] [,2]
#> [1,]   11   12
#> [2,]   21   22
#> 
#> , , 2
#> 
#>      [,1] [,2]
#> [1,]   13   11
#> [2,]   23   21

# margin の長さが1の時
str(array_tree(A, 3))
#> List of 2
#>  $ : num [1:2, 1:2] 11 21 12 22
#>  $ : num [1:2, 1:2] 13 23 11 21

# margin の長さが2以上の時
str(array_tree(A, 1:2))
#> List of 2
#>  $ :List of 2
#>   ..$ : num [1:2] 11 13
#>   ..$ : num [1:2] 12 11
#>  $ :List of 2
#>   ..$ : num [1:2] 21 23
#>   ..$ : num [1:2] 22 21

# margin を省略した時
str(array_tree(A))
#> List of 2
#>  $ :List of 2
#>   ..$ :List of 2
#>   .. ..$ : num 11
#>   .. ..$ : num 13
#>   ..$ :List of 2
#>   .. ..$ : num 12
#>   .. ..$ : num 11
#>  $ :List of 2
#>   ..$ :List of 2
#>   .. ..$ : num 21
#>   .. ..$ : num 23
#>   ..$ :List of 2
#>   .. ..$ : num 22
#>   .. ..$ : num 21

array_tree(ベクトル)

ベクトルに対しては as.list に似た挙動を示す.

str(array_tree(1:3))
#> List of 3
#>  $ : int 1
#>  $ : int 2
#>  $ : int 3

array_branch

array_branch はざっくり言うと,array_tree の返り値をフラットなリストにしたもの,

A
#> , , 1
#> 
#>      [,1] [,2]
#> [1,]   11   12
#> [2,]   21   22
#> 
#> , , 2
#> 
#>      [,1] [,2]
#> [1,]   13   11
#> [2,]   23   21

# array_branch は フラットなリストを返す
str(array_branch(A, 1:2))
#> List of 4
#>  $ : num [1:2] 11 13
#>  $ : num [1:2] 21 23
#>  $ : num [1:2] 12 11
#>  $ : num [1:2] 22 21

str(array_tree(A, 1:2))
#> List of 2
#>  $ :List of 2
#>   ..$ : num [1:2] 11 13
#>   ..$ : num [1:2] 12 11
#>  $ :List of 2
#>   ..$ : num [1:2] 21 23
#>   ..$ : num [1:2] 22 21

# `margin` の長さが1の時は `array_tree` と同じ値を返す.
identical(array_branch(A, 3), array_tree(A, 3))
#> [1] TRUE

ベンチマーク

asplit, array_tree, array_branchmargin の長さが2以上の場合は異なる返り値を返す.どれを使うかは用途次第といったところ.一方で margin の長さが1の場合は似た返り値を持つので asplitarray_tree の性能を比較してみよう.

library(bench)
library(ggplot2)

set.seed(1)
n <- 1e3
x <- matrix(rnorm(n * n), n, n)

result <- mark(
  asplit = asplit(x, 2L),
  array_tree = array_tree(x, 2L),
  check = FALSE,
  min_iterations = 100L
)

autoplot(result, 'beeswarm')

ガベージコレクション次第ではあるが,概ね array_tree が強いようだ.