group_map などの data frame を要約する関数をベンチマーク (dplyr > 0.8.x)

カテゴリ: r

tidyverse において,grouped data frame に対して grouping variables 以外の各列に関数を適用する方法は種々ある.

  • summarize: 関数の返り値が長さ1の時
  • group_map: 関数の返り値がデータフレームの時
  • nest %>% map: 関数の返り値が複雑な時

基本は上述の使い分けのようだが (help(dplyr::group_map)), 一応, summarize も返り値を list() してやると複雑な処理に対応できる (後述).

summarizenest %>% .... を比べた時に,nest が新しいオブジェクトを作るせいで遅くなりがちだと知り, summarize を偏重している1. しかし,dplyr 0.8.0 で group_map がくるし,do が deprecated になって久しいし,これらもひっくるめてベンチマークし直してみることにした.

パッケージ読み込み

pacman::p_load(
  bench, # ベンチマークするためのパッケージ
  broom, dplyr, purrr, tidyr, # ベンチマークするコードで使うパッケージ
  ggridges, knitr # ベンチマークした結果を可視化するためのパッケージ
)

ベンチマーク

mtcars_grouped_by_cyl <- mtcars %>% group_by(cyl) # 共通する操作を予め実行

res <- mark(
  "group_map" = mtcars_grouped_by_cyl %>%
    group_map(~ tidy(lm(mpg ~ disp, data = .x))),
  "nest-map-unnest" = mtcars_grouped_by_cyl %>%
    nest() %>%
    mutate(data = map(data, ~ tidy(lm(mpg ~ disp, data = .)))) %>%
    unnest(),
  "summarize-unnest" = mtcars_grouped_by_cyl %>%
    summarize(list(tidy(lm(mpg ~ disp)))) %>%
    unnest,
  "do" = mtcars_grouped_by_cyl %>%
    do(tidy(lm(mpg ~ disp, data = .))),
  min_time = Inf,
  max_iterations = 100L
) 

コードはユタニさんが group_map を紹介された記事のものを利用しています.

実行結果はこんな感じ

## # 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

結果

# summary(res) を順位で並べ替え+列選択
res_summary <- res %>% 
  summary() %>%
  arrange(mean) %>%
  select(expression, min, mean, median, max, n_itr)

# 表に出力 (S3をうまく扱えないっぽいので文字列にしておく)
gt(mutate_all(res_summary,as.character))
expression min mean median max n_itr
do 4.21ms 4.41ms 4.33ms 6.16ms 87
group_map 4.33ms 4.71ms 4.5ms 7.15ms 86
summarize-unnest 4.71ms 4.92ms 4.85ms 6.29ms 84
nest-map-unnest 6.26ms 6.57ms 6.48ms 8.45ms 79

Ridgeline 図

# res の expression は factor型だが,水準の順序を表と同じにしておく
res$expression <- fct_relevel(res$expression, rev(res_summary$expression))

# Ridgeline 図を出力
plot(res, type = "ridge") + labs(x = NULL, y = NULL)
## Picking joint bandwidth of 0.00939

箱ひげ図

plot(res, type = "boxplot") + labs(x = NULL, y = NULL)

感想と補足

実は do() が最速ということにビビらされていますが, group_map() は高速な上にシンプルに書けるので便利そうですね.

単純なデータの要約の場合は summarize を使う方が簡単に書けますし,動作も高速です. このあたりは臨機応変に.

iris %>%
  group_by(Species) %>%
  summarize_all(mean)
## # A tibble: 3 x 5
##   Species    Sepal.Length Sepal.Width Petal.Length Petal.Width
##   <fct>             <dbl>       <dbl>        <dbl>       <dbl>
## 1 setosa             5.01        3.43         1.46       0.246
## 2 versicolor         5.94        2.77         4.26       1.33 
## 3 virginica          6.59        2.97         5.55       2.03
plot(
  mark(
    "group_map" = iris %>%
      group_by(Species) %>%
      group_map(~ map_dfc(.x, mean)),
    "summarize" = iris %>%
      group_by(Species) %>%
      summarize_all(mean)
  ),
  type = "ridge"
) +
  labs(x = NULL, y = NULL)
## Picking joint bandwidth of 0.00324

Enjoy!


  1. ユタニさんとの会話