dplyr 1.00のacross関数が便利だった

「dplyr 1.00がリリースされCRANからインストールできるようになった」と教祖様(William Hadley氏)がツイートしていました。

主な変更点はTidyverseの公式ウェブサイトにまとめられています。

私も早速dplyr 1.00にアップデートしまして,変更点の中で目玉の1つであるacross()関数を使ってみたので,今回はその使い方をまとめていきます。

across関数とは

dplyr 1.00をインストールしてhelp(across)を見ると以下の記述があります。

Description: across() makes it easy to apply the same transformation to multiple columns, allowing you to use select() semantics inside in summarise() and mutate(). across() supersedes the family of "scoped variants" like summarise_at(), summarise_if(), and summarise_all().

簡単に言うと「across()はsummarise()やmutate()の中でselect()の機能を果たす。従来の_atや_ifに代替されるもの。」ということです。具体例を見ていきましょう。

従来の書き方とacross関数

従来の書き方とacross関数を比較するため,「irisでSpeciesごとに4変数(Sepal.Lengthなど)の平均値を計算する」という処理を考えます。従来の書き方として3つ紹介します。

従来パターン1: summariseの中身をベタ書き

最悪の書き方です。

library(dplyr)
library(tidyr)

> iris %>%
+   group_by(Species) %>%
+   summarise(Sepal.Length = mean(Sepal.Length),
+             Sepal.Width  = mean(Sepal.Width),
+             Petal.Length = mean(Petal.Length),
+             Petal.Width  = mean(Petal.Width))
`summarise()` ungrouping output (override with `.groups` argument)
# A tibble: 3 x 5
  Species    Sepal.Length Sepal.Width Petal.Length Petal.Width
  <fct>             <dbl>       <dbl>        <dbl>       <dbl>
1 setosa             5.01        3.43         1.46       0.246
2 versicolor         5.94        2.77         4.26       1.33 
3 virginica          6.59        2.97         5.55       2.03 
従来パターン2: tidyrでgatherしてspread

処理次第でこの書き方が楽な場合もありますが,今回の処理では悪手です。

iris %>%
  pivot_longer(names_to = "feature", values_to = "value", cols = -Species) %>%
  group_by(Species, feature) %>%
  summarise(value = mean(value)) %>%
  pivot_wider(names_from = feature, values_from = value)
従来パターン3: summarise_ifかsummarise_at

目的の処理に対して従来の最適な書き方がこちらです。

# summarise_if
iris %>%
  group_by(Species) %>%
  summarise_if(is.numeric, mean)

# summarise_at
iris %>%
  group_by(Species) %>%
  summarise_at(vars(Petal.Length:Sepal.Width), mean)

acrossを使った書き方

across()の使い方はこちらです。

across(.cols = everything(), .fns = NULL, ..., .names = NULL)

第1引数には変数,第2引数には関数を渡します。ポイントはselect()と同じように第1引数を渡せるという点です。starts_with()やwhere()などのヘルパー関数を用いることができます。

上で行ったirisの処理を(1)コロン,(2)ヘルパー関数where,(3)ヘルパー関数everythingを使ってやってみるとこんな感じです。

# (1) colon
iris %>%
  group_by(Species) %>%
  summarise(across(Sepal.Length:Petal.Width, mean))

# (2) where()
iris %>%
  group_by(Species) %>%
  summarise(across(where(is.numeric), mean))

# (3) everything()
iris %>%
  group_by(Species) %>%
  summarise(across(everything(), mean))

「従来のsummarise_ifやsummarise_atと一緒じゃん」と思う方もいるかもしれません。しかし,across()関数の方が圧倒的に便利な点があります。

across関数の,ここが凄い!!!

across()を使えば,従来の_ifや_atには不可能だった表現が可能になります。例えばirisを使って

  1. 4変数(SepalとPetal)の平均値を計算
  2. Speciesのレベル数を出力
  3. 行数を出力

を一気にできます。

> iris %>%
+   summarise(across(where(is.numeric), mean),
+             across(where(is.factor), nlevels),
+             count = n())

  Sepal.Length Sepal.Width Petal.Length Petal.Width Species count
1     5.843333    3.057333        3.758    1.199333       3   150

まとめ

across関数を使ってみました。利点のまとめがこちらです。

  • 引数の渡し方に応じて_atや_ifで処理を分ける必要がなく,summariseやmutateの引数として柔軟に表現できる。
  • vars()すら要らない。
  • 従来の_ifや_atには不可能だった表現が可能になる。

皆さんも_ifや_atを卒業して,across()に入門してみましょう。 終わり。