dplyr 0.8.0を使ってみた(新機能編)

困惑しそうな変更点の話は書いたので、次はテンション上がりそうな新機能の話を書きたいと思います。

group_nest()group_split()

group_nest()group_split()はいずれもグループごとにデータフレームを分割する関数です。 違いは、group_nest()だとnestされたデータフレームの列をつくります(tidyr::nest()と同じ)。 group_split()はデータフレームのリストを返します(split()と同じ)。

library(dplyr, warn.conflicts = FALSE)

mtcars %>%
  group_by(cyl) %>%
  group_nest()
#> # A tibble: 3 x 2
#>     cyl data              
#>   <dbl> <list>            
#> 1     4 <tibble [11 x 10]>
#> 2     6 <tibble [7 x 10]> 
#> 3     8 <tibble [14 x 10]>

mtcars %>%
  group_by(cyl) %>%
  group_split()
#> [[1]]
#> # A tibble: 11 x 11
#>      mpg   cyl  disp    hp  drat    wt  qsec    vs    am  gear  carb
#>    <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#>  1  22.8     4 108      93  3.85  2.32  18.6     1     1     4     1
#>  2  24.4     4 147.     62  3.69  3.19  20       1     0     4     2
#>  3  22.8     4 141.     95  3.92  3.15  22.9     1     0     4     2
#>  4  32.4     4  78.7    66  4.08  2.2   19.5     1     1     4     1
#>  5  30.4     4  75.7    52  4.93  1.62  18.5     1     1     4     2
#>  6  33.9     4  71.1    65  4.22  1.84  19.9     1     1     4     1
#>  7  21.5     4 120.     97  3.7   2.46  20.0     1     0     3     1
#>  8  27.3     4  79      66  4.08  1.94  18.9     1     1     4     1
#>  9  26       4 120.     91  4.43  2.14  16.7     0     1     5     2
#> 10  30.4     4  95.1   113  3.77  1.51  16.9     1     1     5     2
#> 11  21.4     4 121     109  4.11  2.78  18.6     1     1     4     2
#> 
#> [[2]]
#> # A tibble: 7 x 11
#>     mpg   cyl  disp    hp  drat    wt  qsec    vs    am  gear  carb
#>   <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1  21       6  160    110  3.9   2.62  16.5     0     1     4     4
#> 2  21       6  160    110  3.9   2.88  17.0     0     1     4     4
#> 3  21.4     6  258    110  3.08  3.22  19.4     1     0     3     1
#> 4  18.1     6  225    105  2.76  3.46  20.2     1     0     3     1
#> 5  19.2     6  168.   123  3.92  3.44  18.3     1     0     4     4
#> 6  17.8     6  168.   123  3.92  3.44  18.9     1     0     4     4
#> 7  19.7     6  145    175  3.62  2.77  15.5     0     1     5     6
#> 
#> [[3]]
#> # A tibble: 14 x 11
#>      mpg   cyl  disp    hp  drat    wt  qsec    vs    am  gear  carb
#>    <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#>  1  18.7     8  360    175  3.15  3.44  17.0     0     0     3     2
#>  2  14.3     8  360    245  3.21  3.57  15.8     0     0     3     4
#>  3  16.4     8  276.   180  3.07  4.07  17.4     0     0     3     3
#>  4  17.3     8  276.   180  3.07  3.73  17.6     0     0     3     3
#>  5  15.2     8  276.   180  3.07  3.78  18       0     0     3     3
#>  6  10.4     8  472    205  2.93  5.25  18.0     0     0     3     4
#>  7  10.4     8  460    215  3     5.42  17.8     0     0     3     4
#>  8  14.7     8  440    230  3.23  5.34  17.4     0     0     3     4
#>  9  15.5     8  318    150  2.76  3.52  16.9     0     0     3     2
#> 10  15.2     8  304    150  3.15  3.44  17.3     0     0     3     2
#> 11  13.3     8  350    245  3.73  3.84  15.4     0     0     3     4
#> 12  19.2     8  400    175  3.08  3.84  17.0     0     0     3     2
#> 13  15.8     8  351    264  4.22  3.17  14.5     0     1     5     4
#> 14  15       8  301    335  3.54  3.57  14.6     0     1     5     8

group_nest()group_split()は、以下のようにgroup_by()をかまさずに直接グループ化に使う列を指定することもできます。

mtcars %>%
  group_nest(cyl)

mtcars %>%
  group_split(cyl)

group_map()

do()がdeprecatedになって幾星霜、ついに決定版が来ました。 グループ化に使っている列以外の列をグループごとに分けたdata.frameに関数を適用することができます。

library(dplyr, warn.conflicts = FALSE)

mtcars %>%
  group_by(cyl) %>%
  group_map(~ broom::tidy(lm(mpg ~ disp, data = .x)))
#> # A tibble: 6 x 6
#> # Groups:   cyl [3]
#>     cyl term         estimate std.error statistic    p.value
#> * <dbl> <chr>           <dbl>     <dbl>     <dbl>      <dbl>
#> 1     4 (Intercept)  40.9       3.59       11.4   0.00000120
#> 2     4 disp         -0.135     0.0332     -4.07  0.00278   
#> 3     6 (Intercept)  19.1       2.91        6.55  0.00124   
#> 4     6 disp          0.00361   0.0156      0.232 0.826     
#> 5     8 (Intercept)  22.0       3.35        6.59  0.0000259 
#> 6     8 disp         -0.0196    0.00932    -2.11  0.0568

これは以下のようにnest()してmap()してunnest()して、というパターンとだいたい同じです(グループ化が残る点が違う)。

mtcars %>%
  group_by(cyl) %>%
  tidyr::nest() %>%
  mutate(data = purrr::map(data, ~ broom::tidy(lm(mpg ~ disp, data = .)))) %>%
  tidyr::unnest()

ちなみに、こっちはgroup_nest()group_split()と違ってグループ化の変数を直接指定することはできません。

nest_join()

nest_join()については昔にメモったgistと変わってなさそうなので再掲(unnest()の挙動は相変わらず謎...)。

group_data()group_rows()group_keys()

group_data()はすでに紹介しましたが、group_rows()group_keys()もグループの情報にアクセスするための関数です。 group_data()は全部入り、group_rows()は各グループの行のインデックス、group_keys()は各グループのキーの情報が取れます。 必要に応じて使い分けましょう。

g <- mtcars %>%
  group_by(cyl)

group_data(g)
#> # A tibble: 3 x 2
#>     cyl .rows     
#>   <dbl> <list>    
#> 1     4 <int [11]>
#> 2     6 <int [7]> 
#> 3     8 <int [14]>

group_rows(g)
#> [[1]]
#>  [1]  3  8  9 18 19 20 21 26 27 28 32
#> 
#> [[2]]
#> [1]  1  2  4  6 10 11 30
#> 
#> [[3]]
#>  [1]  5  7 12 13 14 15 16 17 22 23 24 25 29 31

group_keys(g)
#> # A tibble: 3 x 1
#>     cyl
#>   <dbl>
#> 1     4
#> 2     6
#> 3     8

group_cols()

これはgrouped_dfに対してグループ化に使っている列を表すヘルパ関数です。select()とか_at()の関数の中で使えます。 具体的にどういうときに使うかというと、公式記事に紹介されてる例がわかりやすいです。

mutate_at()とかでグループ化に使っている列まで対象になってしまったとき、その列に変更を加えることはできないのでエラーになってしまいます。

g <- mtcars %>%
  group_by(cyl)

g %>%
  mutate_at(
    vars(starts_with("c")),
    ~ . - mean(.)
  )
#> Error: Column `cyl` can't be modified because it's a grouping variable

そこで、-group_cols()を加えて明示的に対象から外してやるという手が使えます。 (そこまでわかってるなら勝手に外してほしい...とは思いつつ)

g %>%
  mutate_at(
    vars(starts_with("c"), -group_cols()),
    ~ . - mean(.)
  )
#> # A tibble: 32 x 11
#> # Groups:   cyl [3]
#>      mpg   cyl  disp    hp  drat    wt  qsec    vs    am  gear   carb
#>    <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>  <dbl>
#>  1  21       6  160    110  3.9   2.62  16.5     0     1     4  0.571
#>  2  21       6  160    110  3.9   2.88  17.0     0     1     4  0.571
#>  3  22.8     4  108     93  3.85  2.32  18.6     1     1     4 -0.545
#>  4  21.4     6  258    110  3.08  3.22  19.4     1     0     3 -2.43 
#>  5  18.7     8  360    175  3.15  3.44  17.0     0     0     3 -1.5  
#>  6  18.1     6  225    105  2.76  3.46  20.2     1     0     3 -2.43 
#>  7  14.3     8  360    245  3.21  3.57  15.8     0     0     3  0.5  
#>  8  24.4     4  147.    62  3.69  3.19  20       1     0     4  0.455
#>  9  22.8     4  141.    95  3.92  3.15  22.9     1     0     4  0.455
#> 10  19.2     6  168.   123  3.92  3.44  18.3     1     0     4  0.571
#> # ... with 22 more rows

last_col()

これもselect()のヘルパ関数のひとつです。一番最後の列を指すので、↓こんな感じで、 「n番目の列から最後の列までを選択」みたいな範囲指定で便利です。

as_tibble(mtcars) %>% 
  select(3:last_col())
#> # A tibble: 32 x 9
#>     disp    hp  drat    wt  qsec    vs    am  gear  carb
#>    <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#>  1  160    110  3.9   2.62  16.5     0     1     4     4
#>  2  160    110  3.9   2.88  17.0     0     1     4     4
#>  3  108     93  3.85  2.32  18.6     1     1     4     1
#>  4  258    110  3.08  3.22  19.4     1     0     3     1
#>  5  360    175  3.15  3.44  17.0     0     0     3     2
#>  6  225    105  2.76  3.46  20.2     1     0     3     1
#>  7  360    245  3.21  3.57  15.8     0     0     3     4
#>  8  147.    62  3.69  3.19  20       1     0     4     2
#>  9  141.    95  3.92  3.15  22.9     1     0     4     2
#> 10  168.   123  3.92  3.44  18.3     1     0     4     4
#> # ... with 22 more rows

funs()の代わりにpurrrパッケージと同じく~で関数をつくるように

さっきのmutate_at()の例でサラッと流しましたが、この~ . - mean(.)は以前であれば funs(. - mean(.))とか書いていたはずです。実はfuns()は非推奨になって、purrrパッケージと同じく~で関数をつくるようになりました。

g %>%
  mutate_at(
    vars(starts_with("c"), -group_cols()),
    ~ . - mean(.)
  )

複数の関数を指定したい場合はlist()を使います。

data.frame(x = 1:2, y = 3:4) %>% 
  mutate_all(list(plus1  = ~ . +  1,
                  plus10 = ~ . + 10))
#>   x y x_plus1 y_plus1 x_plus10 y_plus10
#> 1 1 3       2       4       11       13
#> 2 2 4       3       5       12       14

感想

あと、dplyr的に大きいのはHybrid evaluationの仕組みが変わったことなんですが、 ユーザーからするとあんまり意識することはなさそうなので省きました。興味ある人はこのvignetteとかを読んでください(私は読んでません)。

group_map()とかはまだexperimentalなので長く使うコードに取り入れるのは不安ですけど、便利っぽいなので使っていきましょう。