須通り
Sudo Masaaki official site
For the reinstatement of
population ecology.

(dplyr::do を)使っちゃいかんのか?

ホーム | 統計 Top | Tidyverseによるデータフレーム加工(04)グループごとに一括処理する:その 1 group_by + mutate, summarise の利用

本記事の前編 「Tidyverseによるデータフレーム加工(03)dplyr_mutate によるデータ加工とその周辺」 では dplyr の機能のうち、filter(), select() といった、データフレーム内の特定条件を満たす行や列を絞り込む関数、そして mutate() や mutate_at() により、行ごとの並列処理としてデータを書き換える(その結果を新規列に追加する)方法について詳述した。

本稿では、tibble のいずれかの行に複数の水準が含まれている場合、水準ごとにデータをグループ化し、グループごとにデータ処理を行う手順を説明する。Rの標準関数でいうと tapply に近いイメージだ。ちょっと前の tidyverse では、dplyr の group_by() と do() が用いられてきたが、2018年ごろから dplyr, tidyr, purrr 等のパッケージが再設計され、nest() や map() といった関数への置き換えが進みつつある。これらを使いこなすには、「階層構造を持つデータフレーム」という概念の理解が必須である。

目次

以下、続編 「須通り_統計_Tidyverseによるデータフレーム加工(05)グループごとに一括処理する:その 2 nest および map 系関数」 へ続く。

tidyなデータフレームを group_by でグループ化する

今回は R の組み込みデータセットである mtcars を使う。旧車のスペックが表になったもので、詳細は「Rによる主成分分析 - データ科学便覧」などをご覧いただきたい。


library(tidyverse)
data(mtcars)
mtcars

mpg が燃費(マイル / ガロン)cyl が気筒数

> mtcars
                     mpg cyl  disp  hp drat    wt  qsec vs am gear carb
Mazda RX4           21.0   6 160.0 110 3.90 2.620 16.46  0  1    4    4
Mazda RX4 Wag       21.0   6 160.0 110 3.90 2.875 17.02  0  1    4    4
Datsun 710          22.8   4 108.0  93 3.85 2.320 18.61  1  1    4    1
Hornet 4 Drive      21.4   6 258.0 110 3.08 3.215 19.44  1  0    3    1
Hornet Sportabout   18.7   8 360.0 175 3.15 3.440 17.02  0  0    3    2
Valiant             18.1   6 225.0 105 2.76 3.460 20.22  1  0    3    1
(以下省略)

スペースの都合上 mtcars の詳細な構造には踏み込まない。とりあえず、車の気筒数である cyl について水準別にデータを分割する。

データフレームの列(変数)に着目して水準ごとにグループ化する dplyr::group_by 関数

tibble には「グループ化されたテーブル」という概念があり、通常のデータフレームに対する操作と同じ要領で、層別の処理を行うことができる。既存の data frame や tibble をグループ化する関数が group_by() である。 公式解説はこちら


テーブル形式のデータを、変数列に着目して水準ごとにグループ化した grouped tbl にする。
ungroup() でグループを解除する。

group_by(.data, ..., add = FALSE, .drop = group_by_drop_default(.data))
ungroup(x, ...)

省略不可な引数
.data, x    操作対象であるテーブル形式のデータ。

...    グループ化の基準に用いる変数列(複数可)。"" を付けない裸の変数名として列挙する。

オプションの引数
add    FALSE(デフォルトはこちら)のとき、すでに group_by() されている tibble に再び group_by() を適用すると、最後の group_by() に指定した変数だけがグループ化の基準として用いられる。add=TRUE とすると、既存のグループに基準を追加できる。

.drop    もし TRUE だと、空のグループの情報は保持されない。デフォルトは group_by_drop_default() の指定に従う。

試しにmtcars を気筒数 cyl でグループ化してみよう。


mtcars %>% dplyr::group_by(cyl)

> mtcars %>% dplyr::group_by(cyl)
# 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     4
 2  21       6  160    110  3.9   2.88  17.0     0     1     4     4
 3  22.8     4  108     93  3.85  2.32  18.6     1     1     4     1
 4  21.4     6  258    110  3.08  3.22  19.4     1     0     3     1
 5  18.7     8  360    175  3.15  3.44  17.0     0     0     3     2
 6  18.1     6  225    105  2.76  3.46  20.2     1     0     3     1
 7  14.3     8  360    245  3.21  3.57  15.8     0     0     3     4
 8  24.4     4  147.    62  3.69  3.19  20       1     0     4     2
 9  22.8     4  141.    95  3.92  3.15  22.9     1     0     4     2
10  19.2     6  168.   123  3.92  3.44  18.3     1     0     4     4
# … with 22 more rows

# 以下は一見動きそうだが、どうやらダメっぽい
mtcars %>% dplyr::group_by(starts_with("cy"))
mtcars %>% dplyr::group_by("cyl")

> mtcars %>% dplyr::group_by(starts_with("cy"))
 エラー: Evaluation error: No tidyselect variables were registered
Call `rlang::last_error()` to see a backtrace.

グループごとに処理を行う:group_by + mutate による方法

Tibble の列を対象に何らかの操作を行った結果を、新しい列として保存する処理を行うのが以前に説明した mutate() 関数である。公式解説はこちら。group_by でグループ化した tibble に対して mutate を適用すると、グループの水準ごとに別のスコープとして処理が実行されるようになる。


mutate(.data, ...)
transmute(.data, ...)

# スカラーないし、元のデータ長と同じベクトルを返すタイプの関数を指定した場合
# cyl のグループごとに mpg の平均を求め、mpg 各行のデータと、そいつが属する気筒数の平均燃費との差分を計算する。
tib.mutate1 <- mtcars %>%
    dplyr::group_by(cyl) %>%
    dplyr::mutate(mpg.mean=mean(mpg), mpg.diff=mpg-mean(mpg))
tib.mutate1

> tib.mutate1
# A tibble: 32 x 13
# Groups:   cyl [3]
     mpg   cyl  disp    hp  drat    wt  qsec    vs    am  gear  carb mpg.mean mpg.diff
   <dbl> <dbl> <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     19.7    1.26
 2  21       6  160    110  3.9   2.88  17.0     0     1     4     4     19.7    1.26
 3  22.8     4  108     93  3.85  2.32  18.6     1     1     4     1     26.7   -3.86
 4  21.4     6  258    110  3.08  3.22  19.4     1     0     3     1     19.7    1.66
 5  18.7     8  360    175  3.15  3.44  17.0     0     0     3     2     15.1    3.6
 6  18.1     6  225    105  2.76  3.46  20.2     1     0     3     1     19.7   -1.64
 7  14.3     8  360    245  3.21  3.57  15.8     0     0     3     4     15.1   -0.800
 8  24.4     4  147.    62  3.69  3.19  20       1     0     4     2     26.7   -2.26
 9  22.8     4  141.    95  3.92  3.15  22.9     1     0     4     2     26.7   -3.86
10  19.2     6  168.   123  3.92  3.44  18.3     1     0     4     4     19.7   -0.543
# … with 22 more rows

group_by() して mutate() することの効果は、上の例でイメージして貰えると思う。mean(mpg) が、車の気筒数 cyl ごとの平均値として求められている。次の mpg.diff = mpg - mean(mpg) という処理では、ベクトル(個々の車の燃費値)からスカラー(その気筒数グループにおける平均燃費)を引き算している。スカラーの要素は再帰的に使われ、返り値は元々の行数と一致するベクトルとなるから、元々の tibble に違和感なく新しい列として貼り付けられる。

グループごとに処理を行う:group_by + summarise による方法

上の group_by + mutate による方法に類似した、一括処理の手法が group_by + summarise である。違いは返り値の形式で、mutate の処理結果は、グループ化前の本来の tibble の行数に等しい要素数のオブジェクトを返す。一方 summarise は原則として個別の処理がスカラーを返すので、tibble全体としてはグループ数と同じ長さ、つまり水準ごとのデータを返す(だから summarise = 要約値を返す関数というのが元々の命名由来)。結果、元々あったデータの総行数である Σ_{i=1}^{n}(水準iの行数) が、水準数 n にまで縮減する。この点では次の記事で紹介予定の nest() に似ているが、 nest() と違い、key 列以外の元データが折り畳まれて保持されてはおらず、グループ化に使った key 列+処理結果の格納列だけが返る。


# 要約統計を行い、スカラーを返すタイプの関数を指定した場合
tib.summarise1 <- mtcars %>%
    dplyr::group_by(cyl) %>%
    dplyr::summarise(mpg.mean=mean(mpg), mpg.max=max(mpg))
tib.summarise1

> tib.summarise1
# A tibble: 3 x 3
    cyl mpg.mean mpg.max
  <dbl>    <dbl>   <dbl>
1     4     26.7    33.9
2     6     19.7    21.4
3     8     15.1    19.2

既存の要約関数を持ってくる分には、まあ難しくはない。 公式解説はこちら


summarise(.data, ...)
summarize(.data, ...) # 綴りはどちらでも動く

既存の tbl に含まれる変数列に要約処理を施し、1 つ以上のスカラー値を返す。group_by() でグループ化されている tbl に対しては、グループごとに 1 つの値を返す。グループ化されていなければ、出力は 1 行になる。

省略不可な引数
.data tbl 形式のオブジェクト。summarise 自体は S3クラスの総称的な関数であり、 tbl_df() や dtplyr::tbl_dt() や dbplyr::tbl_dbi() といった関数を内部的に呼び出している(須藤注:データ型に応じたこれらの関数をユーザーが決め打ちで呼ぶことも出来るが、ふつうは深く考えずに使っている)。

... 要約処理を好きなだけ、 処理結果を格納する列名=処理関数, というペア(Name-value pairs)の形で連ねて書ける。
    右辺である value はたとえば min(x) や n() や sum(is.na(y)) のように、単一の値を返す expression であるべきである。
    処理部分に含まれる引数(須藤注:典型的には min(x) の x みたいに、処理対象列を指定する)については、自動的に "" を付けてから、評価対象のデータフレームの中で対応する列を探してくる。 unquoting や splicing にも対応している。

上に述べた処理は、要約処理の関数が既に定義されており、関数を一発呼び出して出力を適当な変数名で保存すれば事足りる場合である。ときには、前処理や後処理でもう少しコードを足さねばならないフローもあり得る。この場合は、 expression として {最終的にスカラーを返す何らかの処理} を記述すればいい。


# expression として括れば、複数行にまたがる操作も可能(最終的にスカラーを返すならば)。
tib.summarise2 <- mtcars %>%
    dplyr::group_by(cyl) %>%
    dplyr::summarise(hp_wt.sd={q <- hp/wt; sd(q)})
tib.summarise2

> tib.summarise2
# A tibble: 3 x 2
    cyl hp_wt.sd
  <dbl>    <dbl>
1     4     13.8
2     6     10.9
3     8     17.1

summarise における無名関数の即時評価とラムダ式の扱い

一方、上記と似た処理を無名関数で書こうとする場合は、ちょっとしたコツが必要である。


# 以下のような無名関数の記述方法はエラーになる
mtcars %>%
    dplyr::group_by(cyl) %>%
    dplyr::summarise(hp_wt.mean=function() {mean(hp/wt)})
 エラー: Column `hp_wt.mean` is of unsupported type function

# 以下のような無名関数の記述方法はエラーになる
mtcars %>%
    dplyr::group_by(cyl) %>%
    dplyr::summarise(hp_wt.mean=function(hp, wt) {mean(hp/wt)})
 エラー: Column `hp_wt.mean` is of unsupported type function

つまり「関数の実行結果」ではなく「関数オブジェクト」が入っており、このタイプのオブジェクトをsummarise が受け取って格納できないのでエラーになる。


# 以下のように、即時関数(Immediately invoked function expression)として記述すると動く。
mtcars %>%
    dplyr::group_by(cyl) %>%
    dplyr::summarise(hp_wt.mean=(function(x, y) {mean(x/y)})(x=hp, y=wt))

# A tibble: 3 x 2
    cyl hp_wt.mean
    <dbl>    <dbl>
1     4       37.9
2     6       39.9
3     8       53.9

# 即時関数とは何のことやらというと、こんな感じ
> (function(x)print(x+1))(3)
[1] 4
> function(x)print(x+1)(3)
function(x)print(x+1)(3)

どうやら即時評価させることが肝要っぽい。色々実験して分かったのは、summarise は(次の記事で紹介する map も) mutate で受けるまで式が評価されないようだ。これを避けるには () で即時関数化する必要がある(蛇足:map はラムダ式も受けられるが、mutate で受けない場合は ~関数() ではなく ~(関数()) として評価させる必要がある)。

またラムダ式で書こうとすると、summarise() が受取可能な処理として対応していないためエラーになる。


# ラムダ式で summarise の処理を書くとエラーになる
mtcars %>%
    dplyr::group_by(cyl) %>%
    dplyr::summarise(hp_wt.sd=~sd(hp/wt))
 エラー: Column `hp_wt.sd` is of unsupported type quoted call

mtcars %>%
    dplyr::group_by(cyl) %>%
    dplyr::summarise(hp_wt.sd=~{sd(hp/wt)})
 エラー: Column `hp_wt.sd` is of unsupported type quoted call

実は厄介なことに、summarise_all() や summarise_at()は、リストで括るとラムダ式を処理できる。


# summarise_all() は、リストで括るとラムダ式を正しく処理できる。
mtcars %>%
    dplyr::group_by(cyl) %>%
    dplyr::summarise_all(list(~max(.)))

# A tibble: 3 x 11
    cyl   mpg  disp    hp  drat    wt  qsec    vs    am  gear  carb
  <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1     4  33.9  147.   113  4.93  3.19  22.9     1     1     5     2
2     6  21.4  258    175  3.92  3.46  20.2     1     1     5     6
3     8  19.2  472    335  4.22  5.42  18       0     1     5     8

真似して summarise() で処理内容をリストで括ると、ラムダ式が評価されずに、式そのものが値として格納される。


# summarise() で処理内容をリストで括ると
result <- mtcars %>%
    dplyr::group_by(cyl) %>%
    dplyr::summarise(max=list(~max(.)))
result
result[1, 2][[1]]

> result
# A tibble: 3 x 2
    cyl max
  <dbl> <list>
1     4 <formula>
2     6 <formula>
3     8 <formula>

> result[1, 2][[1]]
[[1]]
~max(.)
<environment: 0x0000000028aedc10>

というわけで、summarise() がラムダ式を評価しないため、上記の小細工は通用しない。しかし、リストで括るとエラーを回避できる。

summarise は複数の値を返せるか?

上記からも分かるように、残念ながら summarise で書ける処理は、基本的に mean とか sum とか max とか、データの組を受け取って1つのスカラー値を返すような単純な関数や、最終的にスカラー値を返す expression の範疇である。つまり要素数 1 かつ格納可能なデータ形式のオブジェクトを返さねばならず、返り値がベクトルやリストになるような関数は summarise の受け入れ対象外となる。


# 返り値が複数の値を含む(ベクトル、リスト)関数を summarise で適用できるか?
mtcars %>%
    dplyr::group_by(cyl) %>%
    dplyr::summarise(mpg.quantile=quantile(mpg))

 エラー: Column `mpg.quantile` must be length 1 (a summary value), not 5

mtcars %>%
    dplyr::group_by(cyl) %>%
    dplyr::summarise(hp_wt=hp-wt, mpg.quantile=quantile(mpg))

 エラー: Column `hp_wt` must be length 1 (a summary value), not 11

返り値全体をリストで囲ってもダメである。


mtcars %>%
    dplyr::group_by(cyl) %>%
    dplyr::summarise(zentai_list=list(mpg.mean=mean(mpg), mpg.max=max(mpg)))

 エラー: Column `zentai_list` must be length 1 (a summary value), not 2

でも、せっかく R で高度な統計解析をやるのだから、グループごとに複雑な処理を行った結果を保存したいのが人の心である。だったらこちらで扱ったように、返り値の本体は1つの値だが、属性 attribute として大量の情報を付随させることはできるか?


# 評価中に無理やり属性情報をはめ込んでみた例。
res <- mtcars %>%
    dplyr::group_by(cyl) %>%
    dplyr::summarise(hp_wt={q <- hp/wt; sdq <- sd(q); attr(sdq, "data.all") <- q; sdq})
res
res[1, "hp_wt"][[1]]

> res
# A tibble: 3 x 2
    cyl hp_wt
  <dbl> <dbl>
1     4  13.8
2     6  10.9
3     8  17.1

> res[1, "hp_wt"][[1]]
[1] 13.78213

残念ながら属性情報は落ちてしまう。

   _
.'´ヘ   ヘ
! ノリノ)))》
i从! ´‐`ノリ  ちくしょう・・・
.__,冖__ ,、  __冖__   / //
`,-. -、'ヽ' └ァ --'、 〔/ /  ,. ‐ ''    ̄ ̄" ‐
ヽ_'_ノ)_ノ    `r=_ノ    / ゙               ヽ
.__,冖__ ,、   ,へ    /  ,ィ                ,-―'`ヽ
`,-. -、'ヽ'   く <´   7_//           ,ヘ--‐ヽ‐゙へ   ヽ、
ヽ_'_ノ)_ノ    \>     / ,\__,,. ―i ̄   ', '., \  .\  ヾ,
  n     「 |      /  i7´  l´   i     i i  ヽ  ヽ  .',゙.,
  ll     || .,ヘ   /   i/   .i    i      i i   i   ヽ  , ,
  ll     ヽ二ノ__  {   i .l l |  l l l     l  l   l  ヽ ', i .i
  l|         _| ゙っ  ̄フ .i i i  i  i .l     l  .l  .l i  i l  i i
  |l        (,・_,゙>  / .i i  i i  i i i l_   、l|  i  | i  i | |  .l l
  ll     __,冖__ ,、  >  | i_,,,.L.|+‐||ii.l''「,l.,   ./iト-| ,,|,」|_l .i .| | l .l l
  l|     `,-. -、'ヽ'  \ .| | | i,| i,.|.ii ', ゙,ヽ  /ii.| / | /レii.l .i l .l .| .| |
  |l     ヽ_'_ノ)_ノ   トー .l |ヽl il__ii_i_ i  ヾ,/ ノ レ__|/  ii l l /レ  | | |
  ll     __,冖__ ,、 | |   | .l Or" ̄~~`      '" ̄`Ol /l/   .| | l
  ll     `,-. -、'ヽ' i  l   ト ゙ ,         、     .lノ /|   |, |, ',
  |l     ヽ_'_ノ)_ノ  {l  l   .lヾ、     ,―-┐     l |/ |   | ', l ヾ、
.n. n. n        l  l l   lヽヽ.    l   l     イ  /|   | l、l
..|!  |!  |!         l  i i   .l. `' ,  ヽ___ノ ,. ‐ " / | / |   lli .| ヾ
..o  o  o      ,へ l .|、 lヽ  .l,   ` ‐ ._ ' ヽ|/ | /-| /_ .|   / ii
          /  ヽヽl ヽ ヽ  l ` ‐ ,_|_,./   |.レ  レ  ゙|  /

以下、続編 「須通り_統計_Tidyverseによるデータフレーム加工(05)グループごとに一括処理する:その 2 nest および map 系関数」 へ続く。

グループごとに処理を行う:group_by + do による方法

既に古くなった技術だが、一応 dplyr::do を使ったコード例も載せておく。


# 評価中に無理やり属性情報をはめ込めなかった例。
res <- mtcars %>%
    dplyr::group_by(cyl) %>%
    dplyr::do({q <- .$hp/.$wt; sdq <- sd(q); attr(sdq, "data.all") <- q; data.frame(sdq)})
res

> res
# A tibble: 3 x 2
# Groups:   cyl [3]
    cyl   sdq
  <dbl> <dbl>
1     4  13.8
2     6  10.9
3     8  17.1

見ての通り summarise に近い技術で、結構何でも出来る。ただし summarise に比べると変数の指定に .$hp などとする必要があったりして、記述があまり簡単にならないのが難点。