rlangと戯れて気が付いたこやつとの私的正しい付き合い方

掲題の件、Tidy Evalというかrlangパッケージを、下記の記事をはじめいろいろ使いこなそうと試行錯誤してきた。 いい加減、俺なりの楽なやり方が見つかったのでまとめておく。

eval(parse(text=...))をモダンに書きたい - My Life as a Mock Quant

全ての引数の名前と値をlistとして取得したい - My Life as a Mock Quant

rlangパッケージとかTidy Eval周りをお勉強するための日本語の参考資料は、油谷さんのこれしかほぼこれしかないと思う。

dplyr再入門(Tidyeval編) - Speaker Deck

ちなみに俺はとっかかりとして、このHadleyの動画が一番しっくりきた。 www.youtube.com これを見た後

Programming with dplyr • dplyr

を読めばなんとなく使いこなせるようにはなる(気がする)。

で、結局どうするのがいいの?

結論としては「俺は(いつもの)dplyr的な書き方を捨てるぞ~Hadley~」である。 なんでそんなことになるのかというと、普通にdplyr使ってる分には当然楽で超いいんだが、それを関数化したい、似たような処理を何回もやりたい、そんなときが俺には多い。 従って、その状況をどうにかするSomethingを考えた。

たとえばこんなデータフレームがあったとしよう。

> set.seed(71)
> df <- data.frame(
+   x = 1:12,
+   key1=sample(letters[1:4], 12, replace=TRUE),
+   key2=sample(letters[1:4], 12, replace=TRUE),
+   key3=sample(letters[1:4], 12, replace=TRUE)
+ )
> df
    x key1 key2 key3
1   1    b    a    a
2   2    c    d    b
3   3    b    d    b
4   4    a    d    b
5   5    b    a    b
6   6    d    d    d
7   7    c    d    a
8   8    d    d    c
9   9    b    c    d
10 10    b    b    a
11 11    a    c    d
12 12    d    a    d

このdata.framekey1, key2, key3のそれぞれでグループ化して件数をカウントしたい、そんな状況を考える。 dplyrに習熟している賢明な読者諸君はこんな風に書くだろう。

> library("dplyr")
> dplyr::group_by(df, key1) %>% dplyr::summarize(count=n())
# A tibble: 4 x 2
  key1  count
  <fct> <int>
1 a         2
2 b         5
3 c         2
4 d         3
> dplyr::group_by(df, key2) %>% dplyr::summarize(count=n())
# A tibble: 4 x 2
  key2  count
  <fct> <int>
1 a         3
2 b         1
3 c         2
4 d         6
> dplyr::group_by(df, key3) %>% dplyr::summarize(count=n())
# A tibble: 4 x 2
  key3  count
  <fct> <int>
1 a         3
2 b         4
3 c         1
4 d         4

「あぁ、dplyrは最高だ、楽だ」とお思いだろう?わかる、俺にもその気持ちがわかる。 わかるが、俺には似たようなCodeを三回も手で書くのは宗教上禁止されているので、こいつを関数化してしまいたいのである。

そして俺は決意した、所謂「dplyrでスタンダードな書き方(非標準評価、Non Standard Evaluation, NSE)」を捨てる、と。 そのために以下のような関数を書いた。

> f <- function(df, key){
+   if(!rlang::is_quosure(key)){
+     key <- rlang::sym(key)
+   }
+   dplyr::group_by(df, !!key) %>% dplyr::summarize(count = n())
+ }

こいつを使うとpurrrパッケージと組み合わせて(別に組み合わせないでsapplyとか使えばいいんだけど)、以下のように書ける。 まさに一撃ワンライナーだ。気持ちがいい。

> purrr::map(1:3, ~ f(df, paste0("key", .x)))
[[1]]
# A tibble: 4 x 2
  key1  count
  <fct> <int>
1 a         2
2 b         5
3 c         2
4 d         3

[[2]]
# A tibble: 4 x 2
  key2  count
  <fct> <int>
1 a         3
2 b         1
3 c         2
4 d         6

[[3]]
# A tibble: 4 x 2
  key3  count
  <fct> <int>
1 a         3
2 b         4
3 c         1
4 d         4

そして先ほど宣言したように、俺は非標準評価、要するに列名を直渡しするスタイルを捨てたのでその場合にはエラーになる。

> f(key1)
Error in rlang::is_quosure(key) : 
  argument "key" is missing, with no default

この状況でも一応、動かすことはできて、その場合にはrlang::quo()を用いればよい。 一件落着である。

> f(df, rlang::quo(key1))
# A tibble: 4 x 2
  key1  count
  <fct> <int>
1 a         2
2 b         5
3 c         2
4 d         3

「なんでこの関数fでうまくいくか?」についてはがんばってrlangを勉強して学んでもらいたい私もまだ道半ばなので解説はなしだ。