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で書くと対応できるということ以外あまりそのあたりわかってないので詰まったらまた何か書くかも。
ちなみに、以下の過去記事で表面的な解決をしたが、根本的にはこの仕組みが原因っぽい。
参考
Programming with dplyr • dplyr
Rの関数定義でNSEを使う(表現式を引数にとれるようにする) | marketechlabo
↓このあたりのセルフ引用ツイート群 https://twitter.com/fronori/status/1002797245928955904
冒頭の疑問に戻って、Rで関数を作るときにexpressionを変数として認識(評価)させるtidyなやり方(tidyeval)は表現と環境をセットで表せるquosuresを用いる。 Quineのquasiquotationの概念が紹介され、実装される。まさに、意味深長。 https://t.co/jGJAtnJDO8 さっきの.dotsもpronounの一種だろう。
— Tetsuo Ishikawa (@fronori) 2018年6月2日