DATE : 2008/03/14 (Fri)
データフレームから条件式が成り立つ行のみを抽出する subset という関数があります。例えば、result データフレームの「x」という列の値が0を超える行のみを抽出するには、次のようなコードを実行します。(「>」や「+」はプロンプトです)
> subset(result, 0 < x)
通常、データフレームの列にアクセスするには、「result$x」とするか、「result['x']」としなければなりません。ただ単に x と書いても、x という名前のオブジェクトが参照されるだけです。ところが、subset 関数の場合は、条件式に当たる部分では自動的にデータフレームのオブジェクトを補ってくれます。そのため、subset 関数の条件式の部分では、ただ単に列名のみを書くだけで十分です。
このような subset 関数を R で直接実行する分には、なんの問題もありません。ところが、あるとき subset を内部で使う関数を書いていたときに、はたと困ってしまいました。条件式の渡し方が分からなかったのです。例えば、次のような関数を実行しようとしても、失敗してしまいます。
> subset_function <- function(condition) { + subset(result, condition) + } > subset_function(0 < x) 以下にエラー eval(expr, envir, enclos) : オブジェクト "x" は存在しません
R では、式は必要になるまで評価されません。そのため、subset_function に渡した条件式は評価されずに関数内部に入ります(遅延評価)。ところが、関数内部の subset の部分で評価が失敗しているようです。
しばらく悩んだところ、次のような解決策が思い浮かびました。
> subset_function <- function(condition) { + subset(result, eval(condition)) + } > subset_function(expression(0 < x))
式表現を渡して、subset のところで評価するという方法です。ところが、いまひとつスマートではありません。実際、subset 関数は expression 関数を使用せずに式を評価できています。
そこで、R 2.6.2 のソースコードから、subset 関数を定義している部分(src/library/base/R/frametools.R)を調べてみました。条件式の評価に関わる部分のみを抜き出すと、次のようになります。「(...)」は省略を表します。
subset.data.frame <- function (x, subset, select, drop = FALSE, ...) { (...) e <- substitute(subset) r <- eval(e, x, parent.frame()) (...) }
subset 関数の定義を診ると、条件式(subset)は、substitute 関数に渡され、その結果が eval 関数に渡されています。まず、substitute 関数によって式を解析木オブジェクトに変換しています。そして、eval 関数によって、その解析木を渡されたデータフレームの環境で、そのデータフレームは親の呼び出しスタックから取得するという処理になっています。eval の引数は少々分かりづらかったのですが、引数として渡されたデータフレームに対して評価を行う際の書き方であると、eval 関数のヘルプに書かれていました。subset_function は、引数でデータフレームを受け取ってはいませんが、親の呼び出しスタックにあるデータフレームを関数内で直接使っています。そのため、データフレームの取得に親の呼び出しスタックを使用しています。
subset 関数の定義が上のようになっていたので、subset_function も次のように書き直せます。
> subset_function <- function(condition) { + e <- substitute(condition) + r <- eval(e, result, parent.frame()) + subset(result, r) + } > subset_function(0 < x)
このようにすることで、条件式をそのまま使用できるようになりました。
注意
上の関数の内容を、以下のように1行にまとめた場合はうまく動作しません。
> subset_function <- function(condition) { + subset(result, eval(substitute(condition), result, parent.frame())) + }
subset 関数の引数となる eval 関数や、その引数となる substitute 関数は、遅延評価のために、その関数の実行される呼び出しスタックが、subset 関数以下になります。そのため、result のある呼び出しスタックがうまく呼び出せずに失敗するようです。