dplyr 0.4を使ってみる

追記('15/01/04):*_join()の図を追加しました。


新年早々、dplyrの新バージョンがリリースされるらしいです。

dplyr/NEWS.md at master · hadley/dplyr · GitHub

新機能についてはHadleyさんがRPubsに書いてるので、もうブログ書かなくてもいいかなーと思いつつ、修行のため使ってみます。

RPubs - dplyr-0.4

インストール

まだCRANには来てないのでinstall_github()します。

devtools::install_github("dplyr")

バージョン確認。

> packageVersion("dplyr")
[1]0.4.0’

add_rownames()

add_rownames() turns row names into an explicit variable (#639).

rownamedata.frameの列として扱うようにできます。

これがある理由

RPubs - dplyr-0.4によると、dplyrではrownameを使うのは推奨されていなくて、その理由は

no dplyr method is guaranteed to preserve them

というのと、

I don’t think using row names is a good idea because it violates one of the principles of tidy data: every variable should be stored in the same way.

ということらしいです。

使ってみる

こんなデータがあるとします。

> (a <- data.frame(x = 1:6, y = runif(6), row.names = letters[1:6]) )
  x         y
a 1 0.1704538
b 2 0.2886246
c 3 0.6670413
d 4 0.4474072
e 5 0.6183439
f 6 0.2718875

これをadd_rownames()を通すとこうなります。

> add_rownames(a, var = "行名")
  x         y 行名
1 1 0.1704538    a
2 2 0.2886246    b
3 3 0.6670413    c
4 4 0.4474072    d
5 5 0.6183439    e
6 6 0.2718875    f

filter()するときとか便利そうです。

> a %>%
+ add_rownames("rn") %>%
+ filter(rn == "c")
  x         y rn
1 3 0.6670413  c

as_data_frame()

as_data_frame() efficiently coerces a list into a data frame (#749).

これはRPubs - dplyr-0.4を見てると、as.data.frame()の爆速バージョン、みたいな感じらしいです。コードを見てると、

  1. data_frameに変換できるデータ形式(各要素が1次元のベクターで長さがそろっているか、とか)かチェック
  2. classrownamesだけ設定する

ということをやっているようです。結果として得られるものはあんまり変わらなさそうなので説明は省きます。listの扱いはlowlinerを使ってもっとよくなるよ、的なことが仄めかされているのでdplyr 0.5のときにはもっと便利になっているかもしれません。

bind_rows(), bind_cols(), combine()

bind_rows() and bind_cols() efficiently bind a list of data frames by row or column. combine() applies the same coercion rules to vectors (it works like c() or unlist() but is consistent with the bind_rows() rules).

bind_rows()

rbind()の代わり。すでに、rbind_list()とかrbind_all()がありますが、bind_rows()を使うと引数のデータ形式で悩むことが減ります。

こういうデータがあるとします。

> a <- split(mtcars, mtcars$cyl)
> str(a, max.level = 1)
List of 3
 $ 4:'data.frame':    11 obs. of  11 variables:
 $ 6:'data.frame':    7 obs. of  11 variables:
 $ 8:'data.frame':    14 obs. of  11 variables:

rbind_list()とかrbind_all()だと、引数の形を間違えるとエラーが出たりします。(正しくは、rbind_all()data.framelistオブジェクトをひとつだけ引数に取ります。rbind_list()data.frameオブジェクトを複数引数に取ります)

> rbind_all(a[[1]], a[[2]])
Error in rbind_all(a[[1]], a[[2]]) : unused argument (a[[2]])
> rbind_list(a)
Source: local data frame [0 x 0]

bind_rows()はどっちでも大丈夫です。(内部でやってるのはデータ形式を整えてrbind_all()を呼び出すだけのようです)

> bind_rows(a) %>% head(3)
Source: local data frame [3 x 11]

   mpg cyl  disp hp drat   wt  qsec vs am gear carb
1 22.8   4 108.0 93 3.85 2.32 18.61  1  1    4    1
2 24.4   4 146.7 62 3.69 3.19 20.00  1  0    4    2
3 22.8   4 140.8 95 3.92 3.15 22.90  1  0    4    2
> bind_rows(a[[1]], a[[2]]) %>% head(3)
Source: local data frame [3 x 11]

   mpg cyl  disp hp drat   wt  qsec vs am gear carb
1 22.8   4 108.0 93 3.85 2.32 18.61  1  1    4    1
2 24.4   4 146.7 62 3.69 3.19 20.00  1  0    4    2
3 22.8   4 140.8 95 3.92 3.15 22.90  1  0    4    2

bind_cols()

cbind()の代わり。do.call(cbind, list(data.frame1, data.frame2,..))とやるのと同じみたいです。

> bind_cols(data.frame(a = 1:3, b = 11:13), data.frame(c = letters[1:3], d = LETTERS[1:3]))
  a  b c d
1 1 11 a A
2 2 12 b B
3 3 13 c C

が、Hadleyさんは、列の順番が意図通りになってないこともあるから*_join()を使った方がいいよ、と言ってます。

(Generally you should avoid bind_cols() in favour of a join; otherwise check carefully that the rows are in a compatible order).

combine()

c()とかunlist()の代わり。

ヘルプの例に書いてあるのをやってみます。c()とかunlist()はfactorをかってに数字にしてしまったり(この挙動なやましいですよね)、levelを合成してしまったりします。

> f1 <- factor("a")
> f2 <- factor("b")
> c(f1, f2)
[1] 1 1
> unlist(list(f1, f2))
[1] a b
Levels: a b

combine()はcharacter型に直してくれます。いい感じ。

> combine(f1, f2)
[1] "a" "b"
> combine(list(f1, f2))
[1] "a" "b"

ただしその分引数のチェックが少し厳しくなっていて、c()とかunlist()とかにはnumericとcharacterを混ぜて入れても大丈夫ですが、combine()だとエラーが出ます。(これで困る人あんまりいない気もしますけど)

> unlist(list(1, "character"))
[1] "1"         "character"
> combine(list(1, "character"))
Error: incompatible type at index 2 : character, was collecting : numeric

right_join(),full_join()

right_join() (include all rows in y, and matching rows in x) and full_join() (include all rows in x and y) complete the family of mutating joins (#96).

これで*_join()のファミリーがそろったことになります。two-tableというVignetteにSQLとの対照表があります。

Visual Representation of SQL Joins - CodeProjectに触発されて図にしてみたので貼っておきます。

f:id:yutannihilation:20150104115003p:plain

group_indices()

group_indices() computes a unique integer id for each group (#771). It can be called on a grouped_df without any arguments or on a data frame with same arguments as group_by().

ユニークなグループIDを振ってくれます。

これいまいち使い方がピンときてないんですけど、何度もgroup_by()するときは、

mtcars %>%
  mutate_(.,
          group1 = group_indices(., cyl, vs, am),
          group2 = group_indices(., cyl, gear, carb)
          ) -> grouped_mtcars

とかやっておくと、いちいち毎回グループ分けに使う変数名のリストを渡さなくても

group_by(grouped_mtcars, group1)
group_by(grouped_mtcars, group2)

とかできて便利、とかでしょうか?(詳しい方、教えてください...)

print()

ここから先はMinor improvementsの中から気になったところだけ。

print()まわりの改善は、NEWSではminor扱いですが、RPubs - dplyr-0.4には詳しく書いています。

たとえば、

All print() method methods invisibly return their input so you can interleave print() statements into a pipeline to see interim results.

つまり、こんな感じで結果を表示しつつパイプをつないでいくことができます。

mtcars %>% print %>% group_by(cyl) %>% print %>% summarize(mean(mpg))

これまでも、magrittrの%T>%を使うとできましたが(参考:magrittr 1.5を試してみる。 - Technically, technophobic.)、これデバッグの時に便利そうですね。

do_()

do()のnon-standard evaluation版。

slice()

data.tableも動くようになったらしいです。 RDBの場合は動かないので、filter()で同じことをする方法がヘルプに書いてあります。

# Equivalent code using filter that will also work with databases,
# but won't be as fast for in-memory data. For many databases, you'll
# need to supply an explicit variable to use to compute the row number.
filter(mtcars, row_number() == 1L)
filter(mtcars, row_number() == n())
filter(mtcars, between(row_number(), 5, n()))

感想

そんなに劇的に変わった部分はなさそうな印象です。ちょこちょこ便利になってるっぽいですが、なんか細かいのが多くて追いきれませんでした。

RPubs - dplyr-0.4にはちらほらdplyr 0.5のことも仄めかされていて、今後の方向性が気になるところです。

個人的には一番最後のこのへんが気になっています。lowliner使ってみようかな。

My vision of list-variables is still partial and incomplete, but I’m convinced that they will make pipeable APIs for modelling much eaiser. See the draft lowliner package for more explorations in this direction.