Column-wise operations

複数の列に対して同じ操作を行うと便利なことが多いのですが、コピー&ペーストは面倒だし、エラーにもなりがちです。:

df %>% 
  group_by(g1, g2) %>% 
  summarise(a = mean(a), b = mean(b), c = mean(c), d = mean(d))

(各行の mean(a, b, c, d) を計算しようとしている場合は、代わりに vignette("rowwise") を参照してください)

この vignette では、前のコードをより簡潔に書き直すことができるacross() 関数を紹介します。:

df %>% 
  group_by(g1, g2) %>% 
  summarise(across(a:d, mean))

まず、across() の基本的な使い方、特に summarise() に適用される使い方について説明し、複数の関数と一緒に使用する方法を示します。その後、他の動詞との併用をいくつか紹介します。最後に少し歴史的な話をして、最後のアプローチ(_if()_at()_all() 関数)よりも across() を好む理由と、古いコードを新しい構文に変換する方法を紹介します。

library(dplyr, warn.conflicts = FALSE)

基本的な使い方

across() には2つの主要な引数があります。

ここでは、across() とそのお気に入りの動詞である summarise() を組み合わせた例をいくつか紹介します。しかし、後で説明するように、across() はどのような dplyr の動詞でも使用できます。

starwars %>% 
  summarise(across(where(is.character), ~ length(unique(.x))))
#> # A tibble: 1 x 8
#>    name hair_color skin_color eye_color   sex gender homeworld species
#>   <int>      <int>      <int>     <int> <int>  <int>     <int>   <int>
#> 1    87         13         31        15     5      3        49      38

starwars %>% 
  group_by(species) %>% 
  filter(n() > 1) %>% 
  summarise(across(c(sex, gender, homeworld), ~ length(unique(.x))))
#> # A tibble: 9 x 4
#>   species    sex gender homeworld
#>   <chr>    <int>  <int>     <int>
#> 1 Droid        1      2         3
#> 2 Gungan       1      1         1
#> 3 Human        2      2        16
#> 4 Kaminoan     2      2         1
#> # ... with 5 more rows

starwars %>% 
  group_by(homeworld) %>% 
  filter(n() > 1) %>% 
  summarise(across(where(is.numeric), ~ mean(.x, na.rm = TRUE)))
#> # A tibble: 10 x 4
#>   homeworld height  mass birth_year
#>   <chr>      <dbl> <dbl>      <dbl>
#> 1 Alderaan    176.  64         43  
#> 2 Corellia    175   78.5       25  
#> 3 Coruscant   174.  50         91  
#> 4 Kamino      208.  83.1       31.5
#> # ... with 6 more rows

across() は通常、summarise()mutate()と組み合わせて使用されるため、誤って変更してしまわないようにグループ化変数を選択しないようになっています。

df <- data.frame(g = c(1, 1, 2), x = c(-1, 1, 3), y = c(-1, -4, -9))
df %>% 
  group_by(g) %>% 
  summarise(across(where(is.numeric), sum))
#> # A tibble: 2 x 3
#>       g     x     y
#>   <dbl> <dbl> <dbl>
#> 1     1     0    -5
#> 2     2     3    -9

複数の関数

第 2 引数に関数やラムダ関数の名前付きリストを指定すると、各変数を複数の関数で変換することができます。

名前の作成方法は、glueという仕様の.namesという引数で制御します。:

同じ機能を持つすべてのサマリーをグループ化したい場合は、自分でコールを拡張する必要があります。:

(いつかこれがacross()の引数になるかもしれませんが、どうなるかはまだわかりません)。

しかし、この最後のケースでは、where(is.numeric)は使えません。 は新たに作成された変数(“min_height”、“min_mass”、“min_birth_year”)を拾ってしまうからです。

この問題を解決するには、両方の across() を組み合わせて、1つの式にして、その式の戻り値を tibble にします。

代わりに、relocate() で結果を再編成することもできます。:

現在の列

必要に応じて、cur_column()を呼び出すことで、内部の「現在の」列の名前にアクセスすることができます。これは、ベクトルにエンコードされている文脈依存の変換を行いたい場合に役立ちます。

問題 (gotcha)

数値のサマリーを where(is.numeric) で組み合わせる場合は注意が必要です。

ここで nNA になります。n は数値なので、across() はその標準偏差を計算し、3 (定数) の標準偏差は NA になります。この問題を避けるためには,n()を最後に計算したいところです。

また、操作対象の列から n を明示的に除外することもできます。

もう一つの方法は,n()across() の両方の呼び出しを一つの式にまとめて,tibble を返すことである。

その他の動詞

これまでは、summarise()across() を使用することを中心に説明してきましたが、データマスキングを使用する他の dplyr 動詞でも使用できます。

group_by(), count(), distinct() などの一部の動詞では、要約関数を省略することができます。

across() は、select()rename() とは、すでに整然としたselect構文を使っているので、一緒には使えません。列名を関数で変換したい場合は、rename_with() を使います。

filter()

filter()の中でacross()を直接使うことはできません。 結果を結合するための追加のステップが必要だからです。そのために、filter()には2つの特別な目的を持った付属関数があります。

_if, _at, _all

以前のバージョンの dplyr では、接尾辞が _if, _at, _all() の関数を使って、異なる方法で複数の列に関数を適用することができました。これらの関数は差し迫った必要性を解決し、多くの人に利用されていましたが、現在は廃止されています。つまり、これらの関数は存続しますが、新しい機能は追加されず、重要なバグの修正のみが行われます。

なぜ私たちは across() が好きなのか?

なぜこれらの関数を廃止して across() を採用することにしたのでしょうか。

    1. across()を使うと、これまで不可能だった便利なサマリを表現することができます。 以前は不可能でした。
  1. across()は、dplyr が提供する必要のある関数の数を減らします。 これにより、dplyr が使いやすくなり(覚えるべき関数が少ないので)dplyr が使いやすくなり、また、新しい動詞の実装が容易になります。4つの関数ではなく、1つの関数を実装すればよいからです)。)

  2. across()_if_at のセマンティクスを統一して、位置、名前、タイプ、そして 位置、名前、タイプで選択できるようになり、以前は不可能だった複合選択ができるようになりました。これまでは不可能だった複合的な選択が可能になりました。例えば、名前が “x”で始まるすべての数値列を変換することができます。:across(where(is.numeric) & starts_with("x")).

  3. across()vars() を使う必要はありません。dplyr では、_at() 関数が、変数名を手動で引用しなければならない唯一の場所です。奇妙な名前になってしまい、覚えるのが大変です。

なぜ across() の発見にこれほど時間がかかったのか?

もっと早くに across() を発見できなかったことは残念です。その代わり、何度も失敗を繰り返しました(最初は共通の問題であることに気づかず、次に _each() 関数で、そして最近では _if()/_at()/_all() 関数で)。しかし、最近の3つの発見がなければ across() は動作しませんでした。

既存のコードを変換するには?

幸いなことに、既存のコードを across() を使用するように変換するのは一般的に簡単です。

例えば、以下のようになります。

このルールにはいくつかの例外があります。: