Rのこと。

記事は引っ越し作業中。2023年中までに引っ越しを完了させてブログは削除予定

Advanced R: Names and values

はじめに

Advanced R 2nd EditionのPaperBook版が届いたので、Rについてのおさらい備忘録。また、書籍を読んでいるにも関わらず誤った解釈や用語の誤用もあるかもしれませんので、参考にする際は自己責任でお願いします。

この記事のライセンスはAdvanced R 2nd Editionと同様で下記の通りです。

Names and values

Rの名前と値の関係について書いてある章。何気なくx <- 1とかしているけれども、どういった仕組みが裏側に用意されているのかを図解を交えて解説してある。

Binding

名前と値を関連付けることをバインディングという。Advanced R 2nd Editionの例をお借りする。これを日本語的に解釈すると、c(1, 2, 3)というベクトルのオブジェクトを名前xがバインドしている、ということになる。

x <- c(1, 2, 3)

f:id:AZUMINO:20190727151923p:plain

xを使ってyに、yを使ってzに、zを使ってxxで…としても、Rはc(1, 2, 3)というベクトルのオブジェクトを名前xyzxxの各々がバインドすることになる。そのため、オブジェクトの識別子にアクセスしても0x7f886b0ad6d0のままとなる。

x <- 1:3
obj_addr(x)
[1] "0x7f886b0ad6d0"

y <- x
obj_addr(y)
[1] "0x7f886b0ad6d0"

z <- y
obj_addr(z)
[1] "0x7f886b0ad6d0"

xx <- z
obj_addr(xx)
[1] "0x7f886b0ad6d0"

同じc(1, 2, 3)というベクトルのオブジェクトを作って、yバインディングしても、先ほどのオブジェクトとは異なることがわかる。

y <- 1:3
obj_addr(y)
[1] "0x7f8873008e48"

値渡し、参照渡しみたいなものがあるが、下記のようにオブジェクトを入れると同じアドレスをオブジェクトたちは参照する。

x <- c(1, 2, 3)
y <- x
obj_addr(x);obj_addr(y)
[1] "0x7f8aefc93468"
[1] "0x7f8aefc93468"

この状態で、xに新たな値を付け替えると、その時点でコピーが発生し、違うメモリが利用される。Copy-on-modifyという仕組み。これは後述。

obj_addr(x);obj_addr(y)
[1] "0x7f8aee31b218"
[1] "0x7f8aefc93468"

identical(obj_addr(x), obj_addr(y))
[1] FALSE

ちなみに関数のメモリも調べることができる。

lobstr::obj_addr(mean)
[1] "0x7f8ae9caca70"

lobstr::obj_addr(base::mean)
[1] "0x7f8ae9caca70"

lobstr::obj_addr(match.fun("mean"))
[1] "0x7f8ae9caca70"

補足として、match.funはうっかり関数を上書きしても、正しく呼び出すことができる関数。

is.function(outer)
[1] TRUE

outer <- 1:5
is.function(outer)
[1] FALSE

is.function(match.fun(outer))
[1] TRUE

wrap_func <- function(search_lib, ...){
  func <- match.fun(search_lib)
  res <- func(...)
  return(res)
}

wrap_func(rnorm, n = 10, mean = 0, sd = 1)
[1] -0.3497536 -0.1834132  1.2715276 -1.6678541 -0.6154758 -1.0373960
[7]  0.0120612  0.1599219  0.4485161 -1.5320582

Non-syntactic names

Non-syntactic namesという非構文名のことについて。基本的に予約語_や数字などを名前の先頭に持ってきて、バインディングすることはできない。

TRUE <- 1
TRUE <- 1 でエラー:  代入の左辺が不正 (do_set) です 

_x <- 1
 エラー:  想定外の入力です  in "_"

1x <- 1
 エラー:  想定外のシンボルです  in "1x"

ただし、バッククォートで囲むことでバインディングできるが、非推奨。

`TRUE` <- 1
`_x` <- 1
`1x` <- 1

TRUE
[1] TRUE

`TRUE`
[1] 1

`_x` 
[1] 1

`1x` 
[1] 1

Copy-on-modify

Copy-on-modifyというオブジェクトのコピーについて。オブジェクトはいつコピーされるのかを解説している。さきほど、オブジェクトをxバインディングした際に異なる名前でもバインディングしたが、同じオブジェクトを参照していた。下記のような場合は、オブジェクトの識別子が0x7f88724d63480x7f8872563b18と異なっている。

これがCopy-on-modifyという仕組みで、元のオブジェクトに変更が生じる際に、元のオブジェクトをコピーして新たなオブジェクトをリバインディングすることになる。つまり、コピーしてモディファイするので、Copy-on-modifyなのである。

x <- c(1, 2, 3)
obj_addr(x)
[1] "0x7f88724d6348"

x[[3]] <- 4
obj_addr(x)
[1] "0x7f8872563b18"

tracemem()を使うことで、いつコピーが発生したのかを確認できる。xのメモリアドレスは0x7f8873be93b8だったが、値の一部を変更したことで、Copy-on-modifyが発生し、メモリアドレスが0x7f8873be93b8 -> 0x7f8873c5e918に変更されている。

x <- c(1, 2, 3)

cat(tracemem(x))
<0x7f8873be93b8>

x[[3]] <- 4
tracemem[0x7f8873be93b8 -> 0x7f8873c5e918]: 

cat(tracemem(x))
<0x7f8873c5e918>

このCopy-on-modifyはオブジェクトだけではなく関数にも同様に適用される。

x <- 1:3
f <- function(a) {
  print(a)
}
cat(tracemem(f(x)))
[1] 1 2 3
<0x7f886fd1cc38>
  
g <- f(x)
[1] 1 2 3

この仕組は少しややこしいので、Advanced R 2nd Editionのイメージ図をお借りする。

f()は右側の黄色いオブジェクトで、形式的に引数aを持つ。f()を実行すると、形式的に引数axバインディングしているオブジェクトをバインディングする(このときもバインディングという表現が正しいのか不明)。このとき、関数が実行されると、実行環境(灰色の箱)でf()バインディングすることになる。zxバインディングしているオブジェクトをバインディングするだけなので、Copy-on-modifyは発生しない。

f:id:AZUMINO:20190727155126p:plain

Lists

リストのバインディングについて。リストのバインディングは先ほどよりもいささかややこしい。例えば書籍に載っているこんな例があるとする。

l1 <- list(1, 2, 3)
> l1
[[1]]
[1] 1

[[2]]
[1] 2

[[3]]
[1] 3

Advanced R 2nd Editionのイメージ図をお借りする。図からわかるように、リストは、値を格納するのではなく、値への参照を格納する

f:id:AZUMINO:20190727160456p:plain

Copy-on-modifyについては、同じ参照のままであれば変わらない。参照が変わるような修正を行った場合、Copy-on-modifyが発生し、0x7f8872ce2468 -> 0x7f887269ed08へとメモリアドレスが変更される。

l1 <- list(1, 2, 3)
cat(tracemem(l1))
<0x7f8872ce2468>

l2 <- l1

l2[[3]] <- 4
tracemem[0x7f8872ce2468 -> 0x7f887269ed08]: 

f:id:AZUMINO:20190727160857p:plain

さらに詳しく参照関係を見たい場合はlobstr::ref()を使用する。コメントアウトの記号でメモリアドレスを対応させている。

ref(l1, l2)
█ [1:0x7f8872ce2468] <list> 
├─[2:0x7f88716f38e8] <dbl>  # ▲
├─[3:0x7f88716f38b0] <dbl>  # ■
└─[4:0x7f88716f3878] <dbl>  # ●
 
█ [5:0x7f887269ed08] <list> 
├─[2:0x7f88716f38e8] # ▲
├─[3:0x7f88716f38b0] # ■
└─[6:0x7f88716624e8] <dbl> # ×

リストの集合はデータフレームなので、データフレームも参照を格納する。

d1 <- data.frame(x = c(1, 5, 6), y = c(2, 4, 3))

f:id:AZUMINO:20190727161345p:plain

このとき、列を変更すると、Copy-on-modifyが発生し、0x7f8874884058への参照が0x7f8874a02d98へと変わる。

d2 <- d1
d2[, 2] <- d2[, 2] * 2

ref(d1, d2)
█ [1:0x7f8874229208] <df[,2]> 
├─x = [2:0x7f88748840a8] <dbl> 
└─y = [3:0x7f8874884058] <dbl> 
 
█ [4:0x7f88744bb048] <df[,2]> 
├─x = [2:0x7f88748840a8] 
└─y = [5:0x7f8874a02d98] <dbl> 

f:id:AZUMINO:20190727161402p:plain

行を変更すると、すべての列が変更されるので、すべての列でCopy-on-modifyが発生することになる。

d3 <- d1
d3[1, ] <- d3[1, ] * 3

ref(d1, d3)
█ [1:0x7f8874229208] <df[,2]> 
├─x = [2:0x7f88748840a8] <dbl> 
└─y = [3:0x7f8874884058] <dbl> 
 
█ [4:0x7f88749d1748] <df[,2]> 
├─x = [5:0x7f8874c20898] <dbl> 
└─y = [6:0x7f8874c20848] <dbl> 

f:id:AZUMINO:20190727161821p:plain

文字ベクトル

文字ベクトルはグローバルストリングプールを使用する。なので、xは文字ベクトルを下記のようにバインディングしている。そして、各値はグローバルストリングプールを参照します。 f:id:AZUMINO:20190727164756p:plain

x <- c("a", "a", "abc", "d")

ref(x, character = TRUE)
█ [1:0x7f8871a41fa8] <chr> 
├─[2:0x7f886b0a2320] <string: "a"> 
├─[2:0x7f886b0a2320] 
├─[3:0x7f8870e724c8] <string: "abc"> 
└─[4:0x7f886ca8a688] <string: "d"> 

for-loopの改良

Rはよく計算速度が遅いなどと言われる。ALTREPプロジェクトで大きく改善したらしいとも聞く。とにかく遅くなる要因の1つがCopy-on-modifyを発生させるコードが要因。下記のような計算をすると、ループする度に3回のCopy-on-modifyが発生する。

x <- data.frame(matrix(runif(5 * 10), ncol = 5))
medians <- vapply(x, median, numeric(1))
cat(tracemem(x), "\n")
<0x7f8871721ad8> 

for (i in seq_along(medians)) {
  x[[i]] <- x[[i]] - medians[[i]]
}
# Loop-1
tracemem[0x7f8871721ad8 -> 0x7f886d64e758]: 
tracemem[0x7f886d64e758 -> 0x7f886d64e838]: [[<-.data.frame [[<- 
tracemem[0x7f886d64e838 -> 0x7f886d64e8a8]: [[<-.data.frame [[<- 
# Loop-2
tracemem[0x7f886d64e8a8 -> 0x7f886d64ead8]: 
tracemem[0x7f886d64ead8 -> 0x7f886d64eb48]: [[<-.data.frame [[<- 
tracemem[0x7f886d64eb48 -> 0x7f886d64ebb8]: [[<-.data.frame [[<- 
# Loop-3
tracemem[0x7f886d64ebb8 -> 0x7f886d64ec28]: 
tracemem[0x7f886d64ec28 -> 0x7f886d64f248]: [[<-.data.frame [[<- 
tracemem[0x7f886d64f248 -> 0x7f886d6836d8]: [[<-.data.frame [[<- 
# Loop-4
tracemem[0x7f886d6836d8 -> 0x7f8870d7ef78]: 
tracemem[0x7f8870d7ef78 -> 0x7f8870d7efe8]: [[<-.data.frame [[<- 
tracemem[0x7f8870d7efe8 -> 0x7f8870d7f058]: [[<-.data.frame [[<- 
# Loop-5
tracemem[0x7f8870d7f058 -> 0x7f8870d7f0c8]: 
tracemem[0x7f8870d7f0c8 -> 0x7f8870d7fc98]: [[<-.data.frame [[<- 
tracemem[0x7f8870d7fc98 -> 0x7f8870d7fd08]: [[<-.data.frame [[<- 

データフレームの代わりにリストを使用すれば、リストを修正することはR内部ではCコードを使うので、参照はインクリメントされず、1つのコピーだけが作られる。なので、先ほどよりも高速になる。

y <- as.list(x)
cat(tracemem(y), "\n")
<0x7f887354aa18> 

for (i in 1:5) {
  y[[i]] <- y[[i]] - medians[[i]]
}
tracemem[0x7f887354aa18 -> 0x7f8873589c18]: 

たしか…Data Sience for RのFunctionのところでは、基本的に最初にリストなりのアウトプットを格納する箱を用意してループさせることを推奨してた気がする(曖昧で…すいません)。

library(tidyverse)

x <- data.frame(matrix(runif(5 * 10), ncol = 5))
medians <- vapply(x, median, numeric(1))
out <- vector(mode = "list", length = ncol(x))

for (i in seq_along(x)) {
  out[[i]] <- x[[i]] - medians[[i]]
}

y <- out %>% map_dfc(., c)
y
# A tibble: 10 x 5
        V1      V2      V3       V4      V5
     <dbl>   <dbl>   <dbl>    <dbl>   <dbl>
 1  0.123  -0.0514  0.572   0.303    0.0858
 2 -0.0968  0.207  -0.323   0.00796  0.144 
 3  0.0159 -0.257   0.518  -0.0643  -0.529 
 4 -0.0159 -0.314  -0.0670 -0.00796 -0.415 
 5 -0.344   0.0988 -0.0160  0.788   -0.0858
 6  0.218  -0.340   0.0260  0.0779   0.320 
 7  0.503  -0.0644  0.0160 -0.200    0.194 
 8  0.183   0.562  -0.205  -0.0939   0.238 
 9 -0.125   0.0698 -0.199   0.436   -0.376 
10 -0.171   0.0514  0.157  -0.106   -0.232