からあげ博士の日常と研究と

博士課程を満期退学した人が好きなことを好きなままに書くところ。

Rのapplyファミリー(apply, tapply, sapply, lapply)をマスターする ――それぞれの関数の使い方

 こんにちは。からあげ博士(@phd_karaage)です。Rを使いこなす上で一番苦労する関数の1つにapplyファミリーがある気がします。この前も後輩がapplyではなくforループであれこれ書いていて、別の後輩から苦言を呈されていました。

 pythonやRにおいて、forループは避けるべきものとされていて、applyファミリーを使うことでそれを回避することができる。それが一番の要因のようですね。pythonでもRでもとりあえず計算が回ればいい、という思想ではなく、実行速度にまで気を配れるようになると1つステップアップできるのではないかなという気がします。

 さてそんなapplyファミリーですが、どんなことができるのでしょうか。そして実装上はどのようにしていけばいいのでしょうか?

目次

そもそもapplyファミリーとは何か?

 Applyという英単語から連想できるように、「何かに、Xという処理(関数)を適用する」という処理を行う関数群になります。このapplyファミリーには、applyの他にsapplylapplytapplyなどがあり、それぞれがそれぞれの役割をもって処理を行う関数です。

 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の単純な使い方

 これ自体は結構単純な書き方で使うことができます。

 ?applyapplyに関するドキュメントを読んでみましょう。そうすると使い方は以下の通りと書かれています。

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

 これで分かったらそんなに苦労しないよということになりますね。各引数がどんなものか見てみましょう。

 まずXに入るのが、配列、行列です。配列、行列に対してforループを回避してあれこれしてくれる関数であるということを考えると、データをここで入れるという操作に納得です。

 次にMARGINを見てみましょう。これはpythonでいうaxisに相当する部分。1なら行、2なら列に関数を適用してくれるということになります。ただpythonのaxisと違ってc(1,2)としてあげることで各要素に関数を適用してくれるという仕様があります。これは結構便利。

 そして最後に出てくるのはFUNですね。FUNとはそのままfunctionであり、すなわち適用したい関数を入力することでその関数について適用した結果を返してくれるということです。この部分が結構曲者で、自作関数も受け付けてくれるという懐の深さがある一方で、なにをやっているか分かりにくい、コードを読んでもパッと理解できない、ということに繋がっている気がします。

 それでは実際に使ってみましょう。今回もトイデータにはirisデータセットを使います。

www.phd-karaage.com

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)としてあげることで各要素について計算してくれと主張することができ、後から処理を見返す上でもなにをやっているか明確になるでしょう。

sapplylapply

 さて、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))

 ここでは同じくsapplyapplyを比較しています。どうでしょうか。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に基づいて関数を処理してくれるということが分かりますね。

applytapplyの合わせ技で

 このように、わざわざforループを使わずとも各データの行列や要素について処理を行うことができるようになりましたね。applyでは行あるいは列、さらには要素ごとに処理を行い、sapplylapplyは要素ごとに処理を行い、その返り値が異なるということが分かりました。

 因子型が絡むと厄介ですが、それも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をマスターしよう!

 個人的に使う機会が多いのはapplytapplyでしょうか。Rを使ってこれら関数を使いこなせるようになってくると、ああ、自分もそれなりにRが使えるようになったなと実感してしまうところです。あくまで個人の感想ですが。

 このapplyファミリーは状況に応じて使い分けが必要ですが、まずはapplyを使いこなせるようになると、ファミリーに属するほかの関数についても中身が見えるようになってきます。

 forループでももちろん処理できる内容ではありますが、このapplyファミリーを使いこなすことで処理速度やコードの可読性が上がったりしますので、ぜひとも使いこなせるようになってください!  

*1:Peter Dalgaard (2008), Introductory Statistics with R (Statistics and Computing) (English Edition), Springer