[R雑記] rlangのtidyeval 使用例とともに徹底解剖する

雑記

はじめに

前回はゴリゴリのRプログラマーではない人に向けたtidyeval解説記事を書いてみました。

今回は具体的な例も交えつつ、tidyevalの理解を試みます!🧑‍🎓 「こんな感じに分類できそうかな?」という感じでパターン別に解説していきます

前回同様、ほとんどの人はtidyevalの全てを理解するする必要はないし、使うところだけならそんなに理解は難しくないとという方針で進めたいと思います。

前回記事ではtidyevalで行われることのイメージを説明していますので、まだの方はそちらと併せて読んでいただけると理解しやすいと思います😃

[R雑記] rlangのtidyeval ユーザー目線で使うとこだけ理解する
はじめに tidyevalは「コレ」のせいで難しいR言語をそこそこ勉強してる人なら分かると思うのですが、存在は知っていても理解がめちゃくちゃ難しいのがrlangパッケージのtidyevalです。本気でtidyevalを学びたければH...
注意

この記事ではdplyrに付属するstarwarsデータセットおよび、rlangパッケージのtidyeval APIを使用しています。

ℹ️パッケージ読み込みをお忘れ無くℹ️

    
library(dplyr)
library(rlang)
    

📗tidyevalを使うパターン(変数が一つの場合)

tidyevalは大別すると 単一の変数を受け渡してNSEするか、複数の変数をNSEするかに分かれます。

この章では単一変数のケースをさらに以下のパターンに分けます

手っ取り早く使い方だけ知りたい人は {{}}(カーリーカーリーブラケット)を使う方法だけでも押さえておきましょう

原理を知りたい人向けに補足情報も書いてますので、ご参考まで!

  • symbolを受け取り、unquote(!!)してから関数に渡す
    • {{}}
    • enquo() & !!
  • symbolを受け取り、unquote(!!)してから関数の引数名にする
    • {{}} :=
  • expressionを受け取り、unquote(!!)してから関数に渡す
    • {{}}
    • enexpr() & !!
    • enquo() & !!
  • stringを受け取り、symbolに変換した後、unquote(!!)してから関数に渡す
    • ensym() & !!

📗symbol🌟を受け取り、unquote(!!)してから関数に渡す

おそらく、これが最も使うパターンだと思います😐

説明の都合上、ここでは最も一般的に使うであろう{{}}よりもenquo()を使う方法を先に示します

myfunction <- function(df, var) {
    var_quo <- enquo(var)
    # print(var_quo)
    # <quosure>
    # expr: ^name
    # env:  global
    df |>
        select(!!var_quo)
}
myfunction(starwars, name) # `name`をsymbolとして与える
出力結果

# # A tibble: 87 × 1
#    name              
#                 
#  1 Luke Skywalker    
#  2 C-3PO             
#  3 R2-D2             
#  4 Darth Vader       
#  5 Leia Organa       
#  6 Owen Lars         
#  7 Beru Whitesun lars
#  8 R5-D4             
#  9 Biggs Darklighter 
# 10 Obi-Wan Kenobi    
# # … with 77 more rows

ここで行われている操作を詳しく説明すると以下。

  • nameをsymbolとしてvar引数に与える
  • enquo()でquosure化
  • !!でunquoteするとvarnameになる
  • starwars |> select(name)が評価される

dplyr::select()dplyr::group_by()などはtibbleの文脈で変数を評価することで、NSEが成立します。

つまり、quosureの形に一旦変換しておいて、select()関数の中で評価を受ける直前にunquoteする必要があるわけですね。

{{}}を使っても全く同じ

{{}}は「enquo()してから!!する」という一連の流れ(!!enquo(var))を短縮したものです。実際のところは他の操作にも対応しているので、魔法みたいな操作🪄になっています。

普通tidyevalを利用したコードを書く際には、{{}}を使って書くのが一般的だと思います。

なので、これをtidyverseの重要パターンその1としておきます👊

myfunction <- function(df, var) {
    df |>
        select({{ var }})
}
myfunction(starwars, name)
出力結果

# # A tibble: 87 × 1
#    name              
#                 
#  1 Luke Skywalker    
#  2 C-3PO             
#  3 R2-D2             
#  4 Darth Vader       
#  5 Leia Organa       
#  6 Owen Lars         
#  7 Beru Whitesun lars
#  8 R5-D4             
#  9 Biggs Darklighter 
# 10 Obi-Wan Kenobi    
# # … with 77 more rows

The tidyverse style guideでは {{}}の内側にスペースを入れることが推奨されています。 これは”特別な挙動”であることを明示するためだそうです。

Info

{{}}は様々な操作に対応しており、ややこしい!!などの手間から解放されます。しかし、一貫した機能を担っているというよりは複数の機能を一つの{{}}にねじ込んだような感じになっています🛠️

最低限な理解で済ませたい人は{{}}だけ知っておけば良いと思いますが、原理から理解したい方はむしろenquo()や!!を使った方法を必ず理解しておきましましょう。

📗symbol🌟を受け取り、unquote(!!)してから関数の引数名にする

ここで言う関数の引数名というのは、dplyr::mutate()dplyr::transmute()などで新しい列名 =とする時の=の左側です。quosureを関数の引数名のなかでtidyevalするのは代入演算子の機能上不可能だったため、rlangで用意された:=演算子を使うことになります

myfunction <- function(df, var) {
    quo_var <- enquo(var)
    df |>
        mutate(!!quo_var := name, .before = name)
}
myfunction(starwars, newname)

出力結果

# # A tibble: 87 × 15
#    newname      name  height  mass hair_color skin_color
#                           
#  1 Luke Skywal… Luke…    172    77 blond      fair      
#  2 C-3PO        C-3PO    167    75 NA         gold      
#  3 R2-D2        R2-D2     96    32 NA         white, bl…
#  4 Darth Vader  Dart…    202   136 none       white     
#  5 Leia Organa  Leia…    150    49 brown      light     
#  6 Owen Lars    Owen…    178   120 brown, gr… light     
#  7 Beru Whites… Beru…    165    75 brown      light     
#  8 R5-D4        R5-D4     97    32 NA         white, red
#  9 Biggs Darkl… Bigg…    183    84 black      light     
# 10 Obi-Wan Ken… Obi-…    182    77 auburn, w… fair      
# # … with 77 more rows, and 9 more variables:
# #   eye_color , birth_year , sex ,
# #   gender , homeworld , species ,
# #   films , vehicles , starships 

私もtidyevalをよく分かってなかった頃:=は一体どういうときに使うのかサッパリでしたが、:=は引数名でunquoteできない問題を解決するための=代替品と認識しておくと良いでしょう。
極論、以下のようにunquoteを使わない場合でも普通に:=を使うことはできます。

starwars |>
    mutate(newcol := name, .before = name)

starwars |>
    mutate(newcol := height * mass, .before = name)
Info

正確に言うと、:=が使用できるのはdplyr関数等で引数が...として定義されている場合のみです。この場合の...はdynamic dotsというもので、rlangで上書きされた機能です。baseRの可変長引数と全く同じものではないので注意です。

引数名のtidyevalでも{{}}を活用する

先ほどの普通の変数をtidyevalする際と全く同様、{{}}enquo()の手間を省くことができます。

引数名に{{}}を使い、:=演算子を使うパターンは王道だと思うので、これをtidyevalの最重要パターンその2としておきます

myfunction <- function(df, var) {
    df |>
        mutate({{ var }} := name, .before = name)
}
myfunction(starwars, newname)
出力結果

# # A tibble: 87 × 15
#    newname      name  height  mass hair_color skin_color
#                           
#  1 Luke Skywal… Luke…    172    77 blond      fair      
#  2 C-3PO        C-3PO    167    75 NA         gold      
#  3 R2-D2        R2-D2     96    32 NA         white, bl…
#  4 Darth Vader  Dart…    202   136 none       white     
#  5 Leia Organa  Leia…    150    49 brown      light     
#  6 Owen Lars    Owen…    178   120 brown, gr… light     
#  7 Beru Whites… Beru…    165    75 brown      light     
#  8 R5-D4        R5-D4     97    32 NA         white, red
#  9 Biggs Darkl… Bigg…    183    84 black      light     
# 10 Obi-Wan Ken… Obi-…    182    77 auburn, w… fair      
# # … with 77 more rows, and 9 more variables:
# #   eye_color , birth_year , sex ,
# #   gender , homeworld , species ,
# #   films , vehicles , starships 

この場合の{{}}にはちょっとオシャレな使い方ができます📿 glueパッケージと同じような記述法で引数名を定義できるのです。

投稿後追記: glue記法を使う際には"で囲んで文字列化する必要がありました。(@yutannihilation さん即リプありがとうございました🙏)

myfunction <- function(df, var) {
    df |>
        mutate( "meter_{{ var }}" := {{ var }} / 10 ,
        .before = height)
}
myfunction(starwars, height)
出力結果

# # A tibble: 87 × 15
#    name  meter_height height  mass hair_color skin_color
#                           
#  1 Luke…         17.2    172    77 blond      fair      
#  2 C-3PO         16.7    167    75 NA         gold      
#  3 R2-D2          9.6     96    32 NA         white, bl…
#  4 Dart…         20.2    202   136 none       white     
#  5 Leia…         15      150    49 brown      light     
#  6 Owen…         17.8    178   120 brown, gr… light     
#  7 Beru…         16.5    165    75 brown      light     
#  8 R5-D4          9.7     97    32 NA         white, red
#  9 Bigg…         18.3    183    84 black      light     
# 10 Obi-…         18.2    182    77 auburn, w… fair      
# # … with 77 more rows, and 9 more variables:
# #   eye_color , birth_year , sex ,
# #   gender , homeworld , species ,
# #   films , vehicles , starships 

{{}}を使わない方法(!!)ではglue記法的には書けないので、注意が必要です。

myfunction2 <- function(df, var) {
    quo_var <- enquo(var)
    df |>
        mutate( meter_!!var  := {{ var }} / 10 ,
        .before = height)
}
# エラー:   予想外の '!' です  以下の部分: 
#  "            df |> 
#                      mutate( meter_!" 

📗expression➗を受け取り、unquote(!!)してから関数に渡す

filter関数などでは、これまでのように単一の変数を与えるのではなく、species == "Human"などというようなexpression[1]より具体的にはcallクラスオブジェクトです。前回記事のexpressionを参照。を受け渡しすることになります。

expressionの操作なので、enexpr()を使ってexpressionにしてから評価直前にunquoteします。

my_func <- function(df, var) {
    quo_var <- enexpr(var)
    df |>
        filter(!!quo_var)
}
my_func(starwars, species == "Human" & height > 150)
出力結果

# # A tibble: 29 × 14
#    name  height  mass hair_color skin_color eye_color birth_year
#                              
#  1 Luke…    172    77 blond      fair       blue            19  
#  2 Dart…    202   136 none       white      yellow          41.9
#  3 Owen…    178   120 brown, gr… light      blue            52  
#  4 Beru…    165    75 brown      light      blue            47  
#  5 Bigg…    183    84 black      light      brown           24  
#  6 Obi-…    182    77 auburn, w… fair       blue-gray       57  
#  7 Anak…    188    84 blond      fair       blue            41.9
#  8 Wilh…    180    NA auburn, g… fair       blue            64  
#  9 Han …    180    80 brown      fair       brown           29  
# 10 Wedg…    170    77 brown      fair       hazel           21  
# # … with 19 more rows, and 7 more variables: sex ,
# #   gender , homeworld , species , films ,
# #   vehicles , starships 
しかし、{{}}はexpressionに対応していないのか、filter関数ではspecies == "Human"のようなexpressionを渡すことができません。
myfunc <- funciton(df, var) {
    df |>
        dplyr::filter({{ var }})
}
## エラー:   予想外の '}' です  ( "}" の) 
Info

{{}}(cruly-curly bracket)の挙動を詳細に説明した文書が見当たらず、結局なんでこういう仕様なのかは分かりませんでした🤔。しかし、StackoverflowでのLionelさんのコメントによれば「関数内でしか機能しない」性質があるのと、Yutaniさんのブログ によれば「引数をそのまま渡すケースだけで使える」そうです。

正確な説明を知ってる方がいらっしゃれば教えてください!

{{}}は単一の変数ならうまく処理できますので、以下のように「不等式の変数だけを渡す」という回避策があります。

my_func <- function(df, var1, var2) {
    df |>
        filter({{ var1 }} == "Human" & {{ var2 }} > 150)
}
my_func(starwars, species, height)
出力結果

# # A tibble: 29 × 14
#    name  height  mass hair_color skin_color eye_color birth_year
#                              
#  1 Luke…    172    77 blond      fair       blue            19  
#  2 Dart…    202   136 none       white      yellow          41.9
#  3 Owen…    178   120 brown, gr… light      blue            52  
#  4 Beru…    165    75 brown      light      blue            47  
#  5 Bigg…    183    84 black      light      brown           24  
#  6 Obi-…    182    77 auburn, w… fair       blue-gray       57  
#  7 Anak…    188    84 blond      fair       blue            41.9
#  8 Wilh…    180    NA auburn, g… fair       blue            64  
#  9 Han …    180    80 brown      fair       brown           29  
# 10 Wedg…    170    77 brown      fair       hazel           21  
# # … with 19 more rows, and 7 more variables: sex ,
# #   gender , homeworld , species , films ,
# #   vehicles , starships 

filter()はquosureでも可

これまたややこしい話🙃ですが、species == "Human" & height > 150みたいなexpressionはquosureとして与えても大丈夫らしく、enexpr()ではなくenquo()でも同じ挙動になります。

my_func <- function(df, var) {
    quo_var <- enquo(var)
    df |>
        filter(!!quo_var)
}
my_func(starwars, species == "Human" & height > 150)
出力結果

# # A tibble: 29 × 14
#    name  height  mass hair_color skin_color eye_color birth_year
#                              
#  1 Luke…    172    77 blond      fair       blue            19  
#  2 Dart…    202   136 none       white      yellow          41.9
#  3 Owen…    178   120 brown, gr… light      blue            52  
#  4 Beru…    165    75 brown      light      blue            47  
#  5 Bigg…    183    84 black      light      brown           24  
#  6 Obi-…    182    77 auburn, w… fair       blue-gray       57  
#  7 Anak…    188    84 blond      fair       blue            41.9
#  8 Wilh…    180    NA auburn, g… fair       blue            64  
#  9 Han …    180    80 brown      fair       brown           29  
# 10 Wedg…    170    77 brown      fair       hazel           21  
# # … with 19 more rows, and 7 more variables: sex ,
# #   gender , homeworld , species , films ,
# #   vehicles , starships 

どの例でもdata-mask情報が付与されるのは評価の直前であるため、tidyevalする側のfilter()関数がよしなにしている、というのがdplyr関数におけるtidyevalの実際なのではないでしょうか🤔

この辺の機能割り当てについては、ん?と思う部分がありますが、深く考えないことにしておきます。

📗string🔤を受け取り、symbolにしてからunquoteして関数に与える

これまでは自作関数の引数にクォーテーションマークなし[2]bareとも言います。クォーテーションマークなしで「裸」からbareです。で値を入れていましたが、文字列を渡してうまく処理する方法もあります。

クォーテーションなしの変数を関数間でやりとりする場合は いつ評価されるかとビクビクしながら使うくらいなら 、文字列として処理した方がやりやすいケースもありそうですね。

この場合にはenquo()ではなくensym()を使用します。ensym()"を外して`で囲むような操作を行っています。「文字列がsymbolになるのでensym()」と覚えます。

my_func <- function(df, string_var) {
    var <- ensym(string_var)
    df |>
        select(!!var)
}
my_func(starwars, "name")
出力結果

# # A tibble: 87 × 1
#    name              
#                 
#  1 Luke Skywalker    
#  2 C-3PO             
#  3 R2-D2             
#  4 Darth Vader       
#  5 Leia Organa       
#  6 Owen Lars         
#  7 Beru Whitesun lars
#  8 R5-D4             
#  9 Biggs Darklighter 
# 10 Obi-Wan Kenobi    
# # … with 77 more rows
my_func <- function(df, string_var) {
    var <- ensym(string_var)
    df |>
        group_by(!!var) |>
        summarise(mean(height))
}
my_func(starwars, "species")
出力結果

# # A tibble: 38 × 2
#    species   `mean(height)`
#                  
#  1 Aleena               79 
#  2 Besalisk            198 
#  3 Cerean              198 
#  4 Chagrian            196 
#  5 Clawdite            168 
#  6 Droid                NA 
#  7 Dug                 112 
#  8 Ewok                 88 
#  9 Geonosian           183 
# 10 Gungan              209.
# # … with 28 more rows

関数内部で文字列を処理する場合は違う方法をとる

自作関数の引数ではなく、関数内で生成した文字列を使用する場合はensym()ではなくsym()を使います。

myfunc <- function(df) {
    var <- sym("species")
    df |>
        group_by(!!var) |>
        summarise(mean(height))
}
myfunc(starwars)
出力結果

# # A tibble: 38 × 2
#    species   `mean(height)`
#                  
#  1 Aleena               79 
#  2 Besalisk            198 
#  3 Cerean              198 
#  4 Chagrian            196 
#  5 Clawdite            168 
#  6 Droid                NA 
#  7 Dug                 112 
#  8 Ewok                 88 
#  9 Geonosian           183 
# 10 Gungan              209.
# # … with 28 more rows

ここでsym()という新しい関数を登場させたのですが、ensym()sym()というよく似た二つの関数について詳しく考えてみます。

評価の流れを列挙してみると、

  • [関数引数を使う場合] string_var"species"species
  • [関数内の文字列を使う場合] "species"species

という風に、ワンステップ違います💡 私はざっくりイメージ理解程度に考えていますが、ensym()enquo()といった”en”がつくモノは自作関数の引数を通す場合に使って、引数を使わない場合にはsym()quo()を使う、と理解しています。

Info

上記の理由から、自作関数以外の場でtidyevalの動作を知ろうとして`enquo()`などを使っても意図した挙動にはなりません。

別の説明としては、`enquo()`はbaseRの`substitute()`に相当し、`quo()`はbaseRの`quote()`に対応します。前者は与えられた変数を一度評価してからquosureにするのに対して、後者は書かれたとおりのquosureを作ります。

📚tidyevalを使うパターン(変数が二つ以上の場合)

これまでに単一の変数/expressionを受け渡しする方法を紹介してきましたが、これが複数になると処理の仕方がまるっきり変わります

...(可変長引数、dynamic-dots)で済むパターンなら楽ですが、込み入った場合になるとちょっと手間が増え、コードの可読性も若干下がるかも。

今回は以下の2パターンだけご紹介します。

  • 複数のsymbolを受け取り、単一のdplyr関数に渡す
  • 複数のexpressionを受け取り、単一の関数に渡す
  • 複数のexpressionを受け取り、複数の関数に渡す

📚複数のsymbol🌟を受け取り、単一のdplyr関数に渡す

単純に複数の変数を受け取って、そのまま単一の関数に投げるのは簡単です。これまでの操作が大変だっただけに、「逆にこれでいいの?」と心配になるレベルです。

やることは簡単。可変長引数の...を使うだけです。{{}}も、!!も必要ありません。

...を使って引数をそのまま渡す方法がtidyevalの重要パターンその3です。
myfunc <- function(df, ...) {
    df |>
        select(...)
}

myfunc(starwars, name, height, mass)
出力結果

# # A tibble: 87 × 3
#    name               height  mass
#                    
#  1 Luke Skywalker        172    77
#  2 C-3PO                 167    75
#  3 R2-D2                  96    32
#  4 Darth Vader           202   136
#  5 Leia Organa           150    49
#  6 Owen Lars             178   120
#  7 Beru Whitesun lars    165    75
#  8 R5-D4                  97    32
#  9 Biggs Darklighter     183    84
# 10 Obi-Wan Kenobi        182    77
# # … with 77 more rows

また、引数が一つだけの場合などのシンプルなユースケースでは{{}}の代替として使用するのもアリかもしれません。

myfunc <- function(df, ...) {
    df |>
        group_by(...)
}
myfunc(starwars, species) |>
    summarise(mean(height))
出力結果

## A tibble: 38 × 2
#   species   `mean(height)`
#                 
# 1 Aleena               79 
# 2 Besalisk            198 
# 3 Cerean              198 
# 4 Chagrian            196 
# 5 Clawdite            168 
# 6 Droid                NA 
# 7 Dug                 112 
# 8 Ewok                 88 
# 9 Geonosian           183 
#10 Gungan              209.
## … with 28 more rows
Info

...には引数名を与えることは出来ないため、どうしても引数位置(n番目引数)に頼った引数指定になってしまう点には注意です⚠️

関数設計の観点からすると、docstringが無い限りは使いにくい仕様になってしまうかもですね。

📚複数のexpression➗を受け取り、単一の関数に渡す

さっきの...は結構便利で、引数名=値のセットを渡すことができます。

myfunc <- function(df, ...) {
    df |>
        group_by(species, sex) |>
        summarise(...)
}
myfunc(
    starwars,
    mean_height = mean(height),
    max_height = max(height),
    min_height = min(height)
    )
出力結果

# `summarise()` has grouped output by 'species'. You can override
# using the `.groups` argument.
# # A tibble: 41 × 5
# # Groups:   species [38]
#    species   sex    mean_height max_height min_height
#                             
#  1 Aleena    male           79          79         79
#  2 Besalisk  male          198         198        198
#  3 Cerean    male          198         198        198
#  4 Chagrian  male          196         196        196
#  5 Clawdite  female        168         168        168
#  6 Droid     none           NA          NA         NA
#  7 Dug       male          112         112        112
#  8 Ewok      male           88          88         88
#  9 Geonosian male          183         183        183
# 10 Gungan    male          209.        224        196
# # … with 31 more rows

なんだかんだ言ってmutate()summarise()ぐらいでしか使う機会はないかもしれませんが😇

引数名=値はexprsから展開することができる

以下は数少ない!!!の活躍機会です。自作関数の引数のために引数名=値を大量に用意する必要があるなら、expressionのリストとして用意することが可能です。

とは言えシンプルにリストを作ったのでは値の評価が始まってしまうため、exprs()でquoteしてやる必要があります。

myfunc <- function(df, attrs) {
    df |>
        group_by(species, sex) |>
        summarise(!!!attrs)
}

attrs_exprs <- exprs(
    mean_height = mean(height),
    max_height = max(height),
    min_height = min(height)
)
myfunc(starwars, attrs_exprs)
出力結果

# `summarise()` has grouped output by 'species'. You can override
# using the `.groups` argument.
# # A tibble: 41 × 5
# # Groups:   species [38]
#    species   sex    mean_height max_height min_height
#                             
#  1 Aleena    male           79          79         79
#  2 Besalisk  male          198         198        198
#  3 Cerean    male          198         198        198
#  4 Chagrian  male          196         196        196
#  5 Clawdite  female        168         168        168
#  6 Droid     none           NA          NA         NA
#  7 Dug       male          112         112        112
#  8 Ewok      male           88          88         88
#  9 Geonosian male          183         183        183
# 10 Gungan    male          209.        224        196
# # … with 31 more rows

exprs()で作られるリストですが、以下と同じと考えるとわかりやすいでしょうか? 引数名とexpression化された値の組み合わせと全く同じモノができます

attrs <- list(
    mean_height = expr(mean(height)),
    max_height = expr(max(height)),
    min_height = expr(min(height))
)

myfunc(starwars, attrs)

こちら理解の助けにはなりますが、exprs()でリスト化する方が楽ですね😅

📚複数の引数=値セット➗を受け取り、別々の関数に渡す

先ほどのexprs()を複数回使うだけです。

重要度はそこまで高くないですが、汎用性が高いのでtidyevalパターンその4にしときます。
myfunc <- function(df, vars1, vars2) {
       df |>
        group_by(!!!vars1) |>
        summarise(!!!vars2)
}
myfunc(starwars, 
    exprs(species, sex),
    exprs(
        newcol = mean(height),
        secondcol = height / mass))

📊(蛇足)ggplot2も大概これまでの理屈が通じます

さて、以上がtidyevalのパターン集ですが、どれもこれもdplyr関数での説明に終始していました。

一応ggplot2での場合も示しておこうと思いますが、ggplot2ではほとんどが{{}}で解決すると思うので、ちょっとだけ例を示す程度にしようと思います。

aes()内に投げる

エステティックパラメーターに変数を入れる際は{{}}でOK。

myfunc <- function(df, xvar, yvar) {
    df |>
        ggplot(aes(x = {{ xvar }}, y = {{ yvar }}))+
        geom_point()
}
myfunc(starwars, height, mass) # OK
myfunc(starwars, height/mass, mass) # OK 

reorder()等の計算処理も対応しています。

myfunc <- function(df, xvar, yvar) {
    df |>
        ggplot(aes(x = {{ xvar }}, y = {{ yvar }}))+
        geom_point()
}                                           
myfunc(starwars, reorder(name, height), height) # OK

ちなみに👆 tidyevalではないのですが、mappingパラメーターをそのまま投げることもできます。(今調べて知った)

myfunc <- function(df, mapping) {
    df |>
        ggplot(mapping)+
        geom_point()
}                                           
myfunc(starwars, aes(x = reorder(name, height), y = height))

facet_wrap()内に投げる

facet_wrap()facet_grid()などにも対応しています。

~から始まる無名関数形式ではうまくtidyevalされないので、その点注意です⚠️

myfunc <- function(df, var) {
    df |>
        ggplot(aes(x = name, y = height)) +
        geom_point() +
        facet_wrap(vars({{ var }}))
}                                           
myfunc(starwars, species) # OK

複雑なggplotを作ってみる

library(ggforce)
library(gghighlight)

ggstarwars <- function(data, x, y, zoom_var, highlight) {
    data |>
        ggplot(aes({{ x }}, {{ y }})) +
        geom_point() +
        gghighlight({{ highlight }}) +
        facet_zoom(x = {{ zoom_var }})
}

ggstarwars(starwars, mass, height, species == "Human", height > 150 & height < 200)

総まとめ

ということで🤚 とても長いエントリになりましたが、tidyevalの使いそうなパターン集をまとめてみました!

改めて様々なパターンを見てみると、なんだかんだ言って、ほとんどの場合{{}}を使えば解決するということが分かりました💡

勝手に「重要パターン」と名付けた以下の4パターンは活用できるようになった方が良いですね👍

ただquosureやexpressionにもサイレント対応していたりと、その機能の実態はなかなかつかみ所がなく、今後tidyevalを失敗せずに使いたい人は原理も含めて理解した方が良いと強く思いました💪

tidyevalの原理についてはイメージ理解するための記事を書いてますので、そちらの前回記事も参考にしてみてください。

[R雑記] rlangのtidyeval ユーザー目線で使うとこだけ理解する
はじめに tidyevalは「コレ」のせいで難しいR言語をそこそこ勉強してる人なら分かると思うのですが、存在は知っていても理解がめちゃくちゃ難しいのがrlangパッケージのtidyevalです。本気でtidyevalを学びたければH...

情報の網羅性を高めるために複数の変数をtidyevalさせる方法なんかについても紹介しましたが、どうしても一貫した関数設計がやりにくく、再利用性の高い関数を作るのは難しそうです。 tidyevalデザインパターンみたいなものを誰か考えてくれないかな・・・

では今回はこんなところで終わりにしたいと思います!

最近は初心者向けコンテンツを作ってくださる神が多いので、次回以降はマニアックな話題にしようかな~😇

参考資料

公式資料

最も詳しいのはAdvandced R 2nd editionです。邦訳版がないのはつらい・・・。

Advanced R, Second Edition (Chapman & Hall/CRC The R Series) (English Edition)
Advanced R, Second Edition (Chapman & Hall/CRC The R Series) (English Edition) by Wickham, Hadley. Download it once and read it on your Kindle device, PC, phon...

Advanced Rはインターネット上でも公開されています。Metaprogramming章がその内容です。

Introduction | Advanced R

rlangパッケージのオフィシャルドキュメントはある程度参考にはなりますが、情報の網羅性は低いため、他のAdvanced Rと一緒に読むと良いと思います。

Functions for Base Types and Core R and Tidyverse Features
A toolbox for working with base types, core R features like the condition system, and core Tidyverse features like tidy evaluation.

昔のtidy evaluation説明資料は一部情報が古いですが、説明が今より分かりやすい部分もあります。

Tidy evaluation
The primary goal of this book is to get you up to speed with tidy evaluation and how to write functions around tidyverse pipelines and grammars.

ggplot2とtidyeval

Tidy evaluation in ggplot2 - Tidyverse
Using tidy evaluation in ggplot2 3.0.0.

脚注

脚注
1 より具体的にはcallクラスオブジェクトです。前回記事のexpressionを参照。
2 bareとも言います。クォーテーションマークなしで「裸」からbareです。

コメント

タイトルとURLをコピーしました