dplyr 0.8.0を使ってみた(group_by()のbreaking changes編)

更新(2019/02/19):

いろいろ議論があって、group_by()のデフォルトの挙動はこれまでと同じ(empty groupはつくらない)ようになりました。 group_by()するときに.drop = FALSEとした場合だけ、empty groupがつくられます。


dplyr 0.8.0がもうすぐ(来年1月上旬?)リリースされます。

この公式記事を読んでおけばいいかと思ったんですが、今回は、group_by()に大きな変更がいくつもあります。 まじで大きな変更なので、「もう俺たちが知っているgroup_by()じゃない」くらいのつもりで臨みましょう。 便利な新機能もあるんですが、それは日を改める*1ことにして、 breaking changeに絞って書きます。

factorの扱い

今のdplyrは、factorを文字列と同じように扱って、そのベクトル中に存在する値を見てグループをつくります。 一方、0.8.0からは、ベクトル中に存在しないfactorのlevelも含めてグループをつくることもできるようになります。

例えば、以下の例ではx列にcというデータはありませんが、factorのlevelとして存在しているので.drop = FALSEを指定するとcのグループができます。 summarise()の結果にcも登場しているのに注目してください。

library(dplyr, warn.conflicts = FALSE)

d <- data.frame(x = factor(c("a", "b", "b"),
                           levels = c("a", "b", "c")),
                val = 1:3)

d %>%
  group_by(x) %>%
  summarise(n = n(), mean = mean(val))
#> # A tibble: 2 x 3
#>   x         n  mean
#>   <fct> <int> <dbl>
#> 1 a         1   1  
#> 2 b         2   2.5

d %>%
  group_by(x, .drop = FALSE) %>%
  summarise(n = n(), mean = mean(val))
#> # A tibble: 3 x 3
#>   x         n  mean
#>   <fct> <int> <dbl>
#> 1 a         1   1  
#> 2 b         2   2.5
#> 3 c         0 NaN

あと、もう一つ注目すべきは、cグループのmeanNaNになっていることです。 これは、mean(numeric(0))の結果がNaNなのでこうなります。 このように、長さゼロのベクトルをうまく扱えない集計関数もあるので、 そのあたりで予期せぬエラーが起こるかもしれません。注意しましょう。

グループ情報の持ち方

これまで、group_by()によってつくられるグループ化されたデータフレーム(grouped_df)は、 グループ化に使う列名をメタデータとして持っていました。 mutate()filter()などを実行するたびにグループ分けが動的に計算されます。

一方、0.8.0からはgroup_by()した時点のグループを保持し続けることもできます。 保持するかどうかは.preserveという引数で選びます。

どのような挙動か試してみましょう。grouped_dfが持っているグループ情報はgroup_data()を使うと見ることができます(他にもいろいろ関数がありますが後述)。

d <- data.frame(group_id = c(1L, 1L, 2L, 2L),
                value    = 1:4)

g <- d %>%
  group_by(group_id)

group_data(g)
#> # A tibble: 2 x 2
#>   group_id .rows    
#>      <int> <list>   
#> 1        1 <int [2]>
#> 2        2 <int [2]>

.rowsというのはそのグループに所属する行です。

さて、このデータからgroup_id1の行をfilter()で取り除くとグループ情報がどう変わるか見てみましょう。

g1 <- g %>%
  filter(value >= 3)

group_data(g1)
#> # A tibble: 1 x 2
#>   group_id .rows    
#>      <int> <list>   
#> 1        2 <int [2]>

g2 <- g %>%
  filter(value >= 3, .preserve = TRUE)

group_data(g2)
#> # A tibble: 2 x 2
#>   group_id .rows    
#>      <int> <list>   
#> 1        1 <int [0]>
#> 2        2 <int [2]>

.preserve = TRUEを指定した方は<int [0]>と表示されているように、もうこのグループに所属する行ありません。 にも関わらず、グループとしては存在し続けています。

filter()slice()の結果の順序

これは公式記事のやつがわかりやすかったので似たようなコードを載せておきます。 xでグループ化したgrouped_dffilter()slice()を使うと、行がグループごとに寄せられた結果になります。

d <- tibble(
  x = c(1,   2, 1,  2, 1, 2), 
  y = c(1, 100, 3, 10, 5, 1)
)

d
#> # A tibble: 6 x 2
#>       x     y
#>   <dbl> <dbl>
#> 1     1     1
#> 2     2   100
#> 3     1     3
#> 4     2    10
#> 5     1     5
#> 6     2     1

d %>% 
  group_by(x) %>% 
  slice(1:2)
#> # A tibble: 4 x 2
#> # Groups:   x [2]
#>       x     y
#>   <dbl> <dbl>
#> 1     1     1
#> 2     1     3
#> 3     2   100
#> 4     2    10

グループ内の順序は入れ替わったりしないので通常は問題にならないと思いますが、 グループごとの計算結果で行を絞り込む(グループ内の順位がn位以上の行、とか)処理をしている場合は注意が必要です。 行の順序が問題になるような処理をする場合は、ちゃんと直前でarrange()するようにしましょう。

*1:つまり・・・・我々がその気になればこの記事を書くのは10年20年後ということも可能だろう・・・ということ・・・・!