Rではforループを使うと計算速度が遅くなると言われます。でも、apply系の関数を使えば、forループを使わずに、ベクトル、リストなどに対する繰り返し処理を実行でき、計算速度が遅くなることを回避できます。
apply系の関数
ベクトル、リストに関して、まとめて処理するための関数として、以下のような関数があります。
- apply
- tapply
- sapply,lapply
- mapply
Rを使えるようになるために、apply系の関数の理解は必須だと思います。でも、それぞれどう違うのか、混同しがちなので整理しました。
apply関数
行列タイプのデータを処理します。
行ごとにまとめた処理を各行繰り返す or 列ごとにまとめた処理を各列繰り返します。
apply(X,MARGIN,FUN,…)
X : array,matrix(data.frameでも機能する)
MARGIN : 1なら行毎、2なら列毎に
FUN : 適用する関数
apply関数の適用対象Xはarrayかmatrixが基本です。Xがdata.frameでも機能しますが、使用においては注意が必要です。
tapply関数
列ごとにまとめた処理を各列繰り返します。
INDEXとなる列を作っておいて、その列に基づたグループ化を実施した後、各グループごとの各列の繰り返し処理の結果を返します。
tapply(X,INDEX,FUN,…)
X : INDEXでグループ化して評価するデータ
INDEX : Xをグループ化するための分類
FUN : 適用する関数
lapply関数、sapply関数
ベクトルやリストのデータを列毎に処理します。
lapplyはlistを返します。
sapplyは可能であればvectorもしくはmatrixを返します(これがユーザーフレンドリーらしいけど、どういう型を返すか、sapplyは必ずしも確定していないので、かえって使いにくいように思うけど...)。
lapply(X, FUN, …), sapply(X,FUN, …)
X : ベクトル or list
FUN :適用する関数
mapply関数
複数のベクトルやリストのそれぞれから行毎に順次データを取り出して、それらをまとめて処理します。
mapply(FUN, …)
FUN : 適用する関数
… : FUNに渡す引数(ベクトル or リスト)(個数は任意)
■ 簡単な計算例
各関数ができることは上のような内容になりますが、これだけではわかりにくいので計算例を示します。
以下のようなデータを用意します。
x = 平均0、標準偏差1の正規分布からの乱数N個
y = 平均1、標準偏差2の正規分布からの乱数N個
z = 平均2、標準偏差1の正規分布からの乱数N個
x、y、zを以下のようなdata.frameに入れておきます。
df1 : x列、y列、z列にx、y、zを並べたdata.frame
df2 : type列にx、y、zの識別文字
data列にx、y、zを縦に並べたdata.frame
N <- 2000
set.seed(1)
df1 <- data.frame(x = rnorm(N, 0, 1),
y = rnorm(N, 1, 2),
z = rnorm(N, 2, 1))
df2 <- data.frame(type = rep(NA, 3*N), data = rep(NA, 3*N))
df2[1:N,] = list("x", df1$x)
df2[(N+1): (2*N),] = list("y", df1$y)
df2[(2*N+1): (3*N),] = list("z", df1$z)
x,y,zの分布をグラフにするとこんな感じです。
df1から作る場合
library(ggplot2)
ggplot(df1) +
geom_freqpoly(aes(x), colour = "red") +
geom_freqpoly(aes(y), colour = "green") +
geom_freqpoly(aes(z), colour = "blue") +
ggsave("test_apply1.png", width = 1920/350, height = 1280/350)
df2から作る場合
ggplot(df2) +
geom_freqpoly(aes(x = data,colour = type), binwidth = 0.5) +
ggsave("test_apply2.png", width = 1920/350, height = 1280/350)
df1でもdf2でもグラフは作れますが、df2の形式の方が、グラフ化するためのコードは簡単ですね。
このデータに対して、以下のような計算をしてみます。
(A)df1の各行の和を求める
(B)df1の各列の平均を求める
(A) df1の各行の和を求める。
各行ごとに計算するので、使える関数は
- MARGIN = 1としたapply
- mapply
# df1の各行ごとの和
df1_sum <- df1$x + df1$y + df1$z
ap1 <- apply(df1, 1, sum)
ap2 <- mapply(function(x,y,z){x + y + z}, df1$x, df1$y, df1$z)
head(data.frame(df1_sum,ap1,ap2), 5) # 結果はどれも同じになります
実行結果
df1_sum ap1 ap2
1 -0.5333832 -0.5333832 -0.5333832
2 0.1036906 0.1036906 0.1036906
3 5.9744830 5.9744830 5.9744830
4 4.2821267 4.2821267 4.2821267
5 1.1879224 1.1879224 1.1879224
(B) df1の各列の平均を求める。
各列毎に計算するので、使える関数は
- MARGIN = 2としたapply
- sapply
- lapply
# df1の各列ごとの平均値
df1_mean <- c(mean(df1$x), mean(df1$y), mean(df1$z))
ap3 <- apply(df1, 2, mean)
ap4 <- sapply(df1, mean)
head(data.frame(df1_mean, ap3, ap4))
# 結果はどれも同じになります
df1_mean ap3 ap4
x -0.01395503 -0.01395503 -0.01395503
y 1.03203128 1.03203128 1.03203128
z 1.98411778 1.98411778 1.98411778
lapplyを使う場合も基本的には同じですが、結果がlistで返ってきます。
ap5 <- lapply(df1, mean)
(ap5)
$x
[1] -0.01395503
$y
[1] 1.032031
$z
[1] 1.984118
また、df2を使うのであれば、グループ化して処理すればよいのでtapplyが使えます。グラフ化するにはdf2の形式の方が簡単ですので、グラフ化と各グループに対する繰り返し処理の両方を実施する場合は、データをdf2の形式にまとめておいた方が良さそうです。
ap6 <- tapply(df2$data, df2$type, mean)
ap6
x y z
-0.01395503 1.03203128 1.98411778
適用するデータがlistの場合
data.frameは「長さが同じベクトル」のリストみたいなものです。data.frameに対しては、上で見てきたようにapply、lapply、sapplyのいずれも機能しました。では、リストに対してapply、lapply、sapplyを適用するとどうなるでしょう?
長さが2000のベクトルx、y、zを含むリストls1を考えます。
set.seed(1)
ls1 <- list(x = rnorm(2000, 0, 1),
y = rnorm(2000, 1, 2),
z = rnorm(2000, 2, 1))
apply(ls1, 2, mean)
apply(ls1, 2, mean) でエラー: dim(X) は正の長さを持たねばなりません
このようにリストに対してapplyを適用するとエラーになってしまいます。data.frameは「長さが同じベクトル」のリストのようですが、applyの適用においては、listとdata.frameが異なるということです。ただし、listをdata.frame に変換すれば、当然計算はできます。
apply(as.data.frame(ls1), 2, mean)
x y z
-0.01395503 1.03203128 1.98411778
リストに対してlapply、sapplyの適用も当然できます。
lapply(ls1, mean)
$x
[1] -0.01395503
$y
[1] 1.032031
$z
[1] 1.984118
sapply(ls1, mean)
x y z
-0.01395503 1.03203128 1.98411778
長さが異なるベクトルをもつリストの場合はどうでしょう?
ls2 <- list(x = rnorm(1000, 0, 1), # 長さ1000
y = rnorm(2000, 1, 2), # 長さ2000
z = rnorm(3000, 2, 1)) # 長さ3000
as.data.frame(ls2)
(function (..., row.names = NULL, check.rows = FALSE, check.names = TRUE, でエラー:
引数に異なる列数のデータフレームが含まれています: 1000, 2000, 3000
もはや、このリストはdata.frameに変換することはできないので、applyは使えません。lapply、もしくは、sapplyで処理しましょう。
lapply(ls2, mean)
$x
[1] -0.02068914
$y
[1] 0.9744418
$z
[1] 1.991365
関連記事
使えるようになるととても便利なapply系関数ですが、思うように動いてくれないことも多いです。lapply関数で注意すべき点はこちら
まとめ
apply系の関数について整理して、色々計算してみました。簡単なものならapply系の関数を使わなくても計算できますが、計算したい関数が複雑になってくるとapply系の関数の出番が多くなりそうですね。
- apply : 列毎 or 行毎の処理ができる。
- tapply : 列毎の処理をグループ化して実施できる。
- lapply,sapply : ベクトルやリストの列毎の処理できる。
- mapply : 複数のベクトルやリストのデータを引数にした処理ができる。