読者です 読者をやめる 読者になる 読者になる

Think more, try less

Owen Zhangの言葉を大切にして,日々Kaggleに取り組んでいます.

「R言語徹底解説」をご恵贈いただきました

R

Hadley Wickhamの著書「Advanced R」の邦訳「R言語徹底解説」を訳者のみなさまよりご恵贈いただきました,ありがとうございました.

読んでみて気になった所

R言語徹底解説」の原著の「Advanced R」についてですが,これは無料で公開されており,2013年11月頃にはほぼ内容も充実したものでした.当時から多くのRユーザーにとって新しい上級な教科書としての位置づけのドキュメントとして参照されているものになります.

現在,2016年の今でも「Advanced R」は色褪せない内容ではありますが,2年前ということもあり一部,新しい機能の実装などについては反映されていません.当然その翻訳本である「R言語徹底解説」にも同様に載っていない内容があります.「Advanced R」から親しんできたユーザーであるような人なら最新のR事情はキャッチアップされていると思うので問題ないと思いますが,「R言語徹底解説」からはじめる新規のRユーザーである場合,現在の新しい機能の実装との差分がどこにあるのかわからないかもしれません.そこで,すべてを取り上げることはできませんが,私が気になった部分について内容を補足をしたいと思います.

13章「非標準評価」を一通り読まれた方に紹介したい記事として,

Lazyeval: a new approach to NSE (2015-01-02)

をご紹介したいと思います.

Lazyeval: a new approach to NSE (2015-01-02)

この記事では,{lazyeval}パッケージによる新しい非標準評価(Non-Standard Evaluation)に対するアプローチの解説をしています.要点は3つあります.

  • substitute()を使用する代わりに,表現式と環境の両方を捕捉することができるlazyeval::lazy()を使用する.(また...でプロミスを捕捉するlazyeval::lazy_dots(...)を使用する.)

  • 非標準評価(NSE)を使用するすべての関数は,実際の計算をする標準評価(SE)のエスケープハッチを持っているべきである.標準評価(SE)関数の名前の末尾は_とする.

  • 標準評価(SE)関数は,ユーザーがプログラミングしやすいように柔軟な入力仕様を持っている.

とあります.以下,どういうことなのか例を見て確認してみたいと思います.

lazy()lazy_eval()

lazy()は,関数の引数に関連した表現式と環境の両方を捕捉するsubstitute()と等価のものとあります.Vignetteにsubstitute()の振る舞いが載っていないので比較してみます.substitute()の方は,environmentの情報が得られませんが、一見、同様の振る舞いに見えます.

g <- function(x = a - b) {
  substitute(x)
}
g()
# a - b
g(a + b)
# a + b
library(lazyeval)

f <- function(x = a - b) {
  lazy(x)
}
f()
#> <lazy>
#>   expr: a - b
#>   env:  <environment: 0x7fd2f3a406b0>
f(a + b)
#> <lazy>
#>   expr: a + b
#>   env:  <environment: R_GlobalEnv>

eval()を補完するものとして,{lazyeval}パッケージは,遅延オブジェクト(lazy object)に関連した環境を使用するlazy_eval()を備えているとあります.eval()と比較してみると以下のようになります.

a <- 10
b <- 1
eval(g())
# [1] 9
eval(g(a + b))
# [1] 11
a <- 10
b <- 1
lazy_eval(f())
#> [1] 9
lazy_eval(f(a + b))
#> [1] 11

標準評価(Standard evaluation)

私たちは非標準評価をするある関数を必要とするときはいつでも,常に最初に標準評価版を書こうとあります.あとで非標準評価版を実装するために,標準評価版をlazy()lazy_eval()を用いて実装してみます.

subset2_ <- function(df, condition) {
  r <- lazy_eval(condition, df)
  r <- r & !is.na(r)
  df[r, , drop = FALSE]
} 
subset2_(mtcars, lazy(mpg > 31))
#>     mpg cyl disp hp drat   wt qsec vs am gear carb
#> 18 32.4   4 78.7 66 4.08 2.20 19.5  1  1    4    1
#> 20 33.9   4 71.1 65 4.22 1.83 19.9  1  1    4    1

以下でも同様に振る舞います.

subset2_(mtcars, ~mpg > 31)
#>     mpg cyl disp hp drat   wt qsec vs am gear carb
#> 18 32.4   4 78.7 66 4.08 2.20 19.5  1  1    4    1
#> 20 33.9   4 71.1 65 4.22 1.83 19.9  1  1    4    1
subset2_(mtcars, quote(mpg > 31))
#>     mpg cyl disp hp drat   wt qsec vs am gear carb
#> 18 32.4   4 78.7 66 4.08 2.20 19.5  1  1    4    1
#> 20 33.9   4 71.1 65 4.22 1.83 19.9  1  1    4    1
subset2_(mtcars, "mpg > 31")
#>     mpg cyl disp hp drat   wt qsec vs am gear carb
#> 18 32.4   4 78.7 66 4.08 2.20 19.5  1  1    4    1
#> 20 33.9   4 71.1 65 4.22 1.83 19.9  1  1    4    1

非標準評価(Non-standard evaluation)

手元にある標準評価版で,非標準評価版を書くことは簡単である.私たちは環境に関連する未評価の表現式を捕捉するlazy()を使用するだけとあります.ここで13章「非標準評価」のsubset2()と比較してみます.:

subset2 <- function(df, condition) {
  subset2_(df, lazy(condition))
}
subset2(mtcars, mpg > 31)
#>     mpg cyl disp hp drat   wt qsec vs am gear carb
#> 18 32.4   4 78.7 66 4.08 2.20 19.5  1  1    4    1
#> 20 33.9   4 71.1 65 4.22 1.83 19.9  1  1    4    1

別の書き方をすると,

subset2 <- function(df, condition) {
  condition_call <- lazy(condition)
  r <- lazy_eval(condition_call, df)
  r <- r & !is.na(r)
  df[r, , drop = FALSE]
}
subset2(mtcars, mpg > 31)
# mpg cyl disp hp drat    wt  qsec vs am gear carb
# Fiat 128       32.4   4 78.7 66 4.08 2.200 19.47  1  1    4    1
# Toyota Corolla 33.9   4 71.1 65 4.22 1.835 19.90  1  1    4    1

これは,13章「非標準評価」のsubset2()(P288.)と対応しています.

subset2 <- function(df, condition) {
  condition_call <- substitute(condition)
  r <- eval(condition_call, df)
  r <- r & !is.na(r)
  df[r, , drop = FALSE]
}
subset2(mtcars, mpg > 31)
# mpg cyl disp hp drat    wt  qsec vs am gear carb
# Fiat 128       32.4   4 78.7 66 4.08 2.200 19.47  1  1    4    1
# Toyota Corolla 33.9   4 71.1 65 4.22 1.835 19.90  1  1    4    1

このように,13章「非標準評価」ではいきなり非標準評価の実装を行っていますが,手続きとしては標準評価版の実装を行い,その後lazy()を用いて非標準評価版の実装を行うことが推奨されています.これから私たちが実装する際に常にこうするべきとは思いませんが、少なくともHadley Wickhamはそのように実装しているんだということを知っているだけで彼のパッケージをさらに使いやすくなると思います.また、非標準評価(NSE)については批判もありますが,標準評価版の実装を常に用意してくれているという配慮を考えるとこのような批判はなくなるのかなと思います.

interp()

{lazyeval}パッケージには,interp()という便利な関数が実装されています.Interpolate values into an expression.(表現式に値を挿入する)と説明されていますが,下記のように振る舞います.

var_ <- "mpg"
threshold <- 31
interp(~var > x, var = as.name(var_), x = threshold)
# ~mpg > 31
subset2_(mtcars, interp(~var > x, var = as.name(var_), x = threshold))
#                 mpg cyl disp hp drat    wt  qsec vs am gear carb
# Fiat 128       32.4   4 78.7 66 4.08 2.200 19.47  1  1    4    1
# Toyota Corolla 33.9   4 71.1 65 4.22 1.835 19.90  1  1    4    1

このinterp()を用いれば,標準評価版の関数は,私たちがプログラミングしやすいように柔軟な入力仕様を持つことができます.

var_ <- "mpg"
threshold <- 31
above_threshold_ <- function(df, var_, threshold) {
  subset2_(mtcars, interp(~var > x, var = as.name(var_), x = threshold))
}
above_threshold_(mtcars, var_, 31)
#                 mpg cyl disp hp drat    wt  qsec vs am gear carb
# Fiat 128       32.4   4 78.7 66 4.08 2.200 19.47  1  1    4    1
# Toyota Corolla 33.9   4 71.1 65 4.22 1.835 19.90  1  1    4    1

above_threshold <- function(df, var_, threshold) {
  cond <- interp(~ var > x, var = lazy(var_), x = threshold)
  subset2_(df, cond)
}
above_threshold(mtcars, mpg, 31)
#                 mpg cyl disp hp drat    wt  qsec vs am gear carb
# Fiat 128       32.4   4 78.7 66 4.08 2.200 19.47  1  1    4    1
# Toyota Corolla 33.9   4 71.1 65 4.22 1.835 19.90  1  1    4    1

スコーピング(Scoping)

またlazy()は関数の引数に関連した環境を捕捉するので,以下のようなバグを回避することができます.

x <- 31
f1 <- function(...) {
  x <- 30
  subset(mtcars, ...)
}
# Uses 30 instead of 31
f1(mpg > x)
#>     mpg cyl disp  hp drat   wt qsec vs am gear carb
#> 18 32.4   4 78.7  66 4.08 2.20 19.5  1  1    4    1
#> 19 30.4   4 75.7  52 4.93 1.61 18.5  1  1    4    2
#> 20 33.9   4 71.1  65 4.22 1.83 19.9  1  1    4    1
#> 28 30.4   4 95.1 113 3.77 1.51 16.9  1  1    5    2

f2 <- function(...) {
  x <- 30
  subset2(mtcars, ...)
}
# Correctly uses 31
f2(mpg > x)
#>     mpg cyl disp hp drat   wt qsec vs am gear carb
#> 18 32.4   4 78.7 66 4.08 2.20 19.5  1  1    4    1
#> 20 33.9   4 71.1 65 4.22 1.83 19.9  1  1    4    1

lazy()substitute()よりもう一つ利点を持っており,非標準評価(NSE)をよりカジュアルに使用することが可能になります.

x <- 31
g1 <- function(comp) {
  x <- 30
  subset(mtcars, comp)
}
g1(mpg > x)
#> Error: object 'mpg' not found
g2 <- function(comp) {
  x <- 30
  subset2(mtcars, comp)
}
g2(mpg > x)
#>     mpg cyl disp hp drat   wt qsec vs am gear carb
#> 18 32.4   4 78.7 66 4.08 2.20 19.5  1  1    4    1
#> 20 33.9   4 71.1 65 4.22 1.83 19.9  1  1    4    1

以上,簡単ではありますが、{lazyeval}パッケージのご紹介でした*1.この内容は,13章「非標準評価」にも記載されていない内容となるのでぜひキャッチアップしておきたいところです.(私もまだまだ勉強中です.間違い等あればご教授頂ければ幸いです.)

おわりに

上記内容は「Advanced R」の補足ですが,「R言語徹底解説」の方について触れますと,私自身で{lazyeval}パッケージの簡単なご紹介を行うことだけでも,英語の解釈で詰まったり,適切な訳語がわからず,説明に悩みました.こんな少しのVignetteで非常に時間のかかったことを考えると,「Advanced R」という書籍の分量を翻訳されたこと,そしてその翻訳が極めて精緻なレベルであることに敬服するばかりです.このような書籍をRユーザーに届けてくれたことに感謝しかありません.またこの素晴らしい書籍を多くの方に読んでもらいたいと思いました.ぜひ手にとって頂ければ幸いです.

*1:GPL-3の問題でややグレーな内容かもしれません...