こんにちは。からあげ博士(@phd_karaage)です。Rを使いこなす上で一番苦労する関数の1つにapply
ファミリーがある気がします。この前も後輩がapply
ではなくforループであれこれ書いていて、別の後輩から苦言を呈されていました。
pythonやRにおいて、forループは避けるべきものとされていて、apply
ファミリーを使うことでそれを回避することができる。それが一番の要因のようですね。pythonでもRでもとりあえず計算が回ればいい、という思想ではなく、実行速度にまで気を配れるようになると1つステップアップできるのではないかなという気がします。
さてそんなapply
ファミリーですが、どんなことができるのでしょうか。そして実装上はどのようにしていけばいいのでしょうか?
目次
そもそもapply
ファミリーとは何か?
Applyという英単語から連想できるように、「何かに、Xという処理(関数)を適用する」という処理を行う関数群になります。このapply
ファミリーには、apply
の他にsapply
やlapply
にtapply
などがあり、それぞれがそれぞれの役割をもって処理を行う関数です。
R、特に統計手法とRの使い方を勉強しましょうねと有名なこの本では1.2.15節において"Implicit loops"(暗黙のループ)としてこれら関数が紹介されていますが、結構単純な使い方しか教えてくれません。その一方でこの筆者はこのように記載しています。
The looping constructs of R are described in Section 2.3.1. For the purposes of this book, you can largely ignore their existence. However, there is a group of R functions that it will be useful for you to know about.*1
ループ構造についてはこの後の節で解説するがほとんど無視してよい。だけど役立つ関数があるからここに記すと。
ここに書かれた通り、forループは忌み嫌われる存在であることがわかりますね。それを代替してくれるのがapply
ファミリーという訳です。
apply
の単純な使い方
これ自体は結構単純な書き方で使うことができます。
?apply
でapply
に関するドキュメントを読んでみましょう。そうすると使い方は以下の通りと書かれています。
apply(X, MARGIN, FUN, ...)
これで分かったらそんなに苦労しないよということになりますね。各引数がどんなものか見てみましょう。
まずXに入るのが、配列、行列です。配列、行列に対してforループを回避してあれこれしてくれる関数であるということを考えると、データをここで入れるという操作に納得です。
次にMARGINを見てみましょう。これはpythonでいうaxisに相当する部分。1なら行、2なら列に関数を適用してくれるということになります。ただpythonのaxisと違ってc(1,2)としてあげることで各要素に関数を適用してくれるという仕様があります。これは結構便利。
そして最後に出てくるのはFUNですね。FUNとはそのままfunctionであり、すなわち適用したい関数を入力することでその関数について適用した結果を返してくれるということです。この部分が結構曲者で、自作関数も受け付けてくれるという懐の深さがある一方で、なにをやっているか分かりにくい、コードを読んでもパッと理解できない、ということに繋がっている気がします。
それでは実際に使ってみましょう。今回もトイデータにはirisデータセットを使います。
data <- iris print(apply(data[1:4], 2, mean))
やっていることは単純です。data
にirisデータセットを格納し(Rはベース関数の中にあるからやっぱり便利ですよね)、各列ごとに平均を求めているだけです。なぜdata
のうち、[1:4]
としてデータの一部を使っているのか、それはhead(data)
をしていただくと分かるのですが、5列目にはSpeciesのデータが入っていて、このデータは文字列からなるデータだからです。mean
を適用してもエラーが出るのは明白です。
今度は各行に同じ処理を適用してみましょう。
print(apply(data[1:4], 1, sqrt))
ここでは各値の平方根を取ってみました。出力された結果を見ても、各行に、すなわち各個体のデータに平方根を取るsqrt
が適用されていることが分かりますね。
自作関数とapply
さて、apply
の引数、FUNにあたる部分は自作関数でもよい、ということになっていましたね。ということで、さっそく自作関数を使ってapply
を使ってみましょう。とりあえずデータの中身を10倍にする自作関数を作って実行してみます。
func_1 <- function(x){ a <- x * 10 return(a) } b <- 5 print(func_1(b)) print(apply(data[1:4], 1, func_1))
このfunc_1
は入力値を10倍にして返す自作関数です。とりあえず、5行目でb
に5を代入して、func_1
の挙動を確かめています。確かに10倍されたことが明らかになったので、apply
のFUN部分にfunc_1
を指定してみます。
結果はちゃんと各行ごとに10倍された値が返ってきましたね。これが列の場合、つまりMARGINが2の場合どうなるでしょうか?
print(apply(data[1:4], 2, func_1))
列ごとに実行しても値を10倍にするという挙動は変わりません。printされた配列はある種転置された形になっているということが分かりますね。ただし今回の場合は各要素ごとに10倍したい訳ですから同じ結果になるとはいえ、こちらの書き方のほうが適切でしょう。
print(apply(data[1:4], c(1, 2), func_1))
MARGINの引数をc(1, 2)
としてあげることで各要素について計算してくれと主張することができ、後から処理を見返す上でもなにをやっているか明確になるでしょう。
sapply
とlapply
さて、apply
の簡単な使い方が分かったところで、apply
ファミリーの他の関数についても見ていきましょう。結構単純なのはsapply
ですね。これは与えられたデータのすべての要素に対して同じ処理をしてくれるものになります。挙動を早速確認してみましょう。
print(sapply(data[1:4], mean))
今回各特徴量ごとに名前が与えられていることから、名前ごとに平均値を出してもらうことができました。このsapply
では配列のどの方向に平均を求めるかを指定する必要がないのがポイントです。挙動としては、apply(data[1:4], 2, mean)
と同じことをしてくれます。それでは名前のインフォメーションを消してみましょうか。
data_2 <- as.matrix(data[1:4]) sapply(data_2, mean)
ただの行列にしてしまった場合、異なる結果を返してきましたね。各要素に対して平均を取るという面白い挙動をしていて、当然値は変わりません。さらに返り値は行列ではなく数値となっています。これはclass()
で確認することができます。
res_1 <- sapply(data_2, mean) res_2 <- sapply(data[1:4], mean) res_3 <- apply(data[1:4], 2, mean) print(class(res_1)) print(class(res_2)) print(class(res_3)) print(class(data_2)) print(class(data[1:4]))
結果の通り、res_X
の中身はすべて実数ベクトルとして返ってくることが分かります。一方でdata_2
は先に変換した通り行列として、data[1:4]
はデータフレームとして識別されています。
さらに自作関数でも試してみましょう。
res_4 <- sapply(data_2, func_1) res_5 <- apply(data_2, 2, func_1) print(head(res_4)) print(head(res_5)) print(class(res_4)) print(class(res_5))
ここでは同じくsapply
とapply
を比較しています。どうでしょうか。sapply
では「すべての要素に」を忠実に守って、返り値は実数ベクトルとなっています。一方でapply
では列ごとにという指定が生きていることから、行列型で返ってきます。
apply
で各要素ごとに、つまりMARGINをc(1, 2)
と指定した場合はどうでしょうか?
res_6 <- apply(data_2, c(1, 2), func_1) print(head(res_6)) print(class(res_6))
同じく行列型で返ってくるというのはおもしろいところですね。
それではさらにlapply
もやってみましょう。これも使い方は基本的にsapply
と同じです。
res_7 <- lapply(data[1:4], mean) print(res_7) res_8 <- lapply(data_2, mean) print(head(res_8)) print(class(res_7)) print(class(res_8))
こちらも面白い結果になりましたね。データフレームに適用した場合は各特徴量に対して平均値を返すのに、行列に適用した場合はすべての要素に平均を適用するという結果になりました。そして返り値はリスト型であることが分かります。
tapply
はちょっと趣が違う
apply
ファミリーの中でちょっと趣が違うのがtapply
です。使い方としてはこんな感じ。
tapply(data$Sepal.Length, data$Species, mean)
なにかが違いますね。引数について見てみましょう。tapply(X, INDEX, FUN, ...)
という形になっています。apply
に似ているようで、違う部分がありますね。そうです。INDEXですね。
このINDEXには因子型が入る必要があります。第二引数のdata$Species
についてclass()
を見てみましょう。
print(class(data$Species))
このようにfactorが返ってきます。
tapply
はINDEXに基づいて関数を処理してくれるということが分かりますね。
apply
とtapply
の合わせ技で
このように、わざわざforループを使わずとも各データの行列や要素について処理を行うことができるようになりましたね。apply
では行あるいは列、さらには要素ごとに処理を行い、sapply
やlapply
は要素ごとに処理を行い、その返り値が異なるということが分かりました。
因子型が絡むと厄介ですが、それもtapply
が解決してくれるというのも分かりました。今回使っているirisデータセットには4つの特徴量と、3つの品種に関するデータがあります。4つの特徴量について品種ごとの平均などが知りたいと思うのは当然のことです。
これをforループを使わずに処理するにはどのようにすればいいでしょうか?
func_2 <- function(x){ tapply(x, data$Species, mean) } print(apply(data[1:4], 2, func_2))
これで解決しました。func_2
の中にtapplyを用いて品種ごとの平均を求める関数を作成し、apply
で各列ごと、すなわち各特徴量ごとに対してfunc_2
を適用するという流れになっています。これですべてが解決しますね。
ちなみに1行で書くこともできて、
print(apply(data[1:4], 2, function(x) tapply(x, data$Species, mean)))
とすることもできます。
どちらのほうが可読性が高いかは人それぞれだとは思いますが、余計な関数を定義しない分後者のほうが見やすいかもしれませんね。
まずはapply
をマスターしよう!
個人的に使う機会が多いのはapply
とtapply
でしょうか。Rを使ってこれら関数を使いこなせるようになってくると、ああ、自分もそれなりにRが使えるようになったなと実感してしまうところです。あくまで個人の感想ですが。
このapply
ファミリーは状況に応じて使い分けが必要ですが、まずはapply
を使いこなせるようになると、ファミリーに属するほかの関数についても中身が見えるようになってきます。
forループでももちろん処理できる内容ではありますが、このapply
ファミリーを使いこなすことで処理速度やコードの可読性が上がったりしますので、ぜひとも使いこなせるようになってください!
*1:Peter Dalgaard (2008), Introductory Statistics with R (Statistics and Computing) (English Edition), Springer