dplyr 1.0.4で複数列を対象としたfilterが簡単になった


dplyr 1.0.0から導入されたacross関数は、mutate関数やsummarize関数を複数列に簡単に適用できる便利な道具です。 *_at*_ifといった関数を過去のものにした他、group_byでも使えるなど、使いどころは多いです。

ただし、@yutannihilationさんも指摘している通り、filter関数内でのacrossの利用はお世辞にも直感的ではありませんでした。たとえば、numericな列のいずれかが0.1未満な行を抽出するならこんな感じ。

library(magrittr)

set.seed(9)
d <- data.frame(x = runif(20), y = runif(20), z = letters[1:20])

d %>%
  dplyr::filter(
    rowSums(dplyr::across(where(is.numeric), function(x) x < 0.1)) > 0
  )
#>             x           y z
#> 1 0.024233910 0.020831235 b
#> 2 0.008471164 0.972864807 l
#> 3 0.492743506 0.004426156 o

acrossの返り値はデータフレームであることを利用し、各行の総和をとれば、1つでもTRUEな行は1以上の値をとるわけですね。ということは、すべてが0.1未満な行を抽出しようとすると、行の積をとらなければなりません。 rowProdがないので面倒ですね。あと、括弧が多過ぎてわけわからない……。

dplyr 1.0.4では新たにif_any関数とif_all関数を導入し、この問題に対処します。たとえば先の例をif_anyで書き直すとこんな感じ。

d %>%
  dplyr::filter(
    dplyr::if_any(where(is.numeric), function(x) x < 0.1)
  )
#>             x           y z
#> 1 0.024233910 0.020831235 b
#> 2 0.008471164 0.972864807 l
#> 3 0.492743506 0.004426156 o

across関数に代わりにif_any関数を使い、rowSums関数や>演算子とはおさらばです。すべての列が条件を満たす場合も、if_any関数をif_all関数に書き直すだけ。

d %>%
  dplyr::filter(
    dplyr::if_all(where(is.numeric), function(x) x < 0.1)
  )
#>            x          y z
#> 1 0.02423391 0.02083123 b

ちなみに本機能を紹介するTidyverseのBlog記事「dplyr 1.0.4: if_any() and if_all()」で紹介されている通り、if_anyif_allmutatesummarizeの中でも使えます。

d %>%
  head() %>%
  dplyr::mutate(
    all_low = dplyr::if_all(where(is.numeric), function(x) x < 0.1)
  )
#>            x          y z all_low
#> 1 0.22160140 0.89929956 a   FALSE
#> 2 0.02423391 0.02083123 b    TRUE
#> 3 0.20711902 0.31957158 c   FALSE
#> 4 0.21573355 0.11292940 d   FALSE
#> 5 0.44372359 0.52832265 e   FALSE
#> 6 0.13407615 0.91291264 f   FALSE

他にもdplyr 1.0.4ではacross関数使用時のパフォーマンスが改善されたらしいですね。素晴らしい!

Enjoy!