R : apply系の関数の使い方

R

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でも機能しますが、使用においては注意が必要です。

R:apply関数をdata.frameに使う場合の注意点
data.frameにapply()関数を適用する場合の注意点をまとめています。apply()関数の使い方をネットで調べても、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関数で注意すべき点はこちら

R: matrixとdata.frameでlapplyの挙動が違って混乱?スッキリ解決
lapply()関数って、matrix型に適用した場合とdata.frame型に適用した場合で、関数の挙動がちょっと違ってみえたりして、混乱しがち。matrix型、data.frame型に対するlapply()関数の挙動をスッキリ整理します。

 

まとめ

apply系の関数について整理して、色々計算してみました。簡単なものならapply系の関数を使わなくても計算できますが、計算したい関数が複雑になってくるとapply系の関数の出番が多くなりそうですね。

  • apply : 列毎 or 行毎の処理ができる。
  • tapply : 列毎の処理をグループ化して実施できる。
  • lapply,sapply : ベクトルやリストの列毎の処理できる。
  • mapply : 複数のベクトルやリストのデータを引数にした処理ができる。