R:apply関数をdata.frameに使う場合の注意点

R

data.frameにapply()関数を適用する場合の注意点をまとめています。apply()関数の使い方をネットで調べても、data.frameに対する使用法はたくさん見つかりますが、注意点が全然出てきません。エラーに悩まされたり、間違った計算をしないように、押さえておくべきところをちゃんと押さえておきましょう。

ネットでapply()関数の使い方を調べると

ネットでapply()関数の使い方を調べると、

  • apply関数は,data.frame形式の行方向や列方向に対して処理を行う関数
  • apply関数で、data.frameの行ごとの計算をする
  • data.frameの各行/各列に対して、同種の演算を一括に行うときにapply を利用する

というような説明がなされていることが多いです。けれど、この説明だとちょっと危険。apply()関数のことをきちんと理解していないと、意味不明のエラーメッセージに悩まされることになりかねません。

apply()関数をヘルプで調べてみる

RStudioのヘルプでapply()関数を調べてみると、適用できるデータ型は配列(array)、行列(matrix)であると書かれています(以下の文章の赤線部分)。data.frame型は書かれていません。

Description
Returns a vector or array or list of values obtained by applying a function to margins of an array or matrix.

Usage
apply(X, MARGIN, FUN, …)

Arguments

X
an array, including a matrix.

MARGIN
a vector giving the subscripts which the function will be applied over. E.g., for a matrix 1 indicates rows, 2 indicates columns, c(1, 2) indicates rows and columns. Where X has named dimnames, it can be a character vector selecting dimension names.

FUN
the function to be applied: see ‘Details’. In the case of functions like +, %*%, etc., the function name must be backquoted or quoted.


optional arguments to FUN.

じゃぁどうして、data.frameでも機能するの?

じゃぁ、apply()関数はdata.frameに対して使えないかというと、そういうわけでもありません。

apply関数の中身を確認してみると、初めの段階で変数Xがオブジェクトかどうかチェックされ、オブジェクトの場合、Xはmatrix型かarray型に変換されてから指定した関数が適用されます(下のコードの赤線部分)。つまり、data.frame型に対して、apply()関数を適用すると、一旦、matrix型に変換されてから、指定した関数が適用されているということ。

function (X, MARGIN, FUN, ...)
{
    FUN<-match.fun(FUN)
    dl<-length(dim(X))
    if (!dl)
        stop("dim(X) must have a positive length")
    if (is.object(X))
        X<-if (dl==2L)
               as.matrix(X)
           else as.array(X)
    d<-dim(X)
    dn<-dimnames(X)
    ds<-seq_len(dl)

以下省略

data.frameは列が異なっていればデータ型は異なっても構わないのに対し、matrix型やarray型は全ての要素(行、列とも)が同じデータ型であることが必要です。このため、異なる型が混ざっているdata.frameにapply関数を適用すると、指定した関数に渡される変数の型がdata.frameの型と異なる可能性が出てきます。これをきちんと理解していないと、意味不明のエラーメッセージに悩まされたり、最悪の場合は間違った計算結果を出してしまう可能性があります。

以下のようなdata.frameを考えます(df1)。

df1 <- data.frame(x = 1:5, y = 11:15, z = 21:25)
df1
> df1
  x  y  z
1 1 11 21
2 2 12 22
3 3 13 23
4 4 14 24
5 5 15 25

これに対して、apply()関数を使って、以下の関数を適用してみます。

my_func1 <- function(x){ 
    paste0("val1 = ", x[1] ," val2 + val3 = ", x[2] + x[3]) 
} 

apply(df1, MARGIN = 1, FUN = my_func1) 
apply(df1, MARGIN = 2, FUN = my_func1)
> apply(df1, MARGIN = 1, FUN = my_func1)
[1] "val1 = 1 val2 + val3 = 32"
[2] "val1 = 2 val2 + val3 = 34"
[3] "val1 = 3 val2 + val3 = 36"
[4] "val1 = 4 val2 + val3 = 38"
[5] "val1 = 5 val2 + val3 = 40"
> apply(df1, MARGIN = 2, FUN = my_func1)
x
"val1 = 1 val2 + val3 = 5"
y
"val1 = 11 val2 + val3 = 25"
z
"val1 = 21 val2 + val3 = 45"

これは想像どおりの結果になったのではないかと思います。

次に、以下のようなdata.frame型の変数df2を考えます。df2はdf1のxを数字ではなく、文字に変えただけのdata.frameです。

df2 <- df1
df2$x <- as.character(df2$x)   # xのみデータ型をcharacter型に変える
df2
> df2
  x  y  z
1 1 11 21
2 2 12 22
3 3 13 23
4 4 14 24

単純に表示しただけでは、df1と見た目は同じです。でもstr()関数で構造を確認してみると、以下のようにdf1とdf2は違っていることがわかります(そういう風に作ったので当たり前ですが)。

str(df1)
str(df2)
> str(df1)
'data.frame': 5 obs. of 3 variables:
$ x: int 1 2 3 4 5
$ y: int 11 12 13 14 15
$ z: int 21 22 23 24 25
> str(df2)
'data.frame': 5 obs. of 3 variables:
$ x: chr "1" "2" "3" "4" ...
$ y: int 11 12 13 14 15
$ z: int 21 22 23 24 25

df2に対して、my_func1を適用するとエラーになります。

apply(df2, MARGIN = 1, FUN = my_func1)
> apply(df2, MARGIN = 1, FUN = my_func1)
x[2] + x[3] でエラー: 二項演算子の引数が数値ではありません
Called from: paste0("val1 = ", x[1], " val2 + val3 = ", x[2] + x[3])

data.frameに対して、apply()関数を適用するとデータ型が変わる可能性があることを理解していないと、

「x[2] + x[3] でエラー: 二項演算子の引数が数値ではありません」ってどういうこと???
2列目と3列目は数値のはずなのに…

って、なっちゃいますよね。知っていてもなってしまうかも…。でもエラーが出てくるなら、まだまし。もしエラーが出ずに処理できてしまう関数だったりしたら…、怖っ!

data.frameに対してapply()関数を適用する場合、こういうリスクがあるということを認識して、事前の確認を丁寧に行いながらコードを書いていくしかないのかなと考えています。

データ型が変わっていることの確認

先程の例に対して、引数の型を返す関数を適用してみると、渡されるデータは型変換されてしまっているのが確認できます。

my_func2 <- function(x){
    paste0("val1 = ", class(x[1]),
                  " val2 = ", class(x[2]),
                  " val3 = ", class(x[3]))
}

apply(df1, MARGIN = 1, FUN = my_func2) 
apply(df2, MARGIN = 1, FUN = my_func2)
> apply(df1, MARGIN = 1, FUN = my_func2)
[1] "val1 = integer val2 = integer val3 = integer"
[2] "val1 = integer val2 = integer val3 = integer"
[3] "val1 = integer val2 = integer val3 = integer"
[4] "val1 = integer val2 = integer val3 = integer"
[5] "val1 = integer val2 = integer val3 = integer"
> apply(df2, MARGIN = 1, FUN = my_func2)
[1] "val1 = character val2 = character val3 = character"
[2] "val1 = character val2 = character val3 = character"
[3] "val1 = character val2 = character val3 = character"
[4] "val1 = character val2 = character val3 = character"
[5] "val1 = character val2 = character val3 = character"

まとめ

apply()関数はmatrix, array型に対して使うのが基本。data.frame型に対して使う場合は、処理結果を丁寧に確認して使ってください。

apply系の関数の使い方はこちら。

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

それにしても、Rって、ちょっと癖がありますね。とても面白い言語なんだけど。

R : if文の使い方。間違えやすいポイントに注意
Rにも、他のプログラミング言語と同様、条件判定の制御文があります。ただ、Rのif文は、挙動を正しく理解していないと誤った計算をしてしまう場合があります。if文の使い方と間違えやすいポイントについて整理しています。
R : length() 関数が返す長さって何?
length()関数は、長さを返す関数であるということは想像できるのですが、きちんと理解していないと、引数の型によっては期待しているものと違う答えが返ってきます。そして、誤った評価…。そんなことにならないように、きちんと理解しておきましょう。