R apply系をわかりやすく解説する (1)lapply

今回はRの処理を高速に処理するためには避けて通れないapply系の使い方を自分自身で仕組みを理解しつつまとめていきます。
Rもいわゆるループ系の処理が早ければそのほうがなじみがあるのですが、このapply系はいままで他の言語では接したことがなかったのでシックリくるまで時間がかかりました。

すべてのapply系をまとめて記載するとどうしても膨大になり「書式の紹介+チョットしたサンプル」程度になりがちなので、ここでは一つ一つ(自分で理解しながら)まとめていきたいと思います。
そんななか最初に取り上げるにはいきなりいまいちでマイナーですが、lapplyについてまとめてみます。

統計分析などではカラム(説明変数)ごとに処理を回して有意水準を見てみるなど、分析ををまとめて処理したいケースが多々ありますが、こういった場合に効率よく処理するときにはlapplyは重宝すると思います。

apply系の使い方についてはネットを検索するといくらでも出てきますが、それらを読んでもどうも使い方が
腹に落ちないことが多く、結局やりたいことと最も近い解説があるサイトをなんとか探して”コピペで使う・・・”ことで
なんとか使っている方も多いのではないでしょうか。
そんな方が(私もそうですが)apply系の仕組みを理解することで
もっと自由に使いこなせるようになることを目指します。

今回のテーマ: lapply

最初はよくある例をいくつか。

lapplyの基本形

1−2行目を実行すると3行目以降の結果が出力されます。

a = list(c(1:5), c(6:10), c(11:15))
lapply(a, mean)
> lapply(a, mean)
[[1]]
[1] 3
[[2]]
[1] 8
[[3]]
[1] 13

list 形式で与えた3つのベクトルが含まれる変数aのそれぞれのリスト要素ごとに
平均値を計算して、その結果をリスト形式で返してくれています。

念のための基本の確認ですが、lapplyは入力されたデータを処理して結果をリスト形式で返す関数なので、結果のリストから要素の取り出しには二重のカギ括弧[[]]を使う必要があるため

[1]]
[1] 3

のように、帰ってくる結果も上記のような表示となっています。実際にlapplyの結果を変数resに入れてから
1番目の要素をres[[1]]で取り出すことができます。

res = lapply(a, mean)
res[[1]]
[1] 3

なるほど・・・ここのmeanstdなど他の関数に変えればOKなのか!となりますが、
では、組み込み関数以外はどうすればよいのか? すなわち、独自に作った関数を計算させるにはどうしたらよいのでしょうか。

こういったチョットした応用になると途端に基本的な使い方の解説とのギャップが大きくなるのがこのapply系かとおもいます。

独自関数を利用する方法

すこし考えてみるとR的に考えればmeanの部分をfunctionで定義すれば良さそうではないかと思われます。

上記の例をfunctionを使って記述した例が以下の通りとなります。

lapply(a, function(x) {
mean(x)
})

先の例では、リストaの各要素を関数meanに順番に渡してその結果をリスト形式で格納しましたが、
この例では、リストaの各要素を独自に設定した関数の引数xに順番に渡してfunction(x){...}
計算結果を返してリストに格納する形になります。この例を実行してみると先の結果と一致するのが確認できます。

これで独自関数を使ったlapplyの基本的な使い方がわかりましたので、少し関数部分を変化させてみます。

lapply(a, function(x) {
y = 3
z = mean(x) + y
return(z)
})
[[1]]
[1] 6
[[2]]
[1] 11
[[3]]
[1] 16

上記の例ではそれぞれの平均に3を足した結果を変数zに保存して、そのzを戻す(return)計算となっています。
1−5行目を計算した結果が6行目以降となります。

このようにするとfunctionの内部でどのような関数計算も処理が可能となりますので必要な計算をさせて結果をreturnで戻してあげればOKです。

データフレームを渡して計算する方法

上記の例ではリスト形式で計算するためのデータを渡していますが、実際の計算ではdataframe形式でデータを保管していることが多いと
思いますので、そのままデータフレームを渡してカラム(列)ごとに何らかの操作を行いたいということが多いのではないでしょうか。

こういったときには、上記の事例では変数aに当たる部分に、処理する対象のカラム名を記載すれば、カラム名が順番に関数の中に引き渡されます

lapply(colnames(mtcars[1:3]), function(x) {
ret = mean(mtcars[,x])
return(ret)
})

上記の例では、おなじみのR組み込みデータのmtcarsのデータをつかっています。

colnames(mtcars[1:3])mtcarsの1−3行目のカラム名をベクトルとして取り出しています。
これをfunctionの引数として順番に渡してmtcars[,x]で対象の行だけが取り出せるのでその平均を計算して返しています。

この例では引き渡す値がカラム名のベクトルでありリストではありませんでした。データセット自体はデータフレームとして別途保存してあるデータを
使っているので、lapplyの中でデータセット自体を引き渡す必要ななく、単純にカラム名だけを渡せばよいのでこのような使い方が可能となります。

こうすれば関数内に好きな計算式を入れればカラムごとに計算して結果を返すプログラムが自由に書けるようになります。

複数の引数を渡す方法

関数の計算では2つ以上の引数を使う場合もありますが、その場合はlapplyの関数の後に追加の引数を記入することで使うことができます。

では実際の事例を見てみます。以下の例では関数部分をmyfuncで外出しにしてあり、二つの引数x, yが使われています。 lapplyで順番に引き渡す変数は一つだけなので、
それ以外の引数は関数(この場合はmyfunc)の後に追加でlapply(a, myfunc, y = 3)のように記入していけばOKです。この例では、yは関数myfuncの外部から与える
形になっているので、プログラムの他の部分で作ったyの値を引き渡すなどの計算が可能となります。

myfunc = function(x, y) {
a = list(c(1:5), c(6:10), c(11:15))
z = mean(x) + y
return(z)
}
lapply(a, myfunc, y = 3)

結果の取り出し

lapplyで順番に処理した結果はリスト形式で格納されています。これを取り出す方法はいくつかありますが
最も簡単な方法は先にも説明した[[]]ダブルのカッコで囲う方法です。

上記の例でres = lapply(a, myfunc, y = 3)のようにして結果をresという変数に保存してみます。
このときresはリスト形式となっています。
一つ目の要素を取り出すにはres[[1]]とすればOKです。
特定の結果を取り出すだけならこれでOKですがまとめて取り出したいときは面倒ですので、その際は
リストをベクトルに変更するunlist関数を使います。

unlist(res)とすれば結果がベクトルで一度に得られます。

> res = lapply(a, myfunc, y = 3)
> unlist(res)
[1]  6 11 16

もう少し複雑な計算の場合

上記の例では平均値の結果”だけ”を計算しているので結果を簡単にベクトルで取得することができますが、複数の計算結果が含まれるような関数やパッケージを使う
場合について検討します。

以下の例では正規性の検定をシャピロウイルク検定で行った例です。データセットmtcarsの1−3列のデータを順番に正規検定を行って結果を
resにリスト形式で格納しています。

res = lapply(names(mtcars[1:3]), function(x) {
shapiro.test(mtcars[,x])
})

この結果の1番目を出力すると以下の通りの計算結果が得られています。

> res[[1]]
Shapiro-Wilk normality test
data:  mtcars[, x]
W = 0.94756, p-value = 0.1229

計算結果には統計量やp値などいろいろ含まれているようです。以下に内容を展開してみました。

f:id:okdata:20190923112852p:plain:w600

これをunlistで展開してもデータがベクトル形式で羅列されるので
非常に加工しにくいものになってしまいます。

> res = lapply(names(mtcars[1:3]), function(x) {
+           shapiro.test(mtcars[,x])
+         })
>
> unlist(res)
statistic.W                       p.value
"0.947564726479274"           "0.122881358539443"
method                     data.name
"Shapiro-Wilk normality test"                 "mtcars[, x]"
statistic.W                       p.value
"0.753310022842721"        "6.05833813310341e-06"
method                     data.name
"Shapiro-Wilk normality test"                 "mtcars[, x]"
statistic.W                       p.value
"0.920012680133146"          "0.0208065696108598"
method                     data.name
"Shapiro-Wilk normality test"                 "mtcars[, x]"

ではこのリストの中からp値だけを取り出してみたいと思います。

このような場合には、以下のような形で$を使って要素をとりだすことが基本になります。

res[[1]]$p.value

> res[[1]]$p.value
[1] 0.1228814

目的のリストの一番目のp値だけ取り出すことができました。

では、このp値をすべてのリスト項目からまとめて取り出したいときにはどうしたら良いでしょうか。
具体的な方法は以下の通りです。

res = lapply(names(mtcars[1:3]), function(x) {
shapiro.test(mtcars[,x])
})
res.pval <- unlist(lapply(res, `[[`, "p.value"))

この形で処理すれば、変数res.pvalにp値がベクトル形式で取得できます。

> unlist(lapply(res, `[[`, "p.value"))
[1] 1.228814e-01 6.058338e-06 2.080657e-02

※ ちなみにres.pval <- sapply(res,[[, "p.value")の形で実行しても同じ結果が取得できますが、こちらはまたsapplyの時に紹介します。

※※ さらに、res.pval <- do.call(rbind, lapply(res,[[, "p.value"))とすると、結果がデータフレーム形式で取得できます。

上記の例でポイントになるのはlapply(res,[[, "p.value")が何をやっているのか?といったあたりかと思います。が、実はこのあたりまだよくわかってません。
この形式で書けば結果が取り出せるのはわかるのですが、このカギ括弧が何をやっているのか?その理由が明快に書かれている場所(マニュアルなど)がまだみつけられていないので、現段階ではおまじない的に利用しています。

なお、このコードでp.valuestatisticにすれば統計量が得られます。

今回は、lapplyの使い方を自分自身で再確認しながらまとめてみました。
まだまだ奥が深そうで自由に使いこなせるようになるにはしばらく時間がかかりそうですが、今後も地道に理解を深めていきたいとおもいます。