dplyr 0.7.0を使ってみる

注:これは2017/04/15に公開した「dplyr 0.6.0-rcを使ってみる」という記事を加筆修正したものです。


dplyr 0.6.0は5/11ごろにリリース予定でしたがなかなかリリースされず…

約1カ月遅れで0.7.0として6/9についにリリースされました!

かなりの変更が入っています。全部は見ていられないので、NEWSに載っている中から気になった変更点を拾ってみます。

New data, functions, and features

新規のデータセット

データセットが増えました。

  • starwars: スターウォーズのキャラクターのデータ。リスト型の列がある。
  • storms: 台風のデータ
  • band_membersband_instrumentsband_instruments2: バンドメンバーのデータ。joinの説明に使うやつらしいです。

add_count()add_tally()

  • New add_count() and add_tally() for adding an n column within groups (#2078, @dgrtwo).

おさらいですが、count()は指定した列のカテゴリごとの個数を出してくれます。

count(starwars, species, sort = TRUE)
#> # A tibble: 38 × 2
#>     species     n
#>       <chr> <int>
#> 1     Human    35
#> 2     Droid     5
#> 3      <NA>     5
#> 4    Gungan     3
#> 5  Kaminoan     2
#> 6  Mirialan     2
#> 7   Twi'lek     2
#> 8   Wookiee     2
#> 9    Zabrak     2
#> 10   Aleena     1
#> # ... with 28 more rows

しかし、このnをもとのデータと紐づけて使いたいことがよくあります。例えば、nが2以上の種族だけに絞り込みたい場合とか。 そういうとき、これまでは、一度グループ化して、n()をとって、またグループ化を解除する、ということをする必要がありました。

sw <- select(starwars, name, species)

sw %>%
  group_by(species) %>%
  mutate(n = n()) %>%
  ungroup()

add_count()を使うとこれをあっさりやってくれます。

sw %>%
  add_count(species)
#> # A tibble: 87 × 3
#>                  name species     n
#>                 <chr>   <chr> <int>
#> 1      Luke Skywalker   Human    35
#> 2               C-3PO   Droid     5
#> 3               R2-D2   Droid     5
#> 4         Darth Vader   Human    35
#> 5         Leia Organa   Human    35
#> 6           Owen Lars   Human    35
#> 7  Beru Whitesun lars   Human    35
#> 8               R5-D4   Droid     5
#> 9   Biggs Darklighter   Human    35
#> 10     Obi-Wan Kenobi   Human    35
#> # ... with 77 more rows

add_tally()も似たような感じなので省略(多くの人はそもそもtally()になじみがなさそう…)。

arrange().by_group引数

  • arrange() for grouped data frames gains a .by_group argument so you can choose to sort by groups if you want to (defaults to FALSE) (#2318)

dplyrは0.5.0になったときに、arrange()がグループ化を無視する挙動になっていました。

つまり、こういうことです。

set.seed(6)
by_cyl <- mtcars %>%
  group_by(cyl) %>%
  select(cyl, wt) %>%
  # 各グループから3つづつ列を抽出
  sample_n(3)

by_cyl %>% arrange(desc(wt))
#> Source: local data frame [9 x 2]
#> Groups: cyl [3]
#> 
#>     cyl    wt
#>   <dbl> <dbl>
#> 1     8 5.424
#> 2     8 3.570
#> 3     6 3.440
#> 4     6 3.440
#> 5     8 3.435
#> 6     6 3.215
#> 7     4 3.150
#> 8     4 2.465
#> 9     4 1.513

ちょっとわかりづらいですが、グループ化のキーであるcyl列を無視してwt列だけでソートされています。 cyl列もソートに使うには、明示的に

by_cyl %>% arrange(cyl, desc(wt))

と書く必要がありました。0.6.0では、.by_group = TRUEを指定すると、グループ化のキー列もソートに使ってくれます。

by_cyl %>% arrange(desc(wt), .by_group = TRUE)
#> Source: local data frame [9 x 2]
#> Groups: cyl [3]
#> 
#>     cyl    wt
#>   <dbl> <dbl>
#> 1     4 3.150
#> 2     4 2.465
#> 3     4 1.513
#> 4     6 3.440
#> 5     6 3.440
#> 6     6 3.215
#> 7     8 5.424
#> 8     8 3.570
#> 9     8 3.435

pull()

  • New pull() generic for extracting a single column either by name or position (either from the left or the right). Thanks to @paulponcet for the idea (#2054).

これは、data.frameから1つの列をベクトルとして取り出す関数です。lplyrパッケージというリスト用のdplyrみたいなパッケージがあって、そこから取ってきた関数らしいです。

[[とほぼ動きをします。

data_frame(1:3) %>% pull(1)
#> [1] 1 2 3

[[(とかそのエイリアスであるmagrittr::extract2())でよくない?という話ですが、[[はデータベースをバックエンドに持つものに対してはうまく動作しません。

# 注:この例を実行するにはdbplyrパッケージが必要です

dbplyr::memdb_frame(1:3) %>% `[[`(1)
#> src:  sqlite 3.11.1 [:memory:]
#> tbls: akpfylaauw, kimorgsivb, sqlite_stat1, ykaaxhbrja

pull()を使っておけばそのへんの面倒を見てくれます。

dbplyr::memdb_frame(1:3) %>% pull(1)
#> [1] 1 2 3

インデックスではなくて列名を指定することもできます。

#> dbplyr::memdb_frame(x = 1:3) %>% pull(x)
[1] 1 2 3

as_tibble

  • as_tibble() is re-exported from tibble. This is the recommend way to create tibbles from existing data frames. tbl_df() has been softly deprecated. tribble() is now imported from tibble (#2336, @chrMongeau); this is now prefered to frame_data().

data.frameをtibbleに変換するのにas_tibble()が推奨され、tbl_df()がdeprecatedになりました。 同様に、ゼロからtibbleをつくるときに使うのも、tribble()が推奨され、frame_data()がdeprecatedになりました。

Deprecated and defunct

  • dplyr no longer messages that you need dtplyr to work with data.table (#2489).

dplyrとdata.tableパッケージをいっしょに使うときはdtplyrパッケージを使ってね、という親切メッセージが出てたんですが、もういいだろうということででなくなります。

  • Long deprecated regroup() has been removed.

なんですっけこの関数…?

  • Deprecated failwith(). I’m not even sure why it was here.

purrr::possibly()に当たるものらしいです。同じく使った記憶がない…

Databases

ほとんどのコードはこれまで通り動きますが(ほんとか…?)、2つ大きな変更があるとのことです。

  • DB関連のコードはdbplyrパッケージという新しいパッケージに移されました*1
  • src_*()系の関数は不要になりました。DBIのコネクションを直接渡せば動きます。↓こんな感じです。
library(dplyr)

con <- DBI::dbConnect(RSQLite::SQLite(), ":memory:")
DBI::dbWriteTable(con, "mtcars", mtcars)

mtcars2 <- tbl(con, "mtcars")
mtcars2

ちなみに後者はKirill Muller氏ががんばってDBIを改善したおかげです。次のUTF-8関連の改善もこのひとの貢献です。かっこよすぎかよ…

UTF-8

これまでこのブログでもdplyrの文字コードまわりの問題を取り上げてきましたが、

全部なおっています。

ほんとに劇的に改善しています。ぜひ試してみてください。

ちなみに内部的には、列名をシンボルに変換せずに文字列のまま扱うという方針をとっています。

  • Internally, column names are always represented as character vectors, and not as language symbols, to avoid encoding problems on Windows (#1950, #2387, #2388).

これに伴って挙動が変わっている関数もあるので、dplyrを使ってパッケージを開発されている方は注意した方がいいかもしれません。たとえばgroup_vars()は文字列を返すようになっています。

group_vars(df)
#> [1] "x" "y"

Colwise functions

  • rename(), select(), group_by(), filter() and transmute() now have scoped variants (verbs suffixed with _if(), _at() and _all()). Like mutate_all(), summarise_if(), etc, these variants apply an operation to a selection of variables.

mutate()summarise()のようにrename()select()group_by()filter()transmute()にも_if()_at()_all()というサフィックスが付くバージョンの関数ができました。 Colwise functionsの使い方に関しては以下の記事に書いたので割愛します。新しくできたやつも基本的には同じような挙動のはずです。

  • The scoped verbs taking predicates (mutate_if(), summarise_if(), etc) now support S3 objects and lazy tables. (snip)

データベースとかに対しても使えるようになったよ、ということらしいです。

Tidyeval

これまでdplyrにはNSE版の関数とSE版の関数がありましたが、これからは新しいアプローチのNSEですべてを扱います。それが「tidyeval」と呼ばれる概念です。これについては長くなりそうなので別に書きますが、一点だけ。

This means that the underscored version of each main verb is no longer needed, and so these functions have been deprecated (but remain around for backward compatibility).

「underscored version of each main verb」というのはselect_()とかmutate_()とかの話です。これがdeprecatedになって、いずれ消されます。けっこうな混乱になる予感がします。備えましょう。

Verbs

Joins

[API] xxx_join.tbl_df(na_matches = "never") treats all NA values as different from each other (and from any other value), so that they never match.

*_join()系の関数は、NAでマッチしなくなりました。これは世の中のデータベースがそういう挙動になっているためで、一貫性を持たせるためにそちらの挙動に寄せたようです。

その後の議論で、後方互換性を保つためna_matches = "na"がデフォルトになりました。na_matches = "never"を意図的に指定すると、以下のような挙動になります。

x <- tribble(
  ~name, ~value,
  "a",      1,
  "b",      2,
  "c",      NA
)

y <- tribble(
  ~name, ~value,
  "a",      1,
  "b",    100,
  "c",     NA
)

inner_join(x, y, na_matches = "never")
#> Joining, by = c("name", "value")
#> # A tibble: 1 × 2
#>    name value
#>   <chr> <dbl>
#> 1     a     1

"c", NAの行が共通にありますが、マッチしていません。これがデフォルトの挙動で、na_matches = "na"を付けるとマッチさせることができます。

inner_join(x, y, na_matches = "na")
#> Joining, by = c("name", "value")
#> # A tibble: 2 × 2
#>    name value
#>   <chr> <dbl>
#> 1     a     1
#> 2     c    NA

Select

  • For selecting variables, the first selector decides if it’s an inclusive selection (i.e., the initial column list is empty), or an exclusive selection (i.e., the initial column list contains all columns). This means that select(mtcars, contains("am"), contains("FOO"), contains("vs")) now returns again both am and vs columns like in dplyr 0.4.3 (#2275, #2289, @r2evans).

これはちょっと難しくてよくわからないけど、まあうまく動くようになったよ、ということらしいです。以下の議論を追うとわかりそうでした。

  • select() (and the internal function select_vars()) now support column names in addition to column positions. As a result, expressions like select(mtcars, "cyl") are now allowed.

サラッと書いてますが、これまでselect()select_()と使い分けていたのがもうselect()だけで大丈夫になった、ということみたいです。

つまり、以下はすべて同じ結果になります。tidyevalすらいらない感…

# 列名を変数名として指定
select(mtcars, cyl)

# 列名を文字列として指定
select(mtcars, "cyl")

# 列名を変数に入れてそれを指定
col <- "cyl"
select(mtcars, col)

Other

  • recode(), case_when() and coalesce() now support splicing of arguments with rlang’s !!! operator.

これもtidyevalの話なので詳しくは書きませんが、recode()とかでも!!!が使えるようになりました。便利そう。

  • mutate() recycles list columns of length 1 (#2171).

mutate()がリストもリサイクルしてくれるようになりました。つまり、こういう感じのことができます。

iris %>% mutate(a = list(data.frame(1:3)))

Combining and comparing

bind_rows()bind_cols()の挙動がけっこう変わっている雰囲気なので、よく使っている人は注意しましょう。

  • Breaking change: bind_rows() and combine() are more strict when coercing. Logical values are no longer coerced to integer and numeric. Date, POSIXct and other integer or double-based classes are no longer coerced to integer or double as there is chance of attributes or information being lost (#2209, @zeehio).

bind_rows()combine()が少し厳しくなりました。論理値型と数値型を一緒にしようとしたときや、日付・時刻型を数値型と一緒にしようとしたときはエラーになります。

  • bind_rows() and bind_cols() now accept vectors. They are treated as rows by the former and columns by the latter. Rows require inner names like c(col1 = 1, col2 = 2), while columns require outer names: col1 = c(1, 2). Lists are still treated as data frames but can be spliced explicitly with !!!, e.g. bind_rows(!!! x) (#1676).

bind_rows()bind_cols()がベクトルを受け付けるようになりました。

x <- c(A = 1, B = 2)
y <- c(A = 3, B = 4)
bind_rows(x, y)
#> # A tibble: 2 × 2
#>       A     B
#>   <dbl> <dbl>
#> 1     1     2
#> 2     3     4

ただし、ベクトルのリストだとdata.frameとして扱われてしまうので、bind_rows()であっても列方向にbindされてしまいます。

ll <- list(
  x = x,
  y = y
)
bind_rows(ll)
#> # A tibble: 2 × 2
#>       x     y
#>   <dbl> <dbl>
#> 1     1     3
#> 2     2     4

これもtidyevalですが、こういうときは!!!を使いましょう、とのことでした。

bind_rows(!!! ll)
#> # A tibble: 2 × 2
#>       A     B
#>   <dbl> <dbl>
#> 1     1     2
#> 2     3     4
  • After a period of deprecation, rbind_list() and rbind_all() have been removed from the package. Please use bind_rows() instead.

rbind_listrbind_allがついに消え去りました。もしまだコードの中で使っていたら注意してください。

Vector functions

細かいのがいろいろ、という感じです。

  • recode() gains .dots argument to support passing replacements as list (#2110, @jlegewie).

これも!!!があれば要らない気もするけど、.dotsで置換のリストを渡せるようになったらしいです。

Other minor changes and bug fixes

その他

別パッケージになってしまったのでdplyrのNEWSには載ってきませんが、データベース関連の変更点はdbplyrのNEWSに載っています。

例えば、吐かれるクエリが結構きれいになっているというのもわりと注目すべき点です。

SQL joins have been improved:

というあたりを読んでみてください。

感想

久々のdplyrのリリース、内部的には大幅な変更がたくさんあるので混乱しそうな予感しかしませんが、改善された機能はどれもけっこうよさげに見えます。待ち遠しいですね。

まだまだバグもあるようなので、使ってみて変なことがあればr-wakalangやTwitterあたりで私に声をかけてください。調べたりissueを上げたりとかします。憧れのLionel=サンにツイートをもらえたのがうれしくて私はがんばります。

*1:書き間違いではないです。dbplyrです。実際Hadley自身も書き間違えて事故を起こしたりしているので、もっとわかりやすい名前にすればいいのに、と思うんですけど…