こんばんは、蓬莱です!
apply系の関数をまとめたものを、よくapplyファミリーと言いますよね。分かりやすい総称ということもあり、apply系ファミリーの使い方を一括してまとめた記事が多くあります。
自分もapplyファミリーを一通り学習していますが、lapply()とsapply()だけ扱う難易度が異常に高いと感じています。
そういうわけで、lapply()とsapply()単独の記事があってもいいんじゃないかと思ったのです。lapply()などの扱いに不安がある方、渾身の記事を作りましたので、どうぞご覧くださいませ!
listを活用するには、lapply()が不可欠!
lapply()とsapply()は、主にリストに対して使う関数になります。
applyファミリーの中では異色な存在かもしれませんね。lapply()を知っておくとリストの操作の幅が広がるため、リストを使いまくる人にはぜひ知っておいてほしい知識であります。
なお、「データフレームがあればいいじゃん。リストなんて死ね!」とお思いの方は、ぜひ下記の記事を見てください。
自分もデータフレームだけで1年半生き抜いてきましたが、勉強し直してみてリストも使えるんだなと実感しました。たくさんのベクトルをまとめることができる特徴はは、言葉以上に使える機能ですよ!
おすすめ記事:「データフレームだけでいいの?R言語のlistの実用的な使い方を考える記事」
lapplyの基本的な操作とメリット
list
[[1]]
[1] 11 14 14 22 16 18
[[2]]
[1] 3 6 3
[[3]]
[1] 41 46 32 52
[[4]]
[1] 53 52 68 62 55
[[5]]
[1] 11 9
[[6]]
[1] 18 19 22 22 19 18
[[7]]
[1] 8 10 7
[[8]]
[1] 1 2
まずは簡単なリストを使って、lapply()の機能を見ていきましょう。
上記のリストは、8つのベクトルが入っているだけのものです。階層が1つの単純なリストですので、最初の例題として最適かなと思います。
このリストの要素を1つ1つ取り出して、要素ごとに合計値を出すという操作を考えてみましょう。
この操作をfor文でやるとしたら、以下のようになります。
num <- length(list)
result <- c()
for(i in 1:num) {
res <- sum(list[[i]])
result <- c(result , res)
}
意外と長いかも!!?
for文でやろうとすると、3つの操作が必要になります。
- forループを何回行うかを決めるために、lengthでリスト自体の長さを定める
- forループの結果をまとめるための箱(result)を、あらかじめ作る
- forループで各要素の取得し、その合計をresultにまとめていく
回りくどい書き方をしているように見えますが、これらはforループを回すために必要な行為です。forループの中身を短くまとめることを考えても、3行は必要になります。
対して、lapply()を使うと、下記の様なスクリプトになります。
res <- lapply(list , sum)
なんとこの一行だけで済みます!
まさにぱぱぱっとやって終わりっ! って感じですね。コードが短くなったのは、以下の特徴があるからです。
- lapply()なら、リストの長さに左右されない
- 結果のベクトルが勝手に生成されるため、あらかじめ箱を作る必要がない
forの場合は要素数が変わると、ループさせる数も逐一変えなければなりません。対してlapply()の場合は、リストを分解したものを関数に渡すので、いくら要素数が変化しようが問題ありません。
さらに、結果のベクトルも勝手に生成してくれます。forのようにいちいち定義しておかなくても、resに答えを入れてくるのですね。
res <- lapply(list , sum)
res
[[1]]
[1] 95
[[2]]
[1] 12
[[3]]
[1] 171
[[4]]
[1] 290
[[5]]
[1] 20
[[6]]
[1] 118
[[7]]
[1] 25
[[8]]
[1] 3
res <- sapply(list , sum)
res
[1] 95 12 171 290 20 118 25 3
試しにlapply(list,sum)で、合計を出しました。
lapply()を使うと、計算結果がリストで返ってきているのが分かります。
sapply(list , sum)の結果も、下に載せておきます。
sapply()の結果は、ベクトルで返っているのが分かりますね。
lapply()のイメージ
さて。「何か知らないけど、lapply()役に立ちそう!」と思ってくれた人もいるでしょう。
このlapply()、sapply()を使いこなすためには、処理のイメージを頭に焼き付けておく必要があります。計算の流れを見たところで、lapply()の処理のイメージを見ていきましょう。
最初に提示したリストの構造は、上記の画像のようになっています。階層は1つしかなく、その中身も単純なベクトルが並んでいるだけです。
これにlapply()を適用しますと…。
Listの階層が一つ分解されます!
lapply(list , ??)と命令すると、「8つのベクトルをまとめていたリスト」から「8つのベクトル」に分解してくれます。
この分解した1つ1つの要素に、関数が適用されるわけです。
まるでfor文のように要素を一つずつ取り出して処理しているかのごとく、分解した要素について関数を適用していきます。
この操作があるからこそ、要素数がどんなに多くてもlapply()1つで計算できるのです。
lapply()は、言うなれば「階層を1つ分解して出来た要素を一つずつ計算していく」ということになりますね。
lapply()とapply()を組み合わせる
> list
$c1
meibo jp math eng soc sci
1 1 56 55 63 65 60
2 2 74 67 78 80 73
3 3 88 87 90 92 87
$c2
meibo jp math eng soc sci
1 1 70 61 72 72 70
2 2 72 68 77 77 73
3 3 81 75 88 93 86
$c3
meibo jp math eng soc sci
1 1 69 61 70 73 68
2 2 56 47 63 66 61
3 3 88 87 88 93 85
$c4
meibo jp math eng soc sci
1 1 71 68 72 75 67
2 2 68 62 73 77 70
3 3 74 65 82 83 82
$c5
meibo jp math eng soc sci
1 1 75 66 82 83 80
2 2 74 71 78 79 77
3 3 64 62 73 78 70
$c6
meibo jp math eng soc sci
1 1 88 87 88 93 85
2 2 77 74 80 81 78
3 3 69 67 71 72 70
4 4 79 74 80 84 77
lapply()の使い方とイメージを見たところで、少し応用した使い方を見てみましょう。
次に使うリストは、学生の成績を学年ごとに収納したリストです。各学年3人ずつの成績が入っており、その形式はデータフレームとなっています。
…おっと、6年生のリストには4人のデータが入っていますね。
このリストを使って、各学生の5教科の合計点を算出したいと思います。
lapply()を使用する際にまず思いつくのは、以下のスクリプトですよね。
lapply(list , sum)
$c1
[1] 1121
$c2
[1] 1141
$c3
[1] 1081
$c4
[1] 1095
$c5
[1] 1118
$c6
[1] 1584
基本に忠実なスクリプト!
直感的にlapply()を使用してみる人は多いと思います。しかし結果を見ると、なんだか変な数値が出てきてしまいました。
種を明かしますと、データフレーム内にある数字すべてを足しこんだ数値が返ってきているのです。
どうしてこのようになってしまったか? 理由は以下のイメージで説明できます。
最初に提示した例題では、分解した要素がベクトルでした。それに対して今回の要素はデータフレームです。
この違いにより、lapply(list , sum)としてしまうと、データフレームの中身全部を足してしまうことになるのです。
そういうわけで、学生一人一人の成績を出したいのなら、lapply()で分解した要素を一行ずつ取り出して計算しなければなりません。
行ごとに合計していく操作なら、apply()がうってつけですよね。
fun1 <- function(x){ x$sum <- apply(x[,2:6] , 1 , sum) ; return(x) } #短縮系
result <- lapply(list , fun1)
result
$c1
meibo jp math eng soc sci sum
1 1 56 55 63 65 60 299
2 2 74 67 78 80 73 372
3 3 88 87 90 92 87 444
$c2
meibo jp math eng soc sci sum
1 1 70 61 72 72 70 345
2 2 72 68 77 77 73 367
3 3 81 75 88 93 86 423
$c3
meibo jp math eng soc sci sum
1 1 69 61 70 73 68 341
2 2 56 47 63 66 61 293
3 3 88 87 88 93 85 441
# ・・・省略
新しくapply()を含んだ関数fun1を作り、lapply(list , fun1)とすれば、行ごとに計算してくれます。
これがlapply()とapply()の合わせ技です。この例題を見て、分解した要素に応じて操作を考えられるようになれば、言うことはありません。
多階層のリストにはlapply()を重ねる
> list
[[1]]
[[1]][[1]]
meibo jp math eng soc sci
1 1 56 55 63 65 60
2 2 74 67 78 80 73
3 3 88 87 90 92 87
[[1]][[2]]
meibo jp math eng soc sci
1 1 81 75 88 93 86
2 2 65 57 68 71 67
3 3 78 75 78 80 74
[[2]]
[[2]][[1]]
meibo jp math eng soc sci
1 1 70 61 72 72 70
2 2 72 68 77 77 73
3 3 81 75 88 93 86
[[2]][[2]]
meibo jp math eng soc sci
1 1 78 75 78 80 74
2 2 45 36 50 50 49
3 3 88 87 90 91 85
[[3]]
[[3]][[1]]
meibo jp math eng soc sci
1 1 69 61 70 73 68
2 2 56 47 63 66 61
3 3 88 87 88 93 85
[[3]][[2]]
meibo jp math eng soc sci
1 1 77 74 80 81 78
2 2 69 67 71 72 70
3 3 79 74 80 84 77
[[4]]
[[4]][[1]]
meibo jp math eng soc sci
1 1 71 68 72 75 67
2 2 68 62 73 77 70
3 3 74 65 82 83 82
[[4]][[2]]
meibo jp math eng soc sci
1 1 88 87 90 92 87
2 2 60 59 69 71 66
3 3 75 66 82 83 80
4 4 99 82 78 88 79
# ・・・省略
最後の例題として、2階層リストの操作について取り上げたいと思います。
リストの中身は先の例題とほぼ同じです。違う点としては、学年の下にクラスがある二重構造という点のみです。
1年生のリストに2クラス分の学生が入っているという構造ですね。
絵として書くなら、こんな感じです。
確かに階層が2つあります。問題設定にあまり関係はありませんが、6年生は3クラスにしてあります。
さて。このリストを使って、学生の5教科の合計点を算出してみましょう。
lapply()とapply()を組み合わせるなら、こうでしたよね。
fun1 <- function(x){ x$sum <- apply(x[,2:6] , 1 , sum) ; return(x) } #短縮系
result <- lapply(list , fun1)
これだとうまく回らないということに気づけば、すでにlapply()を自在に操れるようになっているでしょう。
もう1度イメージ的に見てみると、ある問題が浮き彫りになります。
分解した要素がまだリストの形です!
lapply()を使うと、1つ階層を分解してくれるというのはすでに学びました。
今の状況ではまだ1階層分のリストになっているので、apply()が適用できないわけです。
つまり、もう1回lapply()をかけて要素を分解しなければ、apply()を適用できないことになります。
lapply()を2回かけるコードとして様々なやり方があると思いますが、例として以下のコードを作ってみました。
fun1 <- function(x){ x$sum <- apply(x[,2:6] , 1 , sum) ; return(x) } #短縮系
fun2 <- function(x){ y <- lapply(x , fun1) ; return(y) } #短縮系
list <- lapply(list , fun2)
fun2という関数は、とりあえずlapply()で要素を分解するという関数です。
その分解した要素をfun1に渡すことで、2つの階層を分解したことになります。口では説明しにくいので、ぜひコードをコピーして学んでみてくださいね。
肝心の計算結果は、以下に示しておきます。
list
[[1]]
[[1]][[1]]
meibo jp math eng soc sci sum
1 1 56 55 63 65 60 299
2 2 74 67 78 80 73 372
3 3 88 87 90 92 87 444
[[1]][[2]]
meibo jp math eng soc sci sum
1 1 81 75 88 93 86 423
2 2 65 57 68 71 67 328
3 3 78 75 78 80 74 385
[[2]]
[[2]][[1]]
meibo jp math eng soc sci sum
1 1 70 61 72 72 70 345
2 2 72 68 77 77 73 367
3 3 81 75 88 93 86 423
[[2]][[2]]
meibo jp math eng soc sci sum
1 1 78 75 78 80 74 385
2 2 45 36 50 50 49 230
3 3 88 87 90 91 85 441
# ・・・省略
計算結果は、2階層リストにしっかりくっついていますね!
メリットとデメリット
3つの例題をみたところで、メリットとデメリットのまとめに入ります。
メリットについては、最初の例題の項に集約されています。for文で必要な定義が必要なくなるというとことですね。結果をまとめるベクトルを生成しなくていいし、要素数がいくら増えても、lapply()1つで解析が行えるという点です。
後者の要素数については、階層が増えるほど重宝しますよ(for文でやると、階層数に比例してforループを重ねないといけないため)。
デメリットは特にありません。for文のような縛りがなく、コードも軽く書けることから非の打ち所がないと思います。
ただし、lapply()を使ったからと言ってプログラムが速くなるわけではないことには注意すべきです。apply系を使えば速くなるよという話は各所でされていますが、lapply()とsapply()に関してはその限りではないと考えています。
記事の途中で、「まるでfor文のように要素を一つずつ取り出して処理しているかのごとく」と表現した部分があると思います。
lapply()で要素を分解するところまでは、lapply()独自の処理方法だと思います。しかし、そのあとはfor文と何ら変わらない処理方法を行っていると思います。
最後に
lapply()やsapply()は大変使いやすそうに見える反面、階層を分解しきらないと変な計算結果になるという罠も持ち合わせています。
しかしこの罠については、この記事にあるイメージを掴んでしまえば難なく回避できるはずです。
分解した後にも気を配るというのは大変ですが、使いこなせるようになるとワンランク上のプログラマーになれますよ!