R: matrixとdata.frameでlapplyの挙動が違って混乱?スッキリ解決

R

lapply()関数って、matrix型のデータに適用した場合とdata.frame型のデータに適用した場合で、関数の挙動がちょっと違ってみえたりして、分かりにくかったり、混乱してしまったりしますよね。matrix型、data.frame型に対するlapply()関数の挙動をスッキリ整理します。

そもそも、lapply()関数ってどんな関数?

lapplyはリストの各要素に対して、同じ関数FUNを適用した場合の結果を求める関数です。

lapply(X, FUN, ...)

X : 基本的にはリスト
FUN : Xに対して適用する関数
戻り値 : Xに対してFUNを適用した結果

X = list(X1, X2, X3, ..., Xn)

だとしたら、

y <- lapply(X, FUN)

を実行すると

y = list(FUN(X1), FUN(X2), FUN(X3), ..., FUN(Xn))

が返ってきます。

lapply()関数の中身

適用する変数Xは「基本的にはリスト」と記載しましたが、lapplyの関数の中身は以下のようになっており、Xがvectorでなかったり、objectでなかったりした場合は、内部でas.list()が呼ばれ、自動的にlistに変換されてから、laaplyの本体が呼ばれます。だから、Xはリストではなくても良いんです。これが「基本的には」と書いた意味です。

 > lapply
function (X, FUN, ...)
{
FUN <- match.fun(FUN)
if (!is.vector(X) || is.object(X))
X <- as.list(X) # ← 内部で as.list()が呼ばれlistに型変換される
.Internal(lapply(X, FUN))
}

なお、Xがリストの場合、is.vector(X)はTRUEを返します。当然、is.list(X)もTRUEを返します。なんだか分かりにくいですね。

matrixに適用した場合とdata.frameに適用した場合の違い

いよいよ本題です。

lapplyを使っていると、Xがmatrixの場合とXがdata.frameの場合でlapplyの挙動が違うように思ったり、混乱してしまうことがあるかもしれません。ここできちんとlapplyの挙動を理解しておきましょう。

次のようなデータを考えます

a <- matrix(1:9, nrow = 3)
df <- data.frame(a)

a,dfの中身を表示すると以下のようになります。当然ですが、両者の数字の並びは同じような感じになります。これにlapplyを使って、FUN = sumを適用してみます。

> a[,1] [,2] [,3][1,] 1 4 7[2,] 2 5 8[3,] 3 6 9

> df
X1 X2 X3
1 1 4 7
2 2 5 8
3 3 6 9

matrixとdata.frameの違いが曖昧なかたはこちらもどうぞ

R : matrixとdata.frameの違い
matrixもdata.frameも行、列に数字を並べたもののようですが、ここを曖昧にしてしまうと計算ミスを招きかねない。わかっている人には至極当たり前のことかと思いますが、この記事を読んで僕と同じようにつまずく人が少しでも減ってくれれば幸いです。

matrixに適用した場合

ans1 <- lapply(a, sum)

ans1の要素数を確認すると9です。

> length(ans1)[1] 9

中身を確認すると

> ans1
 [[1]]
 [1] 1

 [[2]]
 [1] 2

 [[3]]
 [1] 3

 [[4]]
 [1] 4

 [[5]]
 [1] 5

 [[6]]
 [1] 6

 [[7]]
 [1] 7

 [[8]]
 [1] 8

 [[9]]
 [1] 9

つまり、ans1の結果は、
1要素目 : 1
2要素目 : 2
3要素目 : 3

のように何も処理されていないような結果となります。

期待どおりの結果でしょうか?

ちょっと違うという方もいらっしゃいますよね。

先程lapply()の関数の中身を確認したように、lapplyを適用する変数がvector(is.vector()でTRUEを返す)だったら、その変数は内部で型変換されないのですが、次のようにaをas.list()で型変換してみます。

aa <- as.list(a)

aaの要素数、中身を確認すると要素数が9で、リストの各要素には、aの中身が1つずつ入っていることがわかります。

> length(aa)[1] 9
> aa
 [[1]]
 [1] 1

 [[2]]
 [1] 2

 [[3]]
 [1] 3

 [[4]]
 [1] 4

 [[5]]
 [1] 5

 [[6]]
 [1] 6

 [[7]]
 [1] 7

 [[8]]
 [1] 8

 [[9]]
 [1] 9

このaaに対してlapplyを適用することを考えると、各要素に1つずつ値が入った要素数9のリストに対してlapplyを適用することになるので、戻り値はans1のようになりますよね。このように、matrix aにlapplyを適用するということを、一旦aをlist型に型変換したのち、lapplyを適用していると考えると、その挙動がわかりやすくなるように思います。

data.frameに適用した場合

data.frameの場合はどうでしょう?

ans2 <- lapply(df, sum)

ans2の要素数と中身を確認してみます。

>length(ans2)[1] 3

>ans2 
$X1 
[1] 6

$X2 
[1] 15

$X3 
[1] 24

こちらは期待通りの結果なのではないでしょうか。

dfに対して、as.list()で型変換をすると、2つの要素を持つベクトルを要素とする要素数5つのlist()になります。

> as.list(df)
$X1
[1] 1 2 3

$X2
[1] 4 5 6

$X3
[1] 7 8 9

各要素に対して、sum()を実施するので、6, 15, 24を要素とする要素数3つのlistが返ってきています。

その他のapply系関数

lapply関数だけでなく、Rにはまとめて処理を行うapply系関数と呼ばれる関数があります。lapply以外のapply系関数についてはこちらから。

R : apply系の関数の使い方
Rではforループを使うと計算速度が遅くなると言われます。でも、apply系の関数を使えば、forループを使わずに、ベクトル、リストなどに対する繰り返し処理を実行でき、計算速度が遅くなることを回避できます。

まとめ

matrixにlapplyを適用する場合とdata.frameに対してlapplyを適用する場合で、lapplyの挙動が違うように思える時があるかもしれませんが、lapply(X, FUN)を実行すると、変数Xを一旦、listに変換してから、その各要素にFUNを適用した結果が返ってくると考えると分かりやすく整理できると思います。