前回はゴリゴリのRプログラマーではない人に向けたtidyeval解説記事を書いてみました。
今回は具体的な例も交えつつ、tidyevalの理解を試みます!🧑🎓 「こんな感じに分類できそうかな?」という感じでパターン別に解説していきます。
前回同様、ほとんどの人はtidyevalの全てを理解するする必要はないし、使うところだけならそんなに理解は難しくないとという方針で進めたいと思います。
前回記事ではtidyevalで行われることのイメージを説明していますので、まだの方はそちらと併せて読んでいただけると理解しやすいと思います😃
この記事ではdplyrに付属するstarwarsデータセットおよび、rlangパッケージのtidyeval APIを使用しています。
ℹ️パッケージ読み込みをお忘れ無くℹ️
library(dplyr) library(rlang)
tidyevalは大別すると 単一の変数を受け渡してNSEするか、複数の変数をNSEするかに分かれます。
この章では単一変数のケースをさらに以下のパターンに分けます。
手っ取り早く使い方だけ知りたい人は {{}}(カーリーカーリーブラケット)を使う方法だけでも押さえておきましょう。
原理を知りたい人向けに補足情報も書いてますので、ご参考まで!
{{}}
enquo()
& !!
{{}}
:={{}}
enexpr()
& !!
enquo()
& !!
ensym()
& !!
おそらく、これが最も使うパターンだと思います😐
説明の都合上、ここでは最も一般的に使うであろう{{}}よりも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するとvar
がname
になる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では {{}}の内側にスペースを入れることが推奨されています。 これは”特別な挙動”であることを明示するためだそうです。
{{}}
は様々な操作に対応しており、ややこしい!!
などの手間から解放されます。しかし、一貫した機能を担っているというよりは複数の機能を一つの{{}}
にねじ込んだような感じになっています🛠️ 最低限な理解で済ませたい人は{{}}
だけ知っておけば良いと思いますが、原理から理解したい方はむしろenquo()や!!を使った方法を必ず理解しておきましましょう。
ここで言う関数の引数名というのは、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)
正確に言うと、:=
が使用できるのはdplyr関数等で引数が...
として定義されている場合のみです。この場合の...
はdynamic dotsというもので、rlangで上書きされた機能です。baseRの可変長引数と全く同じものではないので注意です。
{{}}
を活用する先ほどの普通の変数を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_!"
filter関数などでは、これまでのように単一の変数を与えるのではなく、species == "Human"
などというようなexpression[ref]より具体的にはcallクラスオブジェクトです。前回記事のexpressionを参照。[/ref]を受け渡しすることになります。
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 }})
}
## エラー: 予想外の '}' です ( "}" の)
{{}}
(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の実際なのではないでしょうか🤔
この辺の機能割り当てについては、ん?と思う部分がありますが、深く考えないことにしておきます。
これまでは自作関数の引数にクォーテーションマークなし[ref]bareとも言います。クォーテーションマークなしで「裸」からbareです。[/ref]で値を入れていましたが、文字列を渡してうまく処理する方法もあります。
クォーテーションなしの変数を関数間でやりとりする場合は いつ評価されるかとビクビクしながら使うくらいなら 、文字列として処理した方がやりやすいケースもありそうですね。
この場合には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()
を使う、と理解しています。
上記の理由から、自作関数以外の場でtidyevalの動作を知ろうとして`enquo()`などを使っても意図した挙動にはなりません。
別の説明としては、`enquo()`はbaseRの`substitute()`に相当し、`quo()`はbaseRの`quote()`に対応します。前者は与えられた変数を一度評価してからquosureにするのに対して、後者は書かれたとおりのquosureを作ります。
これまでに単一の変数/expressionを受け渡しする方法を紹介してきましたが、これが複数になると処理の仕方がまるっきり変わります。
...
(可変長引数、dynamic-dots)で済むパターンなら楽ですが、込み入った場合になるとちょっと手間が増え、コードの可読性も若干下がるかも。
今回は以下の2パターンだけご紹介します。
単純に複数の変数を受け取って、そのまま単一の関数に投げるのは簡単です。これまでの操作が大変だっただけに、「逆にこれでいいの?」と心配になるレベルです。
やることは簡単。可変長引数の...
を使うだけです。{{}}
も、!!
も必要ありません。
...
を使って引数をそのまま渡す方法が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
...
には引数名を与えることは出来ないため、どうしても引数位置(n番目引数)に頼った引数指定になってしまう点には注意です⚠️
関数設計の観点からすると、docstringが無い限りは使いにくい仕様になってしまうかもですね。
さっきの...
は結構便利で、引数名=値
のセットを渡すことができます。
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))
さて、以上が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
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の原理についてはイメージ理解するための記事を書いてますので、そちらの前回記事も参考にしてみてください。
https://excel2rlang.com/rlang-tidyeval-abstract/
情報の網羅性を高めるために複数の変数をtidyevalさせる方法なんかについても紹介しましたが、どうしても一貫した関数設計がやりにくく、再利用性の高い関数を作るのは難しそうです。 tidyevalデザインパターンみたいなものを誰か考えてくれないかな・・・
では今回はこんなところで終わりにしたいと思います!
最近は初心者向けコンテンツを作ってくださる神が多いので、次回以降はマニアックな話題にしようかな~😇
最も詳しいのはAdvandced R 2nd editionです。邦訳版がないのはつらい・・・。
Advanced Rはインターネット上でも公開されています。Metaprogramming章がその内容です。
https://adv-r.hadley.nz/metaprogramming.html
rlangパッケージのオフィシャルドキュメントはある程度参考にはなりますが、情報の網羅性は低いため、他のAdvanced Rと一緒に読むと良いと思います。
https://rlang.r-lib.org/index.html
昔のtidy evaluation説明資料は一部情報が古いですが、説明が今より分かりやすい部分もあります。
https://tidyeval.tidyverse.org/
https://notchained.hatenablog.com/entry/2021/01/20/222608
https://www.tidyverse.org/blog/2018/07/ggplot2-tidy-evaluation/