まずは蝋の翼から。

学んだことを書きながら確認・整理するためのメモブログ。こういうことなのかな?といったことをふわっと書いたりしていますが、理解が浅いゆえに的はずれなことも多々あると思うのでツッコミ歓迎

tidyverseとNSE

tidyverse系で変数を使いたいときめんどくさいなぁという話。

はじめに要点をざっくりまとめると、tidyverse系でdplyrでdata$列名ではなく列名と直接書けるのはNSE表現になっているので、そういう場所はNSEに合わせた書き方しようね。変数はSEなのでそのままでは評価されないよ、という話。

以下で考える。

library(tidyverse)

df = as_tibble(iris)

 df %>% 
   filter(Species == 'versicolor') %>% 
   head()
 # # A tibble: 6 x 5
 # Sepal.Length Sepal.Width Petal.Length Petal.Width Species   
 # <dbl>       <dbl>        <dbl>       <dbl> <fct>     
 #   1          7           3.2          4.7         1.4 versicolor
 # 2          6.4         3.2          4.5         1.5 versicolor
 # 3          6.9         3.1          4.9         1.5 versicolor
 # 4          5.5         2.3          4           1.3 versicolor
 # 5          6.5         2.8          4.6         1.5 versicolor
 # 6          5.7         2.8          4.5         1.3 versicolor

でのfilter(Species == 'versicolor')のSpeciesを変数で指定する。

# 変数hoge
 hoge = 'Species'
 df %>% 
   filter(hoge == 'versicolor') %>% 
   head()
 # A tibble: 0 x 5
 # … with 5 variables: Sepal.Length <dbl>, Sepal.Width <dbl>, Petal.Length <dbl>,
 #   Petal.Width <dbl>, Species <fct>

このとき、変数hogeが評価されずに'hoge'列 が指定される。そんなものはないのでデータ抽出がされない。

 expr(df %>% 
   filter(hoge == 'versicolor') %>% 
   head())

# df %>% filter(hoge == "versicolor") %>% head()

dplyr, ggplot, formula引数の書き方はNSE(Non-Standard Evaluation)という、表現式(expression)を与えて処理させる方法を取っている。

dplyrでのNSE

dplyrの表現式は例えば、

data %>% mutate(y = x + 1)

というように、x + 1は列名を直接書いていて、表現式で表している。これをSE(Standard Evaluation)という、関数に表現式ではなく値を与える形の場合は

data %>% mutate_(y =' x + 1')

という書き方になる。なお、mutate_のようにdplyrのNSE関数の一部は_を入れることでSE関数になる(非推奨)。

NSE関数の表現式では、上記の例で言えばx + 1は表現式となっているのでxは変数(シンボル)xとして通常使うことはできない。
この表現式はtidyevalのQuosure(quote(表現を評価しないままに留める) + closure(評価される環境を覚えておく)の造語)という特徴が影響しているそうだ。

そのため、表現式内において与える値はQuosureとして書く必要がある。また、そのままでは変数を与えていても評価しないままに留めるため、Quosureで渡した上でquoteを外す処理を外すことでx変数xとして評価させることができる。

そのことを踏まえ、前述の以下の処理に戻ると、

# 変数hoge
 hoge = as.name('Species') #hogeをSpeciesというシンボルにする
 df %>% 
   filter(hoge == 'versicolor') %>% # hogeはQuosure
   head()

expr(df %>% 
  filter(hoge == 'versicolor') %>% # hogeはQuosure
  head()
)
# => df %>% filter(hoge == "versicolor") %>% head()

このfilter(hoge == 'versicolor')filter関数はNSE関数で、hogeはQuosure、つまりただの文字列hogeのため変数hogeとは別扱いになりhogeのまま扱われる。このQuosure内のhogeを変数としたい場合、hogeのquoteを外す!!を用いてこの部分は表現式のなかの文字列ではなく、変数だということ明示し評価対象とするとhogeは変数hogeとして評価される。

 hoge = as.name('Species') #hogeをSpeciesというシンボルにする
 df %>% 
   filter((!! hoge) == 'versicolor') %>% ## hogeというquoteとして渡したあと、!!でquoteを一時的に外して評価される
   head()
 # # A tibble: 6 x 5
 # Sepal.Length Sepal.Width Petal.Length Petal.Width Species   
 # <dbl>       <dbl>        <dbl>       <dbl> <fct>     
 #   1          7           3.2          4.7         1.4 versicolor
 # 2          6.4         3.2          4.5         1.5 versicolor
 # 3          6.9         3.1          4.9         1.5 versicolor
 # 4          5.5         2.3          4           1.3 versicolor
 # 5          6.5         2.8          4.6         1.5 versicolor
 # 6          5.7         2.8          4.5         1.3 versicolor

expr(df %>% 
  filter(!!hoge == 'versicolor') %>% # hogeはQuosure
  head()
)
# =>df %>% filter(Species == "versicolor") %>% head()

ちなみに、hogeをシンボルにしない場合は評価はされるがfilter((!! hoge) == 'versicolor')filter(Species == 'versicolor')ではなくfilter('Species' == 'versicolor')になるのでデータ抽出はできない。

ggplotとformulaでのNSE

ggplotではaes()内に与えた値は、data$列名ではなく、与えたdataの列名を直接指定するような表現式になっている。

#NSE
data %>% ggplot(aes(x = Sepal.Length, y = Sepal.Width))

# SE
data %>% ggplot(aes(x = .$Sepal.Length, y = .$Sepal.Width)) #動かない

formulaでも同様に、dataの列名を直接指定する表現式としてNSEになっている。

# NSE
data %>% lm(formula = Petal.Length ~ Petal.Width)

# SE
data %>% lm(formula = .$Petal.Length ~ .$Petal.Width) #動かない

自作関数の引数をNSEで渡す

自作関数の引数はシンボル(SE形式)なので一度NSE、つまり表現式に直さないといけない。そのため、表現式部分で変数を使いたい場合は一度enquo()でquosureに変換しないといけない。

df = as_tibble(iris)

fun_select = function(df,x){
  x = enquo(x)
  df %>% select(!!x) #df$Speciesの表現式になる
}

fun_select(df, Species)
fun_plot = function(df, x1, y1) {
  x1 = enquo(x1)
  y1 = enquo(y1)
  df %>% 
    ggplot(aes(x = (!!x1), y =(!!y1))) +
    geom_point()
}

fun_plot(df, Sepal.Length, Sepal.Width)

Quosureはtidyevalという仕組みによって成り立っているみたいだが、NSEで書くと対応できるということ以外あまりそのあたりわかってないので詰まったらまた何か書くかも。

ちなみに、以下の過去記事で表面的な解決をしたが、根本的にはこの仕組みが原因っぽい。

knknkn.hatenablog.com

参考

Programming with dplyr • dplyr

Rの関数定義でNSEを使う(表現式を引数にとれるようにする) | marketechlabo

Tidy Eval Meets ggplot2

↓このあたりのセルフ引用ツイート群 https://twitter.com/fronori/status/1002797245928955904