dplyr を使ったプログラミング

はじめに

ほとんどの dplyr の動詞は、何らかの形で Tidy評価 を使用します。 Tidy評価は、tidyverse 全体で使用される非標準的な評価の特別なタイプです。 dplyr には2つの基本的な形式があります。

関数の引数がデータマスキングやTidy選択を使用しているかどうかを判断するには、ドキュメントを見てください。 引数リストには、<data-masking> または <tidy-select> と表示されています。

データマスキングやティディセレクションは、インタラクティブなデータ探索を高速かつスムーズに行うことができますが、for ループや関数の中など、間接的に使用しようとすると、新たな課題が生じます。 この vignette では、これらの課題を克服する方法を紹介します。 まず、データマスキングと Tidy 選択の基本を説明し、それらを間接的に使用する方法について説明し、よくある問題を解決するためのいくつかのレシピを紹介します。

この vignette では、Tidy 評価を使った効果的なプログラマーになるための最低限の知識を得ることができます。 基礎となる理論や、非標準的な評価との違いを正確に知りたい場合は、Advanced R の Metaprogramming の章を読むことをお勧めします。

library(dplyr)

データマスキング

データマスキングを行うと、入力が少なくて済むので、データ操作が速くなります。 ほとんどの(すべてではありませんが1)R の基本関数では,変数を $ で参照する必要があり,データフレームの名前を何度も繰り返すコードになります。

starwars[starwars$homeworld == "Naboo" & starwars$species == "Human", ,]

このコードの dplyr 版は、データマスキングにより starwars と1回入力するだけで済むため、より簡潔なコードになっています。

starwars %>% filter(homeworld == "Naboo", species == "Human")

Data- and env-variables

データマスキングの重要な考え方は、「変数」という言葉の2つの異なる意味の境界線を曖昧にすることです。:

これらの定義をもう少し具体的に説明するために、次のようなコードを考えてみましょう。:

df <- data.frame(x = runif(3), y = runif(3))
df$x
#> [1] 0.08075014 0.83433304 0.60076089

xy という2つのデータ変数を含む環境変数dfを作成し、$ を使って環境変数 df からデータ変数 x を抽出します。

このように「変数」の意味を曖昧にすることで、データ変数を接頭辞なしでそのまま参照することができるので、インタラクティブなデータ分析にはとても良い機能だと思います。 多くの新しいRユーザーは diamonds[x == 0 | y == 0, ] と書こうとするので、これはかなり直感的なことだと思います。

残念ながら、このメリットは無料ではありません。このツールを使ってプログラミングを始めると、その区別に悩まされることになります。 今まで考えたことがなかったので、頭が新しい概念やカテゴリーを覚えるのに時間がかかります。 しかし、「変数」という概念を data-variable と env-variable に分けて考えてみると、かなりわかりやすく使えるようになると思います。

インダイレクト

データマスキングを使用した関数を使ったプログラミングの主な課題は、データ変数名を直接入力するのではなく、env-variable からデータ変数を取得したいというような、間接的な操作を導入する場合に発生します。 これには主に2つのケースがあります。

Tidy selection

データマスキングは、データセット内の値の計算を容易にします。 Tidy select は、データセットの列を簡単に扱うことができる補完的なツールです。

tidyselect DSL

Tidyselect を使用するすべての関数の根底には tidyselect パッケージがあります。 これは、名前、位置、またはタイプで列を選択することを容易にする小型のドメイン固有言語を提供します。 たとえば

詳細は、?dplyr_tidy_select で確認できます。

インダイレクト

データマスキングと同様に、Tidy Selectは一般的なタスクを容易にしますが、その代償として一般的でないタスクを困難にします。 列の仕様を中間変数に格納してTidy Selectを間接的に使用したい場合は、いくつかの新しいツールを学ぶ必要があります。 繰り返しになりますが、インダイレクトには2つの形式があります。

How tos

以下の例は、よくある問題を解決するためのものです。 基本的なアイデアを理解していただくために、最小限のコードを示しています。 実際の問題では、より多くのコードや複数の技術を組み合わせる必要があります。

ユーザーが提供するデータ

ドキュメントを確認すると、.data はデータマスキングや Tidy Select を使用していないことがわかります。 つまり、関数内で特別なことをする必要はありません。

mutate_y <- function(data) {
  mutate(data, y = a + x)
}

R CMD check NOTE を削除

以下のようにパッケージを書いていて、データ変数を使う関数があったとします。

my_summary_function <- function(data) {
  data %>% 
    filter(x > 0) %>% 
    group_by(grp) %>% 
    summarise(y = mean(y), n = n())
}

以下のように、R CMD CHECK NOTE を得ることができます。

N  checking R code for possible problems
   my_summary_function: no visible binding for global variable ‘x’, ‘grp’, ‘y’
   Undefined global functions or variables:
     x grp y

.data$var を使い、.datarlang パッケージ(Tidy評価を実装する基本パッケージ)のソースからインポートすることで、これを解消することができます。

#' @importFrom rlang .data
my_summary_function <- function(data) {
  data %>% 
    filter(.data$x > 0) %>% 
    group_by(.data$grp) %>% 
    summarise(y = mean(.data$y), n = n())
}

1つ以上のユーザー提供の式

データマスキングや tidy Select を使用する引数に渡される式をユーザーに提供したい場合は、その引数を受け入れてください。

my_summarise <- function(data, group_var) {
  data %>%
    group_by({{ group_var }}) %>%
    summarise(mean = mean(mass))
}

これは、ユーザーが提供する1つの表現を複数の場所で使用したい場合に、わかりやすく一般化します。

my_summarise2 <- function(data, expr) {
  data %>% summarise(
    mean = mean({{ expr }}),
    sum = sum({{ expr }}),
    n = n()
  )
}

ユーザーに複数の表現を提供してもらいたい場合は、それぞれの表現を受け入れます。

my_summarise3 <- function(data, mean_var, sd_var) {
  data %>% 
    summarise(mean = mean({{ mean_var }}), sd = sd({{ sd_var }}))
}

変数名を出力に使用したい場合は、グルー構文を := と組み合わせて使用します。

my_summarise4 <- function(data, expr) {
  data %>% summarise(
    "mean_{{expr}}" := mean({{ expr }}),
    "sum_{{expr}}" := sum({{ expr }}),
    "n_{{expr}}" := n()
  )
}
my_summarise5 <- function(data, mean_var, sd_var) {
  data %>% 
    summarise(
      "mean_{{mean_var}}" := mean({{ mean_var }}), 
      "sd_{{sd_var}}" := sd({{ sd_var }})
    )
}

任意の数のユーザー提供の式

ユーザーが提供する任意の数の式を取りたい場合は、... を使用します。 この機能は、group_by()mutate() のように、パイプラインの一部分をユーザーが完全に制御できるようにする場合によく使用されます。

my_summarise <- function(.data, ...) {
  .data %>%
    group_by(...) %>%
    summarise(mass = mean(mass, na.rm = TRUE), height = mean(height, na.rm = TRUE))
}

starwars %>% my_summarise(homeworld)
#> # A tibble: 49 x 3
#>   homeworld    mass height
#>   <chr>       <dbl>  <dbl>
#> 1 Alderaan       64   176.
#> 2 Aleen Minor    15    79 
#> 3 Bespin         79   175 
#> 4 Bestine IV    110   180 
#> # … with 45 more rows
starwars %>% my_summarise(sex, gender)
#> `summarise()` has grouped output by 'sex'. You can override using the `.groups` argument.
#> # A tibble: 6 x 4
#> # Groups:   sex [5]
#>   sex            gender      mass height
#>   <chr>          <chr>      <dbl>  <dbl>
#> 1 female         feminine    54.7   169.
#> 2 hermaphroditic masculine 1358     175 
#> 3 male           masculine   81.0   179.
#> 4 none           feminine   NaN      96 
#> # … with 2 more rows

このように ... を使用する場合は、引数が衝突する可能性を減らすために、他の引数が . で始まることを確認してください。 詳細はhttps://design.tidyverse.org/dots-prefix.htmlを参照してください。

ユーザーが提供した変数の変換

ユーザーが提供したデータ変数を変換したい場合は、 across() を使用します。

my_summarise <- function(data, summary_vars) {
  data %>%
    summarise(across({{ summary_vars }}, ~ mean(., na.rm = TRUE)))
}
starwars %>% 
  group_by(species) %>% 
  my_summarise(c(mass, height))
#> # A tibble: 38 x 3
#>   species   mass height
#>   <chr>    <dbl>  <dbl>
#> 1 Aleena      15     79
#> 2 Besalisk   102    198
#> 3 Cerean      82    198
#> 4 Chagrian   NaN    196
#> # … with 34 more rows

これと同じ考え方で、以下のように複数の入力データ・変数のセットにも使用できます。

my_summarise <- function(data, group_var, summarise_var) {
  data %>%
    group_by(across({{ group_var }})) %>% 
    summarise(across({{ summarise_var }}, mean))
}

across().names 引数を使って、出力の名前を制御します。

my_summarise <- function(data, group_var, summarise_var) {
  data %>%
    group_by(across({{ group_var }})) %>% 
    summarise(across({{ summarise_var }}, mean, .names = "mean_{.col}"))
}

複数の変数に対するループ

変数名の文字ベクトルがあり、それらを for ループで操作したい場合は、特別な代名詞 .data にインデックスを付けます。

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

この手法は、base R apply() ファミリーや purrr map() ファミリーのような for ループの代替でも同じように使えます。

mtcars %>% 
  names() %>% 
  purrr::map(~ count(mtcars, .data[[.x]]))

Shiny の入力から変数を使う

Shiny の入力コントロールは文字のベクターを返すことが多いので、上記と同じ方法である .data[[input$var]] を使うことができます。

library(shiny)
ui <- fluidPage(
  selectInput("var", "Variable", choices = names(diamonds)),
  tableOutput("output")
)
server <- function(input, output, session) {
  data <- reactive(filter(diamonds, .data[[input$var]] > 0))
  output$output <- renderTable(head(data()))
}

詳細や事例については https://mastering-shiny.org/action-tidy.html をご覧ください。


  1. dplyr の filter() は、base R の subset() にヒントを得ています。 subset() はデータのマスキングを行いますが、整然とした評価は行わないため、この章で説明するテクニックは適用されません。↩︎

  2. Rでは、引数は遅延的に評価されます。つまり、使用しようとするまで、引数は値を保持せず、値を計算する方法を記述した__promise__を保持するだけです。 詳しくは、以下ををご覧ください。 https://adv-r.hadley.nz/functions.html#lazy-evaluation↩︎