この記事は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)))