Apply family を使ってみる

この記事はR Advent Calendar 2013の12月7日の配当記事です。
 
そこそこにRを使っていままでやってきたが、Rの特徴としてときたま挙げられるのがapply関数群だと思う。
一番よく使うのは apply だと思うが、初心者が慣れるのに難しい割にはそんなに解説テキストが転がっているわけでもなさそうなので書いてみる。
Rの利点としてのベクトル演算を活かす、というのがapplyっぽいが、それを活かしたからといって高速化するわけではない。が、applyを使いこなせるメリットとしては、何をやっているかがわかりやすい、というのと、スクリプト行数が減る、ということだと思っている。
がしかし、初心者や複数人でコードを書く場合は、愚直に多重 for loop を書いておくほうがかえってわかりやすいかもしれない。
 
まずは apply関数群のお仲間を探す。

apropos("pply") # *pply を探す
[1] ".mapply"    "apply"      "dendrapply" "eapply"     "kernapply" 
[6] "lapply"     "mapply"     "rapply"     "sapply"     "tapply"    
[11] "vapply"

help.searchではすべてのパッケージ内の関数を探してくれるが、Rを起動した直後のデフォルト環境ではこれらが引っかかる。


Description:

Returns a vector or array or list of values obtained by applying a
function to margins of an array or matrix.

Usage:

apply(X, MARGIN, FUN, ...)

おなじみの apply であるが、基本的には行列の行か列かに一括して関数を適応してくれるものと思えばいい。

# apply
nr <- 10 # 行数
nc <- 20 # 列数
mat <- matrix(runif(nr*nc), nr=nr, nc=nc) # 適当な行列

apply(mat, 1, sum) # rowSums と同じ
apply(mat, 2, sum) # colSums と同じ

実際には3次元以上の構造をもつ array に対応している。MARGINの設定を変えることで、どの次元に関数を適応するか変更できる。

dim3 <- 5 # 3次元目に5つのデータを入れる
arr <- array(runif(nr*nc*dim3), c(nr, nc, dim3))
apply(arr, 3, sum) # MARGIN を 3 に設定する
[1]  91.26123  96.47695  94.64727 100.59719 104.18166

いわゆるz軸に5つの座標があって、z軸平面で切ったxy平面上のデータ nr*nc 個についての合計を返したので、計算結果は5つになっている。



Description:

‘lapply’ returns a list of the same length as ‘X’, each
element of which is the result of applying ‘FUN’ to the
corresponding element of ‘X’.

Usage:

lapply(X, FUN, ...)

Rではデータの長さが違うときにどうにかこうにかデータを保持しようと思ったら、listと呼ばれる構造でもつことが多い。そのとき、list内の各要素に関数を適応したいときに使う。
vecという、各ベクトルの長さが違うベクトルがいくつか集まったオブジェクトを適当に作り、それぞれの平均を調べたり、平均0でt検定してみたかったりしたら

# lapply
vec <- mapply(sample, rpois(5, 10), size=rpois(5, 8), replace=TRUE) # 長さの違うベクトルの作成
lapply(vec, mean)   # それぞれに mean を使う
lapply(vec, t.test) # それぞれに t.test を使う

結果は基本的にlistで返ってくる。

Description:

‘sapply’ is a user-friendly version and wrapper of ‘lapply’ by
default returning a vector, matrix or, if ‘simplify = "array"’,
an array if appropriate, by applying ‘simplify2array()’.
‘sapply(x, f, simplify = FALSE, USE.NAMES = FALSE)’ is the same
as ‘lapply(x, f)’.

Usage:

sapply(X, FUN, ..., simplify = TRUE, USE.NAMES = TRUE)

sapplyはlapplyと似たようなもので、listではなくvectorで結果を返す。この作用のことをsimplifyというらしく、それでsが付いている。
ただし、結果の長さが違う場合、例えば、計算上NAがいくつかあってsummary関数でNAの数のカウントが余計にくっついてきた場合、などは勝手にlist化されたはず。

# sapply
sapply(vec, mean) # unlist(sapply(vec, mean)) でゴリ押しできる

Description:

Apply a function to each cell of a ragged array, that is to each
(non-empty) group of values given by a unique combination of the
levels of certain factors.

Usage:

tapply(X, INDEX, FUN = NULL, ..., simplify = TRUE)

tapply とはtableのことで、仲間分けごとに計算したいときに使う。
例えば、tipsというサンプルデータに、曜日による集計結果が載っているので、この曜日ごとにチップの平均額を計算したい場合に使う。

# tapply
data(tips, package="reshape2")
tapply(tips$tip, tips$day, mean)
     Fri      Sat      Sun     Thur 
2.734737 2.993103 3.255132 2.771452 

基本的にINDEXで用いたtableの名前が勝手につく。データ構造はsapplyと同じくvectorで返ってくるが、simplyfy=FALSEを指定すればlapplyのようにlistで返ってくる。




Description:

‘mapply’ is a multivariate version of ‘sapply’. ‘mapply’
applies ‘FUN’ to the first elements of each ... argument, the
second elements, the third elements, and so on. Arguments are
recycled if necessary.

Usage:

mapply(FUN, ..., MoreArgs = NULL, SIMPLIFY = TRUE, USE.NAMES = TRUE)

mapplyは慣れるまではかなり使いにくい関数であるが、慣れればかなりの頻度でfor loopを書く手間を減らすことができる。
例えば、長さの異なる連続した数をいろいろなパターンで発生させたいときには

# mapply
mapply(rep, c(5, 3, 9), c(3, 9, 8))
[[1]]
[1] 5 5 5

[[2]]
[1] 3 3 3 3 3 3 3 3 3

[[3]]
[1] 9 9 9 9 9 9 9 9

とできる。基本的にはvector構造で返ってくるlistが返ってくるが、何を言っているのかわからねーと思うがオレもわからないという気分なので、以下のコマンドを実行すると

mapply(t.test, vec)
            [,1]                [,2]                [,3]                [,4]                [,5]               
statistic   4.296689            6.432945            3.174968            6.5                 7.15458            
parameter   5                   9                   6                   4                   13                 
p.value     0.007739734         0.0001205231        0.01919769          0.002890007         7.427578e-06       
conf.int    Numeric,2           Numeric,2           Numeric,2           Numeric,2           Numeric,2          
estimate    4                   3.1                 4.428571            5.2                 5.642857           
null.value  0                   0                   0                   0                   0                  
alternative "two.sided"         "two.sided"         "two.sided"         "two.sided"         "two.sided"        
method      "One Sample t-test" "One Sample t-test" "One Sample t-test" "One Sample t-test" "One Sample t-test"
data.name   "dots[[1L]][[1L]]"  "dots[[1L]][[2L]]"  "dots[[1L]][[3L]]"  "dots[[1L]][[4L]]"  "dots[[1L]][[5L]]" 

となり、結果に数値や文字列が混在するがゆえに訳のわからない出力になっているが、これもSIMPLIFYオプションがあり、

mapply(t.test, vec, SIMPLIFY=FALSE)
[[1]]

        One Sample t-test

data:  dots[[1L]][[1L]] 
t = 4.2967, df = 5, p-value = 0.00774
alternative hypothesis: true mean is not equal to 0 
95 percent confidence interval:
 1.606919 6.393081 
sample estimates:
mean of x 
        4 


[[2]]

        One Sample t-test

data:  dots[[1L]][[2L]] 
t = 6.4329, df = 9, p-value = 0.0001205
alternative hypothesis: true mean is not equal to 0 
95 percent confidence interval:
 2.009879 4.190121 
sample estimates:
mean of x 
      3.1 


[[3]]

        One Sample t-test

data:  dots[[1L]][[3L]] 
t = 3.175, df = 6, p-value = 0.0192
alternative hypothesis: true mean is not equal to 0 
95 percent confidence interval:
 1.015521 7.841622 
sample estimates:
mean of x 
 4.428571 


[[4]]

        One Sample t-test

data:  dots[[1L]][[4L]] 
t = 6.5, df = 4, p-value = 0.00289
alternative hypothesis: true mean is not equal to 0 
95 percent confidence interval:
 2.978844 7.421156 
sample estimates:
mean of x 
      5.2 


[[5]]

        One Sample t-test

data:  dots[[1L]][[5L]] 
t = 7.1546, df = 13, p-value = 7.428e-06
alternative hypothesis: true mean is not equal to 0 
95 percent confidence interval:
 3.938962 7.346752 
sample estimates:
mean of x 
 5.642857 

とすることで、異なるデータ構造をlistで保持できる。

また、mapplyを連発することもできる。例えば、

mat の行ごとに、groupでラベル付けされる2群にわけてその2群についてsummary関数を適応して、Medianだけとってきたい。

と思った時に、

group <- sample(1:2, size=nc, replace=TRUE)
mapply(function(y) mapply(function(x) x["Median"], apply(mat, 1, tapply, group, summary)[[y]]), seq(nr))
           [,1]   [,2]   [,3]   [,4]   [,5]   [,6]   [,7]   [,8]   [,9]  [,10]
1.Median 0.2989 0.4709 0.7280 0.4661 0.7255 0.5752 0.4816 0.3993 0.5557 0.1960
2.Median 0.7027 0.4264 0.3852 0.7913 0.5812 0.7618 0.6277 0.2205 0.3841 0.3051

とやれば一行一発でできるが、これは一行が煩雑になりがちなのでオススメしない。



Description:

‘vapply’ is similar to ‘sapply’, but has a pre-specified type
of return value, so it can be safer (and sometimes faster) to use.

Usage:

vapply(X, FUN, FUN.VALUE, ..., USE.NAMES = TRUE)

vapplyはぶっちゃけ使ったことがないが、sapplyと似ているらしい。しかし、出力のデータ型を指定しなければならない。
そのため、適応する関数がどんなデータ型を返すか知っていないときついっぽい。

# vapply
vapply(vec, summary, FUN.VALUE=c("Min."=0, "1st Qu."=0, "Median"=0, "Mean"=0, "3rd Qu."=0, "Max."=0))
vapply(vec, summary, FUN.VALUE=rep(0, 6)) # 簡単にすべて数値ですよと指定。上と同じ。

変なデータ型を指定するとエラーとなり、

vapply(vec, summary, FUN.VALUE=rep("1", 6)) # 簡単にすべて文字列ですよと指定。
 以下にエラー vapply(vec, summary, FUN.VALUE = rep("1", 6)) : 
   値の型は 'character' でなければなりません、 
 しかし、FUN(X[[1]]) の結果の型が 'double' です 

と言われる。


Description:

‘rapply’ is a recursive version of ‘lapply’.

Usage:

rapply(object, f, classes = "ANY", deflt = NULL,
how = c("unlist", "replace", "list"), ...)

rapplyは再帰的構造でも使えるlapplyである。例えば、listの作成時にlistがvectorの一種であることに気づかないと

vec1 <- list(vec, vec)
[[1]]
[[1]][[1]]
[1]  2  1  3 12  3

[[1]][[2]]
[1] 4 5 4 6 6 3 4

[[1]][[3]]
[1] 10  5  6 11  2  9 11

[[1]][[4]]
[1] 1 3 2 1 5 4 3 2 3

[[1]][[5]]
 [1] 11 10  6  3  8 10  7 11  8  9  7 12  5


[[2]]
[[2]][[1]]
[1]  2  1  3 12  3

[[2]][[2]]
[1] 4 5 4 6 6 3 4

[[2]][[3]]
[1] 10  5  6 11  2  9 11

[[2]][[4]]
[1] 1 3 2 1 5 4 3 2 3

[[2]][[5]]
 [1] 11 10  6  3  8 10  7 11  8  9  7 12  5

となり、listの中のlistという、ちょっとそれマジ勘弁という構造になってしまう。
余力のある人はlength関数で、vec1自体の長さと、vec1に存在するリストの長さを調べてみて欲しい(lapply or sapply の出番)。

length(vec1)
lapply(vec1, length)

というわけでrapplyを使ってみるのだが、listだと思ってlapplyを安易に使うと

lapply(vec1, mean)
[[1]]
[1] NA

[[2]]
[1] NA

 警告メッセージ: 
1: In mean.default(X[[1L]], ...) :
   引数は数値でも論理値でもありません。NA 値を返します 
2: In mean.default(X[[2L]], ...) :
   引数は数値でも論理値でもありません。NA 値を返します 

と怒られる。

# rapply
rapply(vec1, mean)

rapplyの中にはhowというオプションがあり、これを指定すると出力を指定できる。

how = c("unlist", "replace", "list")

基本的にはsapplyっぽく"unlist"の形でvectorで返ってくる。しかし、中には再帰的なlist構造のままデータが欲しいという人もいるようで、その時には"replace"を指定するとクソみたいな構造のままデータを得られる。"list"はようわからん。
他のapplyと同様に、... 部分に様々なオプションを突っ込めるから、log変換らしきこともやってみた。

rapply(vec1, mean, how="unlist")
rapply(vec1, mean, how="replace")
rapply(vec1, log, how="replace", base=2)

Description:

‘eapply’ applies ‘FUN’ to the named values from an
‘environment’ and returns the results as a list. The user can
request that all named objects are used (normally names that begin
with a dot are not). The output is not sorted and no enclosing
environments are searched.

This is a primitive function.

Usage:

eapply(env, FUN, ..., all.names = FALSE, USE.NAMES = TRUE)

eapplyは環境(environment)について関数を適応する。素人は触らないほうがいい気がする。オレも知らないし。

# eapply
env <- new.env(hash = FALSE) # so the order is fixed
env$a <- 1:10
env$beta <- exp(-3:3)
env$logic <- c(TRUE, FALSE, FALSE, TRUE)
# what have we there?
utils::ls.str(env)

# compute the mean for each list element
eapply(env, mean)
unlist(eapply(env, mean, USE.NAMES = FALSE))

Description:

‘kernapply’ computes the convolution between an input sequence
and a specific kernel.

kernapplyはカーネル関数をなんやかんやする。あれこれ語るとボロがでるのでサンプルコードを丸々コピペする。

# kernapply
# financial time series German stock index DAX.
x <- EuStockMarkets[,1]
k1 <- kernel("daniell", 50)  # a long moving average
k2 <- kernel("daniell", 10)  # and a short one
plot(k1)
plot(k2)
x1 <- kernapply(x, k1)
x2 <- kernapply(x, k2)
plot(x, lwd = 3)
lines(x1, col = "red",   lwd = 3)    # go long if the short crosses the long upwards
lines(x2, col = "green", lwd = 3)    # and go short otherwise

Description:

Apply function ‘FUN’ to each node of a ‘dendrogram’
recursively. When ‘y <- dendrapply(x, fn)’, then ‘y’ is a
dendrogram of the same graph structure as ‘x’ and for each node,
‘y.node[j] <- FUN( x.node[j], ...)’ (where ‘y.node[j]’ is an
(invalid!) notation for the j-th node of y.

Usage:

dendrapply(X, FUN, ...)

dendrapplyは系統樹について計算する。正直なところこれを使うくらいならfor loopを使って逐一地味に処理していくほうが自分にも他人にもわかりやすいと思う。

# dendrapply
dhc <- as.dendrogram(hc <- hclust(dist(USArrests), "ave"))
dhc21 <- dhc[[2]][[1]]

## too simple:
dendrapply(dhc21, function(n) utils::str(attributes(n)))