複数の列に対して同じ操作を行うと便利なことが多いのですが、コピー&ペーストは面倒だし、エラーにもなりがちです。:
(各行の mean(a, b, c, d)
を計算しようとしている場合は、代わりに vignette("rowwise")
を参照してください)
この vignette では、前のコードをより簡潔に書き直すことができるacross()
関数を紹介します。:
まず、across()
の基本的な使い方、特に summarise()
に適用される使い方について説明し、複数の関数と一緒に使用する方法を示します。その後、他の動詞との併用をいくつか紹介します。最後に少し歴史的な話をして、最後のアプローチ(_if()
、_at()
、_all()
関数)よりも across()
を好む理由と、古いコードを新しい構文に変換する方法を紹介します。
across()
には2つの主要な引数があります。
最初の引数である .cols
は、操作したい列を選択します。 これは(select()
のような)tidy selection を使用しているので、位置、名前、タイプで変数を選ぶことができます。 位置、名前、型で変数を選ぶことができます。
第2引数の .fns
は、各カラムに適用する関数または関数のリストです。 各カラムに適用する関数または関数のリストです。2番目の引数、.fns
は、各列に適用する関数または関数のリストです。 .x / 2
のようなpurrrスタイルの数式(または数式のリスト)にすることもできます。(この引数はオプションで、単に基礎となるデータを取得したい場合には省略することができます。 省略することもできます。このテクニックは vignette("rowwise")
で使われています)。
ここでは、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 引数に関数やラムダ関数の名前付きリストを指定すると、各変数を複数の関数で変換することができます。
min_max <- list(
min = ~min(.x, na.rm = TRUE),
max = ~max(.x, na.rm = TRUE)
)
starwars %>% summarise(across(where(is.numeric), min_max))
#> # A tibble: 1 x 6
#> height_min height_max mass_min mass_max birth_year_min birth_year_max
#> <int> <int> <dbl> <dbl> <dbl> <dbl>
#> 1 66 264 15 1358 8 896
starwars %>% summarise(across(c(height, mass, birth_year), min_max))
#> # A tibble: 1 x 6
#> height_min height_max mass_min mass_max birth_year_min birth_year_max
#> <int> <int> <dbl> <dbl> <dbl> <dbl>
#> 1 66 264 15 1358 8 896
名前の作成方法は、glueという仕様の.names
という引数で制御します。:
starwars %>% summarise(across(where(is.numeric), min_max, .names = "{.fn}.{.col}"))
#> # A tibble: 1 x 6
#> min.height max.height min.mass max.mass min.birth_year max.birth_year
#> <int> <int> <dbl> <dbl> <dbl> <dbl>
#> 1 66 264 15 1358 8 896
starwars %>% summarise(across(c(height, mass, birth_year), min_max, .names = "{.fn}.{.col}"))
#> # A tibble: 1 x 6
#> min.height max.height min.mass max.mass min.birth_year max.birth_year
#> <int> <int> <dbl> <dbl> <dbl> <dbl>
#> 1 66 264 15 1358 8 896
同じ機能を持つすべてのサマリーをグループ化したい場合は、自分でコールを拡張する必要があります。:
starwars %>% summarise(
across(c(height, mass, birth_year), ~min(.x, na.rm = TRUE), .names = "min_{.col}"),
across(c(height, mass, birth_year), ~max(.x, na.rm = TRUE), .names = "max_{.col}")
)
#> # A tibble: 1 x 6
#> min_height min_mass min_birth_year max_height max_mass max_birth_year
#> <int> <dbl> <dbl> <int> <dbl> <dbl>
#> 1 66 15 8 264 1358 896
(いつかこれがacross()
の引数になるかもしれませんが、どうなるかはまだわかりません)。
しかし、この最後のケースでは、where(is.numeric)
は使えません。 は新たに作成された変数(“min_height”、“min_mass”、“min_birth_year”)を拾ってしまうからです。
この問題を解決するには、両方の across()
を組み合わせて、1つの式にして、その式の戻り値を tibble にします。
starwars %>% summarise(
tibble(
across(where(is.numeric), ~min(.x, na.rm = TRUE), .names = "min_{.col}"),
across(where(is.numeric), ~max(.x, na.rm = TRUE), .names = "max_{.col}")
)
)
#> # A tibble: 1 x 6
#> min_height min_mass min_birth_year max_height max_mass max_birth_year
#> <int> <dbl> <dbl> <int> <dbl> <dbl>
#> 1 66 15 8 264 1358 896
代わりに、relocate()
で結果を再編成することもできます。:
必要に応じて、cur_column()
を呼び出すことで、内部の「現在の」列の名前にアクセスすることができます。これは、ベクトルにエンコードされている文脈依存の変換を行いたい場合に役立ちます。
数値のサマリーを where(is.numeric)
で組み合わせる場合は注意が必要です。
df <- data.frame(x = c(1, 2, 3), y = c(1, 4, 9))
df %>%
summarise(n = n(), across(where(is.numeric), sd))
#> n x y
#> 1 NA 1 4.041452
ここで n
は NA
になります。n
は数値なので、across()
はその標準偏差を計算し、3 (定数) の標準偏差は NA
になります。この問題を避けるためには,n()
を最後に計算したいところです。
また、操作対象の列から n
を明示的に除外することもできます。
もう一つの方法は,n()
と across()
の両方の呼び出しを一つの式にまとめて,tibble を返すことである。
これまでは、summarise()
で across()
を使用することを中心に説明してきましたが、データマスキングを使用する他の dplyr 動詞でも使用できます。
すべての数値変数を0~1の範囲にリスケールします。
group_by()
, count()
, distinct()
などの一部の動詞では、要約関数を省略することができます。
すべての distinc を見つける
与えられたパターンを持つ変数のすべての組み合わせを数える。:
across()
は、select()
や rename()
とは、すでに整然としたselect構文を使っているので、一緒には使えません。列名を関数で変換したい場合は、rename_with()
を使います。
filter()の中でacross()
を直接使うことはできません。 結果を結合するための追加のステップが必要だからです。そのために、filter()
には2つの特別な目的を持った付属関数があります。
if_any()
は、選択された少なくとも1つの列について述語が真である行を保持する。starwars %>%
filter(if_any(everything(), ~ !is.na(.x)))
#> # A tibble: 87 x 14
#> name height mass hair_color skin_color eye_color birth_year sex gender
#> <chr> <int> <dbl> <chr> <chr> <chr> <dbl> <chr> <chr>
#> 1 Luke~ 172 77 blond fair blue 19 male mascu~
#> 2 C-3PO 167 75 <NA> gold yellow 112 none mascu~
#> 3 R2-D2 96 32 <NA> white, bl~ red 33 none mascu~
#> 4 Dart~ 202 136 none white yellow 41.9 male mascu~
#> # ... with 83 more rows, and 5 more variables: homeworld <chr>, species <chr>,
#> # films <list>, vehicles <list>, starships <list>
if_all()
は、選択されたすべての列について述語が真である行を保持します。starwars %>%
filter(if_all(everything(), ~ !is.na(.x)))
#> # A tibble: 29 x 14
#> name height mass hair_color skin_color eye_color birth_year sex gender
#> <chr> <int> <dbl> <chr> <chr> <chr> <dbl> <chr> <chr>
#> 1 Luke~ 172 77 blond fair blue 19 male mascu~
#> 2 Dart~ 202 136 none white yellow 41.9 male mascu~
#> 3 Leia~ 150 49 brown light brown 19 fema~ femin~
#> 4 Owen~ 178 120 brown, gr~ light blue 52 male mascu~
#> # ... with 25 more rows, and 5 more variables: homeworld <chr>, species <chr>,
#> # films <list>, vehicles <list>, starships <list>
欠損値を持つ変数がないすべての行を検索します。:
starwars %>% filter(across(everything(), ~ !is.na(.x)))
#> # A tibble: 29 x 14
#> name height mass hair_color skin_color eye_color birth_year sex gender
#> <chr> <int> <dbl> <chr> <chr> <chr> <dbl> <chr> <chr>
#> 1 Luke~ 172 77 blond fair blue 19 male mascu~
#> 2 Dart~ 202 136 none white yellow 41.9 male mascu~
#> 3 Leia~ 150 49 brown light brown 19 fema~ femin~
#> 4 Owen~ 178 120 brown, gr~ light blue 52 male mascu~
#> # ... with 25 more rows, and 5 more variables: homeworld <chr>, species <chr>,
#> # films <list>, vehicles <list>, starships <list>
_if
, _at
, _all
以前のバージョンの dplyr では、接尾辞が _if
, _at
, _all()
の関数を使って、異なる方法で複数の列に関数を適用することができました。これらの関数は差し迫った必要性を解決し、多くの人に利用されていましたが、現在は廃止されています。つまり、これらの関数は存続しますが、新しい機能は追加されず、重要なバグの修正のみが行われます。
across()
が好きなのか?なぜこれらの関数を廃止して across()
を採用することにしたのでしょうか。
across()
を使うと、これまで不可能だった便利なサマリを表現することができます。 以前は不可能でした。across()
は、dplyr が提供する必要のある関数の数を減らします。 これにより、dplyr が使いやすくなり(覚えるべき関数が少ないので)dplyr が使いやすくなり、また、新しい動詞の実装が容易になります。4つの関数ではなく、1つの関数を実装すればよいからです)。)
across()
は _if
と _at
のセマンティクスを統一して、位置、名前、タイプ、そして 位置、名前、タイプで選択できるようになり、以前は不可能だった複合選択ができるようになりました。これまでは不可能だった複合的な選択が可能になりました。例えば、名前が “x”で始まるすべての数値列を変換することができます。:across(where(is.numeric) & starts_with("x"))
.
across()
は vars()
を使う必要はありません。dplyr では、_at()
関数が、変数名を手動で引用しなければならない唯一の場所です。奇妙な名前になってしまい、覚えるのが大変です。
across()
の発見にこれほど時間がかかったのか?もっと早くに across()
を発見できなかったことは残念です。その代わり、何度も失敗を繰り返しました(最初は共通の問題であることに気づかず、次に _each()
関数で、そして最近では _if()
/_at()
/_all()
関数で)。しかし、最近の3つの発見がなければ across()
は動作しませんでした。
それ自体がデータフレームであるデータフレームの列を持つことができます。 これはRの基本機能として提供されているものですが、あまり文書化されておらず、単なる理論的な好奇心ではなく、有用であることを理解するのに時間がかかりました。
データフレームを使用して、要約関数が複数の列を返せるようにすることができます。
外側の名前がないことを、データフレームの列を個々の列に展開したいという規約として使うことができます。データフレームの列を個々の列に展開したいという規約として使用できます。
幸いなことに、既存のコードを across()
を使用するように変換するのは一般的に簡単です。
関数から _if()
, _at()
, _all()
のサフィックスを取り除く。
across()
を呼び出す。最初の引数は次のようになります。
_if()
の場合は,古い第2引数を where()
で包んだものになります。_at()
の場合、古い第2引数から vars()
の呼び出しを削除したもの。all()
では、everything()
となります。それ以降の引数はそのままコピーできます。
例えば、以下のようになります。
df %>% mutate_if(is.numeric, mean, na.rm = TRUE)
# ->
df %>% mutate(across(where(is.numeric), mean, na.rm = TRUE))
df %>% mutate_at(vars(c(x, starts_with("y"))), mean)
# ->
df %>% mutate(across(c(x, starts_with("y")), mean, na.rm = TRUE))
df %>% mutate_all(mean)
# ->
df %>% mutate(across(everything(), mean))
このルールにはいくつかの例外があります。:
rename_*()
とselect_*()
は異なるパターンです。これらはすでにセレクトのセマンティクスを持っているので、一般的には across()
と直接等価なものがない別の方法で使用されます。
これまでは、filter_*()
は all_vars()
や any_vars()
というヘルパーとペアになっていました。新しいヘルパー if_any()
と if_all()
は filter()
の中で使われ、述語が少なくとも1つの、あるいは選択されたすべての列について真である行を保持することができます。:
df <- tibble(x = c("a", "b"), y = c(1, 1), z = c(-1, 1))
# Find all rows where EVERY numeric variable is greater than zero
df %>% filter(if_all(where(is.numeric), ~ .x > 0))
#> # A tibble: 1 x 3
#> x y z
#> <chr> <dbl> <dbl>
#> 1 b 1 1
# Find all rows where ANY numeric variable is greater than zero
df %>% filter(if_any(where(is.numeric), ~ .x > 0))
#> # A tibble: 2 x 3
#> x y z
#> <chr> <dbl> <dbl>
#> 1 a 1 -1
#> 2 b 1 1
mutate()
で使用される場合、across()
で実行されるすべての変換が一度に適用されます。これはmutate_if()
、mutate_at()
、mutate_all()
が1つずつ変換を適用するのとは異なります。一般的には、この新しい動作はあまり驚かないと思われます。: