メモ:dplyr・ggplot2 で {{ }} を使う

追記(2021/03/26): 開発版では「facet_*() の場合」も動くようになりました。


こういうデータが手元にあるとします。

library(readr)
library(dplyr, warn.conflicts = FALSE)
library(ggplot2)
library(lubridate, warn.conflicts = FALSE)

d_raw <- read_csv(
  "https://stopcovid19.metro.tokyo.lg.jp/data/130001_tokyo_covid19_patients.csv",
  col_types = cols(
    No = col_integer(),
    全国地方公共団体コード = col_integer(),
    公表_年月日 = col_date(),
    発症_年月日 = col_date(),
    確定_年月日 = col_date(),
    患者_渡航歴の有無フラグ = col_logical(),
    患者_接触歴の有無フラグ = col_logical(),
    退院済フラグ = col_logical(),
    .default = col_character()
  )
)

d <- d_raw %>% 
  transmute(
    公表日 = 公表_年月日,
    
    # まずはとりあえず factor に変換してからあとでいじる
    年代 = factor(患者_年代),
    # マイナーなところをある程度まとめる
    年代 = forcats::fct_collapse(年代,
      "~19" = c("10歳未満", "10代"),
      "90~" = c("90代", "100歳以上"),
      "不明" = c("-", "不明")          # 「-」と「不明」の違いはよくわからないのであってるか自信がない
    ),
    # 並べ替え
    年代 = forcats::fct_relevel(年代, "90~", "不明", after = Inf),
    
    性別 = factor(患者_性別),
    性別 = forcats::fct_other(性別, keep = c("女性", "男性"))
  ) %>% 
  filter(公表日 > ymd("2020-10-01"))

glimpse(d)
#> Rows: 63,222
#> Columns: 3
#> $ 公表日 <date> 2020-10-02, 2020-10-02, 2020-10-02, 2020-10-02, 2020-10-02, 202…
#> $ 年代   <fct> 50代, 20代, 20代, 50代, 20代, 60代, 30代, 20代, 70代, 40代, 20代, 40代, 20代…
#> $ 性別   <fct> 女性, 女性, 女性, 男性, 男性, 男性, 女性, 女性, 女性, 女性, 男性, 女性, 女性, 女性, 男性, 男性,…

このデータを年代性別で色分けして時系列グラフを描きたい、 みたいなとき、指定した列を軸に集計してプロットする関数をつくっておくと、探索的に調べるときに便利だったりします。 そんなときに使うのが {{ }} (curly-curly、これで変数を囲むことを「embrace」と呼ぶらしい)です

具体的にはこんな感じ。

# このあと何度か足し合わせることになるのでリストでまとめておく
styles <- list(
  scale_fill_viridis_d(option = "E", alpha = 0.85),
  scale_x_date(date_breaks = "1 week", date_labels = "%m/%d"),
  scale_y_continuous("陽性判明者数(移動平均)", labels = scales::label_comma(accuracy = 1)),
  theme_minimal(),
  theme(axis.text.x = element_text(angle = -30, hjust = 0))
)

do_plot1 <- function(group) {
  d_summary <- d %>% 
    count(公表日, {{ group }}, name = "n") %>% 
    mutate(
      # 7日間移動平均
      n_ma = slider::slide_index_dbl(n, 公表日, mean, .before = days(6))
    )
    
  ggplot(d_summary) +
    geom_area(aes(公表日, n_ma, fill = {{ group }})) +
    styles
}

こうしておくと、渡した変数によって集計の軸を変えることができ、

do_plot1(年代)

do_plot1(性別)

引数を渡さなければ勝手に省略してくれて便利です。

do_plot1()

facet_*() の場合

ただし、{{ }}はどんな場合でも動くわけでなく、例えば facet_*() に引数を渡そうとすると、その引数を指定しなかったときにエラーになります。

do_plot2 <- function(group, facet) {
  d_summary <- d %>% 
    count(公表日, {{ facet }}, {{ group }}, name = "n") %>% 
    mutate(
      # 7日間移動平均
      n_ma = slider::slide_index_dbl(n, 公表日, mean, .before = days(6))
    )
  
  ggplot(d_summary) +
    geom_area(aes(公表日, n_ma, fill = {{ group }})) +
    facet_grid(cols = vars({{ facet }})) +
    styles
}

# 引数を両方指定すれば普通に動く
do_plot2(年代, 性別)

# `facet`引数を省略しようとするとエラーになる
do_plot2(年代)
#> Error: At least one layer must contain all faceting variables: `<empty>`.
#> * Plot is missing `<empty>`
#> * Layer 1 is missing `<empty>`

facet_*() の場合は NULL だと無視してくれるので、引数のデフォルト値に NULL を指定しておくとうまくいきます。

do_plot2_fixed <- function(group, facet = NULL) {
  d_summary <- d %>% 
    count(公表日, {{ facet }}, {{ group }}, name = "n") %>% 
    mutate(
      # 7日間移動平均
      n_ma = slider::slide_index_dbl(n, 公表日, mean, .before = days(6))
    )
  
  ggplot(d_summary) +
    geom_area(aes(公表日, n_ma, fill = {{ group }})) +
    facet_grid(cols = vars({{ facet }})) +
    styles
}

do_plot2_fixed(年代)

ここは正解がわからず小一時間悩んだんですが、なにか間違えてたら教えてください。。

tidy eval から完全に脱却できるわけではない

あと、そもそも {{ (や ...)が使えるのは、引数をそのまま渡すケースだけなので、一般的に「引数を評価せずに調べたい」みたいなとき、例えば、

  • ある引数が指定されていればグラフの種類を変えたい
  • ... に指定されている引数の数が3以上ならfacet_grid()じゃなくてfacet_wrap()`にしたい

みたいなちょっと凝ったことをするには、enquo()enquos() などを使って quosure 化してから !!!!! で unquote する、という古き良き tidy eval を使っていく必要があります。 その割に、tidy eval はもはやなかったことにされていて、まともなドキュメントが残っていません。 これとか superseded になってるけど、やっぱり使わないといけない場面あるよなという。

あしたはどっちだ。という気分になりますが希望を捨てずにやっていきましょう。