Rのdplyrパッケージでプログラミングするときの注意点 2021

 Rのdplyrパッケージを使っていて困ることのひとつに、非標準評価(NSE)をめぐるトラブルがある。いざ困ったときにすぐ調べられるように、以前、dplyrのvignettesのひとつ”Programming with dplyr“を通読してメモを取った
 ところがdplyrの仕様はあれこれ変わり、このvignetteの中身もすっかり変わってしまった。考えてみたら、前に目を通したのは2017年だもんな。あのころの私は若かった。[すいませんいまちょっと適当なことをいいました]

 というわけで、最新版のvignetteを読み直してメモをとった。以前はquo(), enquo(), quo_name()などが乱れ飛ぶややこしい話だったのだが、びっくりするくらいにシンプルな話に変わっている。すごい。

イントロダクション
 dplyrはtidy evaluationと呼ばれる非標準評価を用いる。tidy evaluationには data maskingとtidy selectionがある。

data masking
 たとえばdf %>% group_by(var1)というとき、環境の中にはvar1がなくても、var1はdfの列名(データ変数名)だと解釈されますよね。そういうののこと。arrange(), count(), filter(), group_by(), mutate(), summarize()で用いられる。
 以下では環境の中にある変数を環境変数、データフレームにある変数をデータ変数と呼ぶ。

 データ変数を(データ変数名を直接入力するのではなくて)環境変数から得ることをindirectionという。主に2つのケースがある。
 ケースその1、関数の引数のなかにデータ変数があるとき。dplyrの関数のなかで、データ変数名を{{ … }} で囲むとよい。たとえば

var_summary <- function(data, var){
  data %>% summarize(min = min({{ var }})) 
} 
mtcars %>% group_by(cyl) %>% var_summary(mpg)

[この例、前掲した2017年のメモでは、関数に裸の変数名 var を渡すのではなく quo(var) として渡して関数のなかで !!var としてアンクオートするか、裸の関数名 var を渡して関数のなかで一旦 var <- enquo(var) としてから!!var としてアンクオートせよ、と書いてあった。ずいぶん簡単な話になりましたねえ]

 ケースその2, 環境変数が文字列としてデータ変数名を持っているとき。.data[[…]]を使う。

for (var in names(mtcars)) {
  mtcars %>% count(.data[[var]]) %>% print()
}

tidy selection
 たとえばselect(df, starts_with(“a”))とか書けるじゃないですか。こういう変数選択は、across(), relocate(), rename(), select(), pull()でできる。裏ではtidyselect パッケージが動いている。詳細は ?dplyr_tidy_selectをみなさい。

 さて、ここでもindirectionという問題が生じる。
 ケースその1、関数の引数のなかにデータ変数があるとき。このときもdplyrの関数の中で、データ変数を{{ … }}で囲むとよい。

summarise_mean <- function(data, vars){
  data %>% summarize(accross({{ vars }}, mean)
} 
mtcars %>% group_by(cyl) %>% summarise_mean(where(is.numeric))

 ケースその2, 環境変数が文字列としてデータ変数名を持っているとき。これは簡単で、all_of(), any_of()を使えばよろしい。

How to
[7つのコツが紹介されている。2項目のみメモする。あとのは、うんまあそうなんだろうなという話なので省略]

  • パッケージを作っているとき、dplyrでデータ変数名を裸で使っているとcheckの際にNOTESが出てうざい → @ importFrom rlang .data と宣言しておき、データ変数名の前にいちいち.dataと書け [これが面倒くさくて困ってるんですけど…]
  • 関数の中で新しく作る変数名を引数として渡したい → = の代わりに:=を使え。左辺では” “でくくったなかで{{…}}が使える。例:
    my_summarize <- function(data, expr) {
      data %>% summarize(
        "mean_{{expr}}" := mean({{ expr }})
      )
    }