戯言日記

Rの話だと思ったら唐突にサバゲーが混じってくる何か。

acrossでsummarize()が楽になる

・across()で複数列の処理を簡易化

doubtpad.hatenablog.com


上の記事でも書いたが、dplyracross()がやってきた。


dplyr.tidyverse.org


分かりやすく言うと_if()_at()_all()を無くすためのツールで、それぞれの関数に存在した固有の書き方(ルール)を1種類に統一することができる。
もう少し噛み砕いて言うと、1つの関数に3つずつ追加するのを止めて、これ1つで全てに対応できるようにしたってことらしい。

使い方はざっくり書くとこんなイメージ。

dplyr::summarize( dplyr::across( "扱いたい列の名前や条件", "関数" ) )

この「扱いたい列の名前や条件」の部分を上手く作ることで上記3つの役割を集約しており、操作はかなり直感的かつ柔軟になった。


という訳で、試してみる。
例にはお馴染みirisを使って、上のURLで公式が紹介している内容をなぞってみる。

一番使い勝手が良くなった部分はたぶんここかと。
これまで同じ計算をする場合でも、複数列で実効するには1行ずつ書かねばならず非常に面倒だったが、今回からは纏めて書くことができる。
楽になる以上に、コードの見通しがよくなるためすっきりする。


#今までは1つずつ指定
iris %>% 
  dplyr::group_by(Species) %>% 
  dplyr::summarize(Sepal.Length = mean(Sepal.Length),
                   Sepal.Width  = mean(Sepal.Width),
                   Petal.Length = mean(Petal.Length),
                   Petal.Width  = mean(Petal.Width) )

#今後は纏めて指定できる
iris %>% 
  dplyr::group_by(Species) %>% 
  dplyr::summarize( dplyr::across(c(Sepal.Length,
                                    Sepal.Width,
                                    Petal.Length,
                                    Petal.Width), mean) )


上の例と同様の処理なら_at()で範囲を指定するという方法も考えられるが、across()ならvars()を使わずに他と同じような書き方で実現できる。


#これまで
iris %>% 
  dplyr::group_by(Species) %>% 
  dplyr::summarize_at( vars(Sepal.Length:Petal.Width), mean )

#これから
iris %>% 
  dplyr::group_by(Species) %>% 
  dplyr::summarize( dplyr::across(Sepal.Length:Petal.Width, mean) )


また、everything()を活用することでデータ全域を指定することが可能。実質これが_all()の代替となる。


#これまで
iris %>% 
  dplyr::group_by(Species) %>% 
  dplyr::summarize_all( mean )

#これから
iris %>% 
  dplyr::group_by(Species) %>% 
  dplyr::summarize( dplyr::across(everything(), mean) )


グループ因子となった列は巻き込んでいても影響はない。
でもそれ以外に因子列や文字列があると普通にエラーを出すので注意。

続いて_if()の代替方法。


#これまで
iris %>% 
  dplyr::group_by(Species) %>% 
  dplyr::summarize_if( is.numeric, mean )

#これから
iris %>% 
  dplyr::group_by(Species) %>% 
  dplyr::summarize( dplyr::across(where(is.numeric), mean) )

#starts_with()などにも対応
iris %>% 
  dplyr::group_by(Species) %>% 
  dplyr::summarize( dplyr::across(starts_with("P"), mean) )


条件の組み合わせにも対応している。


#複数の条件を与える
iris %>% 
  dplyr::group_by(Species) %>% 
  dplyr::summarize( dplyr::across(where(is.numeric) & starts_with("P"), mean) )


ここまでは_if()_at()_all()の代替方法について見てきたので、その他の部分について。

まず今までもできた複数の関数を纏めて使いたい場合は今まで通りlist()で渡すことができるので、複数の列に対して複数の処理を同時に行える。


#列も処理も複数
iris %>% 
  dplyr::group_by(Species) %>% 
  dplyr::summarize( dplyr::across(where(is.numeric), list(mean, median) ) )


しかし、出力してもらえれば分かるのだが、このままだと列名が分かりにくい。
Sepal.Length_1Sepal.Length_2 ……のようになってしまう)
そこで無名関数を利用して、それぞれのデータに名前が付くように設定する。

ちなみに無名関数とはmean = ~mean(.x)みたいに書いている部分のことで、function(x){mean(x)}と(ほぼ)同じ意味。
よく分からない人はエラーを無くすための魔法の構文と思ってください(雑)。


#それぞれの列に適切な名前を付ける
iris %>% 
  dplyr::group_by(Species) %>% 
  dplyr::summarize( dplyr::across(where(is.numeric),
                                  list(mean = ~mean(.x), median = ~median(.x) ) ) )


しかし、ややごちゃついたコードになってしまうので、関数を渡すlist()を変数として用意した方が全体的な見通しという意味ではいい。


#これが一番読みやすい
mean_median <- list(mean = ~mean(.x), median = ~median(.x) )
iris %>% 
  dplyr::group_by(Species) %>% 
  dplyr::summarize( dplyr::across(where(is.numeric), mean_median ) )


更に踏み込んで、列名を正規表現で変更することもできる。
{fn}に関数名、{col}に列の元々の名前が来る。


mean_median <- list(mean = ~mean(.x), median = ~median(.x) )
iris %>% 
  dplyr::group_by(Species) %>% 
  dplyr::summarize( dplyr::across(where(is.numeric), mean_median, .names = "{fn}.{col}" ) )


ここまでは分かりやすさを重視するためにsummarize()についてのみ書いたが、across()は上で書いた通りほとんどのdplyr関数と併用できる。
そのため応用の範囲は広い。


#mutate()で列を生成
iris %>% 
  dplyr::mutate( dplyr::across(contains("Length"), ~.x*0.1) )

#処理を複数入れてそれぞれに名前を付ける
iris %>% 
  dplyr::mutate( dplyr::across(contains("Length"), list(low = ~.x*0.1, high = ~.x*10)) )

#もちろん正規表現も使える
iris %>% 
  dplyr::mutate( dplyr::across(contains("Length"), 
                               list(low = ~.x*0.1, high = ~.x*10),
                               .names = "{fn}.{col}") )

#distinct()と併用とか
iris %>% dplyr::distinct( dplyr::across(contains("Length")) )


正直な話、_all()とかに関してはむしろコードが長くなっていたりするので、既にdplyrを使っている人たちの判断はどうか分からないとは思う。
Hadley神によるとすぐに使えなくなるとかではないらしいので、好きな方使えばいいんじゃないかな(適当)。

けど、これまでと同じ処理をする場合に覚えなければならないことが減ったので、これからRおよびdplyrに触れる人にとっては使いやすくなったような気がする。

なお自分はこっちに乗り換えるつもりです。 たぶんだけど慣れればこっちの方が楽そうなので。


Enjoy!