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

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

ホーム | 統計 Top | Tidyverseによるデータフレーム加工(05)グループごとに一括処理する:その 2 nest および map 系関数

本記事の前編 「Tidyverseによるデータフレーム加工(04)グループごとに一括処理する:その 1 group_by + mutate, summarise の利用」 では dplyr の group_by, mutate, summarise に注目し、tibble のいずれかの行に複数の水準が含まれている場合、水準ごとにデータをグループ化し、グループごとにデータ処理を行う手順を説明してきた。しかし返り値が複数の要素を含むような処理を書くと、出力を summarise で受けられないという問題が発生した。今回は回答編であり、主に map 系関数を用いてこの問題に対処する。またその前段として、「階層化された tibble」の概念を導入したい。

なお残念なことに、計算結果を nest された tibble に格納するところまでは本記事でカバーしたが、字数の関係で取り出し方を紹介できない。続編 「Tidyverseによるデータフレーム加工(06)グループごとに一括処理する:その 3 nest された tibble からのデータ取り出しと階層解除」 で取り扱う予定である。

目次

本記事は R 3.6.1 上で動作する、以下の tidyverse のバージョンで検証している。


> library(tidyverse)
-- Attaching packages --------------------------------- tidyverse 1.2.1 --
√ ggplot2 3.2.0     √ purrr   0.3.2
√ tibble  2.1.3     √ dplyr   0.8.3
√ tidyr   1.0.0     √ stringr 1.4.0
√ readr   1.3.1     √ forcats 0.4.0
-- Conflicts ------------------------------------ tidyverse_conflicts() --
x dplyr::filter() masks stats::filter()
x dplyr::lag()    masks stats::lag()

前回に引き続き R の組み込みデータセットである mtcars を使う。


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
(以下省略)

tidyなデータフレームを nest で階層化する

map による一括処理の手順に入る前に、今回必要となる「階層化された tibble」の概念を導入したい。

グループに基づいて入れ子(階層化)された tibble を作る tidyr::nest 関数


nest(.data, ..., .key = deprecated())

省略不可な引数
.data    操作対象であるデータフレーム。

...    グループ化の基準に用いる変数列(複数可)。"" を付けない裸の変数名として列挙する。
Name-variable pairs of the form new_col = c(col1, col2, col3), that describe how you wish to nest existing columns into new columns. The right hand side can be any expression supported by tidyselect.

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


mtcars.nested <- mtcars %>% dplyr::group_by(cyl) %>% tidyr::nest()
mtcars.nested

# A tibble: 3 x 2
    cyl data
  <dbl> <list>
1     6 <tibble [7 × 10]>
2     4 <tibble [11 × 10]>
3     8 <tibble [14 × 10]>

> mtcars.nested[1, "data"]
# A tibble: 1 x 1
  data
  <list>
1 <tibble [7 × 10]>

# mtcars.nested の data 列の実体は「tibbleを束ねたリスト」
> mtcars.nested$data
<list_of<
  tbl_df<
    mpg : double
(中略)
    carb: double
  >
>[3]>
[[1]]
# A tibble: 7 x 10
    mpg  disp    hp  drat    wt  qsec    vs    am  gear  carb
  <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1  21    160    110  3.9   2.62  16.5     0     1     4     4
(中略)
7  19.7  145    175  3.62  2.77  15.5     0     1     5     6

[[2]]
# A tibble: 11 x 10
     mpg  disp    hp  drat    wt  qsec    vs    am  gear  carb
   <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
 1  22.8 108      93  3.85  2.32  18.6     1     1     4     1
(以下略)

# なので、要素を裸で取り出すには [[n]] の記法が必要
> mtcars.nested[1, "data"][[1]]
[[1]]
# A tibble: 7 x 10
    mpg  disp    hp  drat    wt  qsec    vs    am  gear  carb
  <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1  21    160    110  3.9   2.62  16.5     0     1     4     4
2  21    160    110  3.9   2.88  17.0     0     1     4     4
3  21.4  258    110  3.08  3.22  19.4     1     0     3     1
4  18.1  225    105  2.76  3.46  20.2     1     0     3     1
5  19.2  168.   123  3.92  3.44  18.3     1     0     4     4
6  17.8  168.   123  3.92  3.44  18.9     1     0     4     4
7  19.7  145    175  3.62  2.77  15.5     0     1     5     6

この「階層化された tibble」は、複数の水準を含む巨大なデータセットに、何らかの複雑な一括処理を行い、その結果を受け取ってよろづのことに使ふための基盤データ形式として活用できる。

なお tidyr::nest(-cyl) で nested tibble を作ることも可能で、dplyr::group_by(cyl) %>% tidyr::nest() との違いはグループ化の有無である。


# group_by() して nest() すると、grouping の属性情報を含んだ nested tibble になる。
mtcars %>%
    dplyr::group_by(cyl) %>%
    tidyr::nest()

# A tibble: 3 x 2
# Groups:   cyl [3]
    cyl            data
  <dbl> <list<df[,10]>>
1     6        [7 × 10]
2     4       [11 × 10]
3     8       [14 × 10]

# いきなり除外列を指定して nest() すると、grouping の属性情報を含まない
# 要するに、単に「折りたたみ時に cyl 列が除外された」だけの扱いである。
mtcars %>%
    tidyr::nest(-cyl)

# A tibble: 3 x 2
    cyl            data
  <dbl> <list<df[,10]>>
1     6        [7 × 10]
2     4       [11 × 10]
3     8       [14 × 10]
 警告メッセージ:
All elements of `...` must be named.
Did you want `data = c(mpg, disp, hp, drat, wt, qsec, vs, am, gear, carb)`?

前回紹介した group_by() は、ユーザーの目からはあくまで元データの全体を1枚の tibble として見せつつ、内部的には層別処理を行えるようにお膳立てする操作だった。本記事の一番下に str で実際のデータ構造を調べた結果が載っているが、grouped tibble は元の tibble と実体は同じであり、属性情報を付加することでグループが指定されている。

一方 nest() により、グループごとに分割された tibble が作成されて $data という変数列の要素として格納された、要するに入れ子状の tibble を作ることができる。こちらはデータの実体として構造が作り変えられており、要素へのアクセスには注意が必要である。 公式解説はこちら

nest() で階層化された tibble を flatten する tidyr::unnest 関数

nest() で階層化された tibble は unnest() で階層のない状態に戻せる(flatten)が、実は結構厄介な関数である。使い方は追々説明していく。


unnest( data, cols, ...,
        keep_empty=FALSE, ptype=NULL,names_sep=NULL, names_repair="check_unique",
        .drop=deprecated(), .id=deprecated(), .sep=deprecated(), .preserve=deprecated())

省略不可な引数
data    操作対象であるデータフレーム。

cols    階層化を解除する変数列。複数の変数の組み合わせで nest されていた場合、cols に特定のキーだけを記述すると、記述しなかった変数については階層状態を維持できる。ただし(何らかの加工後に)flatten された列において、各水準のデータ長が、本来あるべき行数に合致しないとヤバいことになる。

追加の引数
keep_empty    デフォルトでは、unchopping/unnesting するリストの要素ごとにつき、1つのデータ行が出力される。もしリストに空の要素があると、これに対応する行全体が出力において消されてしまう。keep_empty=TRUE を指定することで、欠損値を入れたものとして行を残しておくことができる。

ptype    出力される列のプロトタイプとして、既存のデータフレームを指定しておくと、unnest されて復帰するデータで対応部分を適宜書き換えたものを出力してくれる。

names_sep    デフォルトは NULL で、unnest() で上の階層に戻ってくる列たちの名前を、nest内に入っていたときの列名のままにする。文字列を指定すると、nest の名前と内部にあった列名を使って paste(nestの名前, 内部にあった列名, sep=name_sep) みたいな形で結合したものが、新たな列名として使われる。

names_repair    出力されるデータフレームの列名が有効かどうかをチェックし、修復する(デフォルトは、全く同じ列名が複数出てこないかのチェックだけ行う)。様々なオプションがあるが省略。

グループごとに複雑な処理を行う:map による方法

さて nest() の導入が済んだので、いよいよ一括処理の紹介に移る。

しばしば tidyverse の一括処理系のチュートリアルで使われている処理が、lm による線形モデル当てはめである。今回は lm の当てはめと結果の格納、取り出しを題材にとり、様々なワークフローを検討してゆく。前回述べたように、 group_by して summarise というパターンは返り値が(グループごとに)要素長 1 の場合に大変有用だが、lm を内部で適用すると、返り値は list なのでエラーになってしまう。


# 何も考えずに lm して summarise にぶち込んだ例
mtcars %>%
    dplyr::group_by(cyl) %>%
    dplyr::summarise(mpg.lm=lm(mpg~disp, data=.))

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

そこで group_by + summarise による方法に類似した、しかし返り値が複数要素となるデータを返せる方法として、purrr パッケージの map() 関数とその眷属を導入する。

入力の各要素に関数を適用して変形を行い、入力と同じ長さのデータを出力する purrr::map 関数ファミリー


map() 関数は常にリストを返す。入力と同じデータ型を必ず返したければ、modify() ファミリーの関数を検討せよ。
map_lgl(), map_int(), map_dbl(), および map_chr() は、それぞれの名前に示されたデータ型に変換した(出来ないとエラーになる)ベクトルを返す。
map_dfr() および map_dfc() はデータフレームを返す。なお _dfr は row-binding を、 _dfc は column-binding を行う。

処理内容である .f の返り値は、入力である .x の各要素について長さ 1 にならねばならない。もしも .f が extractor function shortcut を用いるならば、それに対応する入力値の変数列が存在しないか、値が空であるときに充てがう値を .default として指定できる(詳細は as_mapper() を見よ)。
walks() は .f を副作用として呼び出し、関数自体の出力は、入力である .x をそのまま返す。

map(.x, .f, ...)
map_lgl(.x, .f, ...)
map_chr(.x, .f, ...)
map_int(.x, .f, ...)
map_dbl(.x, .f, ...)
map_raw(.x, .f, ...)
map_dfr(.x, .f, ..., .id = NULL)
map_dfc(.x, .f, ...)
walk(.x, .f, ...)

省略不可な引数
.x    リストもしくはベクトル(atomic vector)。処理対象のデータ。

.f    関数、式、ないしベクトル(atomic でなくてもよい)
関数であればそのまま使われる。
式であれば(例: ~.x+2)関数に変換される。
それらの処理を書く中で、引数自体に言及する方法は3つある。これらの構文を用いると無名関数をコンパクトに書ける。
1) 引数が1つしか無い関数であれば . で表す。
2) 2つの引数を持つ関数であれば .x と .y で表す。
3) 3つ以上の引数を持つ関数であれば、 ..1, ..2, ..3, などとして表す。

.f の中身を文字列ベクトル、数値ベクトル、ないしリストとして与えると、extractor function に変換される。文字列ベクトルは name によってインデックス化され、数値ベクトルは場所によってインデックス化される。場所によるインデックスと名前によるインデックスを階層依存で使い分けたい場合はリストを使う。あるコンポーネントが存在しない場合は、 .default に指定した値を入れて返す。

...    mapされる関数に指定したい、追加の引数。

.id    map_dfr() だけに存在するオプションの引数で、*_dfr の関数だけに適用される。デフォルトは NULL だが、文字列を指定した場合、その名前で出力に新たな変数(列)が作られ、入力におけるインデックスの値がそこに格納される。

値
map() 関数は常に .x と同じ要素数のリストを返す。
map_lgl(), map_int(), map_dbl(), および map_chr() は、それぞれの名前に示されたデータ型のベクトルを返す。
map_df(), map_dfr(), および map_dfc() はデータフレームを返す。

入力データ .x の要素に名前がついていれば、その情報は可能な限り出力データにも残される。

なお、 .f の出力するデータが真偽値で書ける(0, 1 の2値しか出ない)場合はデータ型が logical になり、整数しか出なければ integer に、実数のみ含むならば double に、数で表せない文字情報まで含む場合は character に、適宜変換される(ただし一方通行であり、元々 "0", "1" だったデータを勝手に 0, 1 へ変換するようなことはない)。

walks() は入力である .x をそのまま返す。パイプの途中でデータ .x をそのまま保持しつつ、外部に何らかの干渉を行いたい場合に便利。

nest + mutate + map(+ ラムダ式)による方法

最初に、group_by&nest してから mutate で列を作成する操作を行い、その中で map を適用することで行単位の並列処理を実現するという、tidyverse ベースのスタンダードな並列化手法を紹介する。

(2020/07/24 追記:色々な書き方を1年ほど試した結果、拙は本手法が最も書きやすいとの個人的確信に至った)


# map の出力を mutate で受ける書き方の基本例

tib.lm.map <- mtcars %>%
    dplyr::group_by(cyl) %>%
    tidyr::nest() %>%
    dplyr::mutate(lm=purrr::map(.x=data, .f=~lm(mpg~disp, data=.)))

tib.lm.map # 以下の出力結果を得る。右端の lm 列が当てはめ結果のオブジェクト。

# A tibble: 3 x 3
# Groups:   cyl [3]
    cyl            data lm
  <dbl> <list<df[,10]>> <list>
1     6        [7 × 10] <lm>
2     4       [11 × 10] <lm>
3     8       [14 × 10] <lm>

というわけで purrr::map() の .f に処理内容を記述し、mutate でこれを評価して結果を新規列に代入できる。先に nest() で折り畳んであるので、出力される tibble はグループ数と一致する 3 行である。

パイプ処理の結果が mutate の第1引数に渡されているので、mutate 関数のスコープ内では、グループ&ネストした mtcars の各行(=各水準)が見えている形になる。そのおかげで map 関数の第1引数 .x には、 data 列を裸で放り込むことができる。

map 関数の処理内容を記述する .f のスコープ内では、data 列の中身であるデータフレームを .x ないし . としてアクセスできる。そして lm() においてデータセットを data=. として指定しているので、線形モデルの形を定義する formula の中では mpg~disp などとして、変数列を裸で記述することができる(2020.07.23 追記:通常は、後に紹介する pluck 関数を使い、 pluck( ., "mgp" ) などとして data 内の列にアクセスする)。

なおネット上で紹介されている方法には、しばしば map して brume したり tidy したりすると書かれている。たしかに「lm した結果」は処理できる。問題は、broom や tidy が受け取れるオブジェクトのクラスは、それ専用の処理がすでに実装されているものに限られており、任意の処理結果を扱えるわけではない点である。そこで本記事では、mapが返す値そのものをしっかり観察し、適切なデータの取り出し方を考えたい。

mutate+map の.f のスコープ内でも他の変数列を参照できる

(2020.07.17 追記) なお上に書いたように、map 関数の処理内容を記述する .f のスコープ内では、data 列の実体であるデータフレームそのものを .x ないし . としてアクセスできる。一方、元々の tibble の列にもアクセスしたければ、単純に変数名を裸で書けばいい。例えば以下の処理は可能である。


# mutate の中の map の .f スコープ内で、 .x 以外の変数を参照する例
mtcars %>%
    dplyr::group_by(cyl) %>%
    tidyr::nest() %>%
    dplyr::mutate(data.head=purrr::map(.x=data, .f=~head(., cyl)))

出力結果は省略するが、 data2 の各行要素はそれぞれ 6 行, 4 行, 8 行の tibble になる。これを知っているか否かで、スクリプトの書きやすさが段違いなので、ぜひ覚えておこう。

nest → map 後の nest の解除は簡単か?

残念ながら非常に厄介。単純に unnest() で折り畳みを戻そうとすると、新たに作られた lm 列の各要素であるリストの行数が、各グループのデータ点数(行数すなわち 7, 11, 14)と一致しないため、エラーになる。


# 上記結果を、そのまま unnest() で折り畳み解除しようとするとエラー。
unnest(tib.lm.map)

 エラー: Input must be list of vectors
 追加情報:  警告メッセージ:
`cols` is now required.
Please use `cols = c(data, lm)`

以下の例のように、要素数が(結果的に)一致するか、スカラー(再帰的に使い回される)であれば、unnest() 可能である。


# 出力がスカラーなので unnest 時に使い回される例。
mtcars %>%
    dplyr::group_by(cyl) %>%
    tidyr::nest() %>%
    dplyr::mutate(meandisp=purrr::map(.x=data, .f=~mean(.$disp))) %>%
    tidyr::unnest(cols=-cyl)

# A tibble: 32 x 12
# Groups:   cyl [3]
     cyl   mpg  disp    hp  drat    wt  qsec    vs    am  gear  carb meandisp
   <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>    <dbl>
 1     6  21    160    110  3.9   2.62  16.5     0     1     4     4     183.
 2     6  21    160    110  3.9   2.88  17.0     0     1     4     4     183.
 3     6  21.4  258    110  3.08  3.22  19.4     1     0     3     1     183.
 4     6  18.1  225    105  2.76  3.46  20.2     1     0     3     1     183.
 5     6  19.2  168.   123  3.92  3.44  18.3     1     0     4     4     183.
 6     6  17.8  168.   123  3.92  3.44  18.9     1     0     4     4     183.
 7     6  19.7  145    175  3.62  2.77  15.5     0     1     5     6     183.
 8     4  22.8  108     93  3.85  2.32  18.6     1     1     4     1     105.
 9     4  24.4  147.    62  3.69  3.19  20       1     0     4     2     105.
10     4  22.8  141.    95  3.92  3.15  22.9     1     0     4     2     105.
# … with 22 more rows

lm の当てはめ例において、predict による予測値を取り出してから unnest する方法は、次回解説する

nest + map(+ ラムダ式)による方法

もちろん、group_by + nest してから、 mutate を使わずに map へ流すことも可能である。ただし、 lm に流し込むデータを取り出す方法が、かなり面倒。Tibble 全体を map に流してから内部で data 列を取り出すのが容易ではないためで、次善策として pull(.data, var="data") で data 列だけを抽出してから map に放り込むとよい。


# map の出力を mutate で受けない書き方の例。まず入力をそのまま観察。
mtcars %>%
    dplyr::group_by(cyl) %>%
    tidyr::nest() %>%
    dplyr::pull(var="data") %>%
    purrr::map(~.)

[[1]]
# A tibble: 7 x 10
    mpg  disp    hp  drat    wt  qsec    vs    am  gear  carb
  <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1  21    160    110  3.9   2.62  16.5     0     1     4     4
2  21    160    110  3.9   2.88  17.0     0     1     4     4
3  21.4  258    110  3.08  3.22  19.4     1     0     3     1
(以下略)

というわけで出力は data 列だけを取り出して、リストにしたものである。pull() 関数を使っているのがコツで、実は select だと data 列だけを拾ってくることができない。もちろん pull で取り出した単なるリストについては、map に放り込んで適当な処理をするのは難しくない。ただ本方針だと cyl の情報が抜けてしまうので、次に示す split を用いた書き方のほうが実用的だと思う。


# map の出力を mutate で受けない書き方の例。data だけ取り出して lm に放り込んでみる。
mtcars %>%
    dplyr::group_by(cyl) %>%
    tidyr::nest() %>%
    dplyr::pull(var="data") %>%
    purrr::map(.f=~lm(mpg~wt, data=.))

# 結果は tibble ではなくリストである。

[[1]]

Call:
lm(formula = mpg ~ wt, data = .)

Coefficients:
(Intercept)           wt
      28.41        -2.78


[[2]]

Call:
lm(formula = mpg ~ wt, data = .)

Coefficients:
(Intercept)           wt
     39.571       -5.647


[[3]]

Call:
lm(formula = mpg ~ wt, data = .)

Coefficients:
(Intercept)           wt
     23.868       -2.192

split + map(+ラムダ式)による方法

一方 group_by ではなく split で tibble を水準別のリストに分割してから、要素ごとに map を適用するというフローもある。元の tibble が保持されないのでパイプライン中では使いにくいが、挙動自体は後述の group_map より素直。


# split で書く方法(map の公式ヘルプより)
mtcars %>%
    split(.$cyl) %>%
    map(~lm(mpg~wt, data=.x))

$`4`

Call:
lm(formula = mpg ~ wt, data = .x)

Coefficients:
(Intercept)           wt
     39.571       -5.647


$`6`

Call:
lm(formula = mpg ~ wt, data = .x)

Coefficients:
(Intercept)           wt
      28.41        -2.78


$`8`

Call:
lm(formula = mpg ~ wt, data = .x)

Coefficients:
(Intercept)           wt
     23.868       -2.192

すぐ上に示した group_by + nest + pull + map による方法と、こちらの split + map を使う方法とでは、出力形式に若干の違いがある。前者はリストの要素に名前が付いておらず [[1]], [[2]], [[3]] などとなっているが、グループの並び順は元の tibble に登場する通り cyl == 6 ,4, 8 の順が保存されている。後者はリストの要素名に cyl の値が付いていて視認性が高いが、なぜか並び順が 4, 6, 8 とソートされている(正確には split した時点でソートされており、 map は順番を弄くらない)。こうしたデータ順の勝手なソートは、自動処理を書く上で大きな問題となる。次のセクションでもう少し詳しく議論する。

group_map による方法

次に紹介するのが dplyr パッケージの group_map() という関数を使う方法である。purrr の関数に似た機能を持つが dplyr の一員だ。不思議やね。


グループ化された tibble に反復処理を施すための purrr スタイルの関数

group_map(.tbl, .f, ..., keep = FALSE)
group_modify(.tbl, .f, ..., keep = FALSE)
group_walk(.tbl, .f, ...)

省略不可の引数
.tbl    グループ化された tibble

.f    各グループに適用すべき関数ないしフォーミュラ。データフレームを返さねばならない(須藤注:このルールが厳格なのは group_modify)。関数であればそのまま評価される。少なくとも2つの仮引数を持つべきである。

たとえば ~head(.x) 等のフォーミュラであれば、関数に変換されてから評価される。フォーミュラ内では . ないし .x として、所与のグループに属する .tbl のデータ行にアクセスできる。.y で key、すなわちグループを定義するために使われているグルーピング変数を中身に持つ 1 x 1 サイズの tibble にアクセスできる。

...    .f に渡される追加引数。
keep    グルーピングに用いた変数を .x 内に残したいか。デフォルトは FALSE

値
group_modify() はグループ化された tibble を返す。 .f は厳密にデータフレームを返す処理でなければならない。
group_map() は、各グループに対し .f を適用した結果をリストにまとめて返す。
group_walk() は .f を副作用として実行し、(invisible な)返り値は .tbl そのままである。パイプライン途中の状態を plot したり、環境を書き換えたい場合などに使えるらしい。

さっそく、 group_map で lm への当てはめを行う。


# group_map を使う書き方
tib.lm.gmap <- mtcars %>%
    dplyr::group_by(cyl) %>% # nest しない
#    dplyr::group_map(~broom::tidy(lm(mpg~disp, data=.x))) # broom::tidy を用いた整形がしばしば紹介されるが、
    dplyr::group_map(~lm(mpg~disp, data=.x)) # こちらが本来の出力形式。

# 実は data=.x でも data=. でも書ける
mtcars %>% group_by(cyl) %>% group_map(~lm(mpg~disp, data=.))

で、以下の出力を得る。


tib.lm.gmap
str(tib.lm.gmap[[1]])

> tib.lm.lam
[[1]]

Call:
lm(formula = mpg ~ disp, data = .x)

Coefficients:
(Intercept)         disp
    40.8720      -0.1351


[[2]]

Call:
lm(formula = mpg ~ disp, data = .x)

Coefficients:
(Intercept)         disp
  19.081987     0.003605


[[3]]

Call:
lm(formula = mpg ~ disp, data = .x)

Coefficients:
(Intercept)         disp
   22.03280     -0.01963

この時、水準は以下の並びになっている。


mtcars %>% group_by(cyl) %>% group_map(~.y)

[[1]]
# A tibble: 1 x 1
    cyl
  <dbl>
1     4

[[2]]
# A tibble: 1 x 1
    cyl
  <dbl>
1     6

[[3]]
# A tibble: 1 x 1
    cyl
  <dbl>
1     8

元の mtcars における cyl の登場順は 6, 4, 8 で、group_by + nest するとやはり 6, 4, 8 の順になるのが本来の並び順だが、 group_map に通した段階で結果の並び順がソートされており、元データと一致しなくなる。つまり先ほどの split と同じ問題だが、今度はリストの要素名として情報が保持されることすら無いので、さらに厄介な状況である。

group_map の出力の並び順には要注意

さて、 group_map() の返り値は水準ごとに実行結果をまとめたリストなので、元の(group_by ないし nest された)tibble に結合させるには、大人しく実行後にくっつけたほうがいい。ただし上の実施例から分かるように「group_map() の実行結果は水準についてソートされている」ので注意。


# 水準と任意の処理結果をいずれも group_map() で取り出しておいて、後で結合する方法。
tib.lm.gmap <- mtcars %>% group_by(cyl) %>% group_map(~lm(mpg~disp, data=.x)) # cyl == 4, 6, 8 の順
tib.levels <- mtcars %>% group_by(cyl) %>% group_map(~.y) # cyl == 4, 6, 8 の順
tib.lm.nested <- tibble(cyl=unlist(tib.levels), lm=tib.lm.gmap) # cyl == 4, 6, 8 の順
tib.lm.nested
str(tib.lm.nested[2, "lm"][[1]]) # cyl == 6 のケース。データ点数 7

なお水準がソートされているが、 cyl も lm も同様にソートされておりデータ自体は正しい。

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

元の tibble のデータを全部残しつつ結合したければ、原理上は以下の方法も可能だが、group_map() の結果だけが水準についてソートされるので、結合するとヤバいことになる。


tib.lm.gmap <- mtcars %>% group_by(cyl) %>% group_map(~lm(mpg~disp, data=.x)) # cyl == 4, 6, 8 の順
tib.nested <- mtcars %>% group_by(cyl) %>% nest() # cyl == 6, 4, 8 の順(元データでの登場順)
tib.lm.nested <- tibble(tib.nested, lm=tib.lm.gmap)
tib.lm.nested
str(tib.lm.nested[2, "lm"][[1]])

# A tibble: 3 x 2
  tib.nested$cyl           $data lm
           <dbl> <list<df[,10]>> <list>
1              6        [7 × 10] <lm>
2              4       [11 × 10] <lm>  ## 実はここの lm の中身は cyl==6 のとき。
3              8       [14 × 10] <lm>

つまり元データにおいて水準の登場順が完全な昇順になっていない場合、bind した列の組み合わせがずれてしまう。

mutate や summarise と group_map を組み合わせるとやばい

上記のような混乱を防ぐには、パイプライン中で mutate や summarise を用いて直接新規列に追加できれば楽である。しかし group_map とこれらの関数はかなり相性が悪い。


# まず summarise との組み合わせ。
tib.lm.gmap.summarise <- mtcars %>%
    dplyr::group_by(cyl) %>%
    dplyr::summarise(lm=list(dplyr::group_map(., ~lm(mpg~disp, data=.))))
tib.lm.gmap.summarise

# A tibble: 3 x 2
    cyl lm
  <dbl> <list>
1     4 <list [3]>
2     6 <list [3]>
3     8 <list [3]>

tib.lm.gmap.summarise[1, "lm"][[1]]

[[1]]
[[1]][[1]]

Call:
lm(formula = mpg ~ disp, data = .)

Coefficients:
(Intercept)         disp
    40.8720      -0.1351


[[1]][[2]]

Call:
lm(formula = mpg ~ disp, data = .)

Coefficients:
(Intercept)         disp
  19.081987     0.003605


[[1]][[3]]

Call:
lm(formula = mpg ~ disp, data = .)

Coefficients:
(Intercept)         disp
   22.03280     -0.01963

一見エラーも出ずに処理が完了するが、何かがおかしいゾ?なぜか summarise された各行に対して、全水準の評価結果が入ってしまっている。また group_map の実行結果を list() でまとめて、強引に mutate に渡す書き方も可能だが、これをやると全ての行に対して全水準の評価が実行されて返されるため、奇妙な結果になる。


# 次に mutate に入れてみる
tib.lm.gmap.mutate <- mtcars %>%
    dplyr::group_by(cyl) %>%
    dplyr::mutate(lm=list(dplyr::group_map(., ~lm(mpg~disp, data=.))))

> tib.lm.gmap.mutate
# A tibble: 32 x 12
# Groups:   cyl [3]
     mpg   cyl  disp    hp  drat    wt  qsec    vs    am  gear  carb lm
   <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <list>
 1  21       6  160    110  3.9   2.62  16.5     0     1     4     4 <list [3]>
 2  21       6  160    110  3.9   2.88  17.0     0     1     4     4 <list [3]>
 3  22.8     4  108     93  3.85  2.32  18.6     1     1     4     1 <list [3]>
 4  21.4     6  258    110  3.08  3.22  19.4     1     0     3     1 <list [3]>
 5  18.7     8  360    175  3.15  3.44  17.0     0     0     3     2 <list [3]>
 6  18.1     6  225    105  2.76  3.46  20.2     1     0     3     1 <list [3]>
 7  14.3     8  360    245  3.21  3.57  15.8     0     0     3     4 <list [3]>
 8  24.4     4  147.    62  3.69  3.19  20       1     0     4     2 <list [3]>
 9  22.8     4  141.    95  3.92  3.15  22.9     1     0     4     2 <list [3]>
10  19.2     6  168.   123  3.92  3.44  18.3     1     0     4     4 <list [3]>
# … with 22 more rows

> tib.lm.gmap.mutate[1, "lm"][[1]]
[[1]]
[[1]][[1]]

Call:
lm(formula = mpg ~ disp, data = .)

Coefficients:
(Intercept)         disp
    40.8720      -0.1351


[[1]][[2]]

Call:
lm(formula = mpg ~ disp, data = .)

Coefficients:
(Intercept)         disp
  19.081987     0.003605


[[1]][[3]]

Call:
lm(formula = mpg ~ disp, data = .)

Coefficients:
(Intercept)         disp
   22.03280     -0.01963

だったら先に nest すればいいじゃんと思うが、これはさらに奇妙な結果になる。


# nest してから mutate 内で group_map を使ってみる
tib.lm.gmap.mutate <- mtcars %>%
    dplyr::group_by(cyl) %>%
    tidyr::nest() %>%
    dplyr::mutate(lm=list(dplyr::group_map( ., ~lm(mpg~disp, data=data[[1]]))))

# group_map( ., ~lm(mpg~disp, data=data[[1]])) の出力が3回反復される
tib.lm.gmap.mutate
tib.lm.gmap.mutate[1, "lm"][[1]]
tib.lm.gmap.mutate[2, "lm"][[1]]

> tib.lm.gmap.mutate
# A tibble: 3 x 3
# Groups:   cyl [3]
    cyl            data lm
  <dbl> <list<df[,10]>> <list>
1     6        [7 × 10] <list [3]>
2     4       [11 × 10] <list [3]>
3     8       [14 × 10] <list [3]>

> tib.lm.gmap.mutate[1, "lm"][[1]]
[[1]]
[[1]][[1]]

Call:
lm(formula = mpg ~ disp, data = data[[1]])

Coefficients:
(Intercept)         disp
  19.081987     0.003605


[[1]][[2]]

Call:
lm(formula = mpg ~ disp, data = data[[1]])

Coefficients:
(Intercept)         disp
  19.081987     0.003605


[[1]][[3]]

Call:
lm(formula = mpg ~ disp, data = data[[1]])

Coefficients:
(Intercept)         disp
  19.081987     0.003605



> tib.lm.gmap.mutate[2, "lm"][[1]]
[[1]]
[[1]][[1]]

Call:
lm(formula = mpg ~ disp, data = data[[1]])

Coefficients:
(Intercept)         disp
    40.8720      -0.1351


[[1]][[2]]

Call:
lm(formula = mpg ~ disp, data = data[[1]])

Coefficients:
(Intercept)         disp
    40.8720      -0.1351


[[1]][[3]]

Call:
lm(formula = mpg ~ disp, data = data[[1]])

Coefficients:
(Intercept)         disp
    40.8720      -0.1351

今度は nest したので、 cyl の別水準のデータについては各イテレーションのスコープ外になっているはずだ。これで解決と思いきや、各水準の出力において、同じあてはめ結果を3回反復して格納するという、明後日の答えが返ってきた。つまり関数の挙動として、group_map() にデータを渡した時点でのネスト状態にかかわらず、グループの数に一致するまで結果を反復して返すような仕様になっているっぽい。まあ各水準について 2 番目以降のリスト要素を廃棄してから結果を返させればいいのだが、素直に別の方法を検討すべきであろう。

他に試したがダメだった方法として、「group_modify を使う→lm が返す結果が元データと異なる長さのリストなので、これを強引にtibbleにまとめても致し方ない」。「group_by ではなく split で分ける→group_map の適用対象外なので動かない」。というわけで、個人的には group_map() の挙動は不可思議な部分が多すぎるので、あまりオススメしない。

group_modify() のこと

なお、 group_map() のファミリー関数の一種 group_modify() は、返り値が必ずデータフレーム形式になる。ただこの関数もけっこう挙動が謎なんだ。以下は head 関数を適用しただけだが、なぜか繰り返し回数(head(x, n) の第2引数)に勝手に cyl が入る。ちなみに dplyr::group_modify(head, n=2) などとしてもダメ。


mtcars %>%
    dplyr::group_by(cyl) %>%
    dplyr::group_modify(head)

# A tibble: 18 x 11
# Groups:   cyl [3]
     cyl   mpg  disp    hp  drat    wt  qsec    vs    am  gear  carb
   <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
 1     4  22.8 108      93  3.85  2.32  18.6     1     1     4     1
 2     4  24.4 147.     62  3.69  3.19  20       1     0     4     2
 3     4  22.8 141.     95  3.92  3.15  22.9     1     0     4     2
 4     4  32.4  78.7    66  4.08  2.2   19.5     1     1     4     1
 5     6  21   160     110  3.9   2.62  16.5     0     1     4     4
 6     6  21   160     110  3.9   2.88  17.0     0     1     4     4
 7     6  21.4 258     110  3.08  3.22  19.4     1     0     3     1
 8     6  18.1 225     105  2.76  3.46  20.2     1     0     3     1
 9     6  19.2 168.    123  3.92  3.44  18.3     1     0     4     4
10     6  17.8 168.    123  3.92  3.44  18.9     1     0     4     4
11     8  18.7 360     175  3.15  3.44  17.0     0     0     3     2
12     8  14.3 360     245  3.21  3.57  15.8     0     0     3     4
13     8  16.4 276.    180  3.07  4.07  17.4     0     0     3     3
14     8  17.3 276.    180  3.07  3.73  17.6     0     0     3     3
15     8  15.2 276.    180  3.07  3.78  18       0     0     3     3
16     8  10.4 472     205  2.93  5.25  18.0     0     0     3     4
17     8  10.4 460     215  3     5.42  17.8     0     0     3     4
18     8  14.7 440     230  3.23  5.34  17.4     0     0     3     4

上記の処理を正しく動作させようと思ったら、たぶん最も素直な戦略は map の結果を mutate で受ける方法だろう。


# 以下のコードであれば正しく動作する。
mtcars %>%
    dplyr::group_by(cyl) %>%
    tidyr::nest() %>%
    dplyr::mutate(data=purrr::map(data, head, n=2)) %>%
    tidyr::unnest(cols=c(data))

# A tibble: 6 x 11
# Groups:   cyl [3]
    cyl   mpg  disp    hp  drat    wt  qsec    vs    am  gear  carb
  <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1     6  21    160    110  3.9   2.62  16.5     0     1     4     4
2     6  21    160    110  3.9   2.88  17.0     0     1     4     4
3     4  22.8  108     93  3.85  2.32  18.6     1     1     4     1
4     4  24.4  147.    62  3.69  3.19  20       1     0     4     2
5     8  18.7  360    175  3.15  3.44  17.0     0     0     3     2
6     8  14.3  360    245  3.21  3.57  15.8     0     0     3     4

# 実は mutate せずに map で止めると、未評価の式を見ることが出来る。
mtcars %>%
    dplyr::group_by(cyl) %>%
    tidyr::nest() %>%
    purrr::map(data, head, n=2)

$cyl
[1] ".x[[i]]" "head"    "2"

$data
[1] ".x[[i]]" "head"    "2"

 警告メッセージ:
1:  .f(.x[[i]], ...) で:  data set ‘.x[[i]]’ not found
2:  .f(.x[[i]], ...) で:  data set ‘head’ not found
3:  .f(.x[[i]], ...) で:  data set ‘2’ not found
4:  .f(.x[[i]], ...) で:  data set ‘.x[[i]]’ not found
5:  .f(.x[[i]], ...) で:  data set ‘head’ not found
6:  .f(.x[[i]], ...) で:  data set ‘2’ not found

複数の引数を取りながらグループごとに複雑な処理を行う map2, pmap 系関数

ここまで紹介してきた map 系関数は原則として、グループごとに異なる値を代入可能な引数が .x の1種類であり、 追加引数 ... の部分に与えられる値は、グループ間で共通であった。これを拡張して2つの引数 .x, .y を取れるようにしたのが map2 であり、さらに任意の数のグループ固有の引数(ただし全て名前が必要)を取れるようにしたのが pmap だ。つまり base R における mapply である。


複数の入力について同時に map する。

以下の関数は map() の変種であり、複数の引数について同時にイテレートできる。ただし parallel といっても、計算がマルチコア化されているわけではない。map2() と walk2() は2つの引数を同時に変化させられる。操作対象のデータを含むリストから、任意の数の引数を同時に取ってこれるのが pmap() と pwalk() である。

特殊だが重要なケースとしてデータフレームがある。データフレームに対して pmap を適用する場合、 処理内容である .f は各行について適用される。 map_dfr() と pmap_dfr() は row-binding して作られたデータフレームを返す一方、 map2_dfc() および pmap_dfc は column-binding して作られたデータフレームを返す。

map2(.x, .y, .f, ...)
map2_lgl(.x, .y, .f, ...)
map2_int(.x, .y, .f, ...)
map2_dbl(.x, .y, .f, ...)
map2_chr(.x, .y, .f, ...)
map2_raw(.x, .y, .f, ...)
map2_dfr(.x, .y, .f, ..., .id = NULL)
map2_dfc(.x, .y, .f, ...)

walk2(.x, .y, .f, ...)

pmap(.l, .f, ...)
pmap_lgl(.l, .f, ...)
pmap_int(.l, .f, ...)
pmap_dbl(.l, .f, ...)
pmap_chr(.l, .f, ...)
pmap_raw(.l, .f, ...)
pmap_dfr(.l, .f, ..., .id = NULL)
pmap_dfc(.l, .f, ...)
pwalk(.l, .f, ...)


省略不可な引数
.x, .y    等しい長さのベクトル。いずれかが長さ1ならばリサイクルされる。

.f    関数、式、ないしベクトル(atomic でなくてもよい)
関数であればそのまま使われる。
式であれば(例: ~.x+2)関数に変換される。
それらの処理を書く中で、引数自体に言及する方法は3つある。
1) 引数が1つしか無い関数であれば . で表す。
2) 2つの引数を持つ関数であれば .x と .y で表す。
3) 3つ以上の引数を持つ関数であれば、 ..1, ..2, ..3, などとして表す。

.f の中身を文字列ベクトル、数値ベクトル、ないしリストとして与えると、extractor function に変換される。文字列ベクトルは name によってインデックス化され、数値ベクトルは場所によってインデックス化される。場所によるインデックスと名前によるインデックスを階層依存で使い分けたい場合はリストを使う。あるコンポーネントが存在しない場合は、 .default に指定した値を入れて返す。

...    mapされる関数に指定したい、追加の引数。

.id    オプションの引数で、*_dfr の関数だけに適用される。デフォルトは NULL だが、文字列を指定した場合、その名前で出力に新たな変数(列)が作られ、入力におけるインデックスの値がそこに格納される。

.l    .f の関数に渡したい引数たちを、ベクトルを束ねたリストとして与える。しばしばデータフレームの形になる。.l のリスト要素数(データフレームならば列数)が、 .f を呼び出すときの引数の長さを決定する。リスト要素に名前が付いていれば、処理中でその名前で参照できる。

値
返る値は、関数の接尾辞に応じて atomic vector であったり list であったり、データフレームであったりする。ベクトルとリストの各要素については、 .x もしくは .l の最初の要素が名前付きであれば、その名前が引き継がれる。
入力が全て長さ 0 であれば出力も長さ 0 になる。入力のいずれかの要素が長さ 1 であれば、最長の要素に合わせて値がリサイクルされる。

ただしこの pmap 関数ファミリー、少々使い勝手が独特である。まずは、 map で適当な処理、たとえば head を行う。この map の結果を mutate で受けると、自動的にリストにまとめられて新規列として入る。


# data 列に入っている tibble の行数を2に切り詰めて、新しい列に格納する。
mtcars %>%
    dplyr::group_by(cyl) %>%
    tidyr::nest() %>%
    dplyr::mutate(data2=purrr::map(.x=data, .f=head, n=2))

# A tibble: 3 x 3
# Groups:   cyl [3]
    cyl            data data2
  <dbl> <list<df[,10]>> <list>
1     6        [7 x 10] <tibble [2 x 10]>
2     4       [11 x 10] <tibble [2 x 10]>
3     8       [14 x 10] <tibble [2 x 10]>

上記の処理を改造し、head する際の長さを「cyl の半分」に変えたい。そこで関数をいきなり map から pmap に置き換えると、mutateの結果を受ける列 "data2" の長さは1でなければいけない、と言われる。


mtcars %>%
    dplyr::group_by(cyl) %>%
    tidyr::nest() %>%
    dplyr::mutate(data2=purrr::pmap(.l=data, .f=head, n=cyl/2))

Error: Column `data2` must be length 1 (the group size), not 10

試しにリストで括ってから data2 に放り込んでみる。


mtcars %>%
    dplyr::group_by(cyl) %>%
    tidyr::nest() %>%
    dplyr::mutate(data2=list(purrr::pmap(.l=data, .f=head, n=cyl/2)))


# A tibble: 3 x 3
# Groups:   cyl [3]
    cyl            data data2
  <dbl> <list<df[,10]>> <list>
1     6        [7 x 10] <named list [10]>
2     4       [11 x 10] <named list [10]>
3     8       [14 x 10] <named list [10]>

エラーこそ出ないものの、このデータは何かおかしい。実は pmap が返すデータ形状がリストである。head 関数については、返り値は本来データフレーム(tibble)でなければまずいのだ。


# この <named list [10]> って何なのさ
ans <- mtcars %>%
    dplyr::group_by(cyl) %>%
    tidyr::nest() %>%
    dplyr::mutate(data2=list(purrr::pmap(.l=data, .f=head, n=cyl/2)))
ans$data2


> ans$data2
[[1]]
[[1]]$mpg
[1] 21.0 21.0 21.4

[[1]]$disp
[1] 160 160 258

[[1]]$hp
[1] 110 110 110

[[1]]$drat
[1] 3.90 3.90 3.08

[[1]]$wt
[1] 2.620 2.875 3.215

[[1]]$qsec
[1] 16.46 17.02 19.44

[[1]]$vs
[1] 0 0 1

[[1]]$am
[1] 1 1 0

[[1]]$gear
[1] 4 4 3

[[1]]$carb
[1] 4 4 1


[[2]]
[[2]]$mpg
[1] 22.8 24.4

[[2]]$disp
[1] 108.0 146.7

                (以下略)

つまり、 head の計算そのものは正しく行われているが、計算結果を自動で [(cyl/2) x 10] のデータフレームに直してくれなかったのが、そもそものボタンの掛け違えであった。修正方針として、pmap の出力をデータフレームに直してやる。

https://purrr.tidyverse.org/reference/map.html をよく読んでみると、*_df と接尾辞の付くファミリー関数ならば、明示的にデータフレームを返せる。なので本事例のまっとうな修正方針は、以下のように pmap_df で置き換えることである。


# pmap ファミリー関数を用いた正しい head の実施例。data.frame を返したいので pmap_df をチョイス。
ans <- mtcars %>%
    dplyr::group_by(cyl) %>%
    tidyr::nest() %>%
    dplyr::mutate(data2=list(purrr::pmap_df(.l=data, .f=head, n=cyl/2)))
ans$data2

> ans$data2
[[1]]
# A tibble: 3 x 10
    mpg  disp    hp  drat    wt  qsec    vs    am  gear  carb
  <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1  21     160   110  3.9   2.62  16.5     0     1     4     4
2  21     160   110  3.9   2.88  17.0     0     1     4     4
3  21.4   258   110  3.08  3.22  19.4     1     0     3     1

[[2]]
# A tibble: 2 x 10
    mpg  disp    hp  drat    wt  qsec    vs    am  gear  carb
  <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1  22.8  108     93  3.85  2.32  18.6     1     1     4     1
2  24.4  147.    62  3.69  3.19  20       1     0     4     2

[[3]]
# A tibble: 4 x 10
    mpg  disp    hp  drat    wt  qsec    vs    am  gear  carb
  <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1  18.7  360    175  3.15  3.44  17.0     0     0     3     2
2  14.3  360    245  3.21  3.57  15.8     0     0     3     4
3  16.4  276.   180  3.07  4.07  17.4     0     0     3     3
4  17.3  276.   180  3.07  3.73  17.6     0     0     3     3

なおどうしても pmap でやりたければ、無理やり data.frame() を噛ませてから list で括る方法も一応可能である。たぶん実用的なメリットは薄い。


# pmap が返すリストを強引に list(data.frame()) を噛ませて受ける例。
mtcars %>%
    dplyr::group_by(cyl) %>%
    tidyr::nest() %>%
    dplyr::mutate(data2=list(data.frame(purrr::pmap(.l=data, .f=head, n=cyl/2))))

# A tibble: 3 x 3
# Groups:   cyl [3]
    cyl            data data2
  <dbl> <list<df[,10]>> <list>
1     6        [7 x 10] <df[,10] [3 x 10]>
2     4       [11 x 10] <df[,10] [2 x 10]>
3     8       [14 x 10] <df[,10] [4 x 10]>

# なお data.frame しただけではダメで、さらに list で受ける必要がある。だったら素直に pmap_df 使うべき。
mtcars %>%
    dplyr::group_by(cyl) %>%
    tidyr::nest() %>%
    dplyr::mutate(data2=data.frame(purrr::pmap(.l=data, .f=head, n=cyl/2)))

Error: Column `data2` is of unsupported class data.frame

ここまで長々と書いてきたが、何が言いたいかというと、group_by → nest してから mutate の処理内容に map 系関数を使う際は、処理結果を「グループごとに 1×1 サイズのオブジェクトで返す」ようにパッキングする必要がある。これを行う最も簡単な方法は、 *map* した結果を data.frame() で括ったり、さらに list() したりすることであり、 後の整形処理を簡単にするためには *map* → *map*_df といった眷属の活用を検討すべきである。

落穂ひろい

tibble の列を取り出せるのに代入できない場合

tibble のある列を抜き出すとベクトルではなく n x 1 の tibble になってしまい、素直に何らかの処理関数に代入できないことがある。dplyr::pull() という関数を使うと、自動的にベクトルとして取り出せる続編 の「テーブルから単一の値を引き出す pull 関数」という節で詳しく述べる。


dplyr::pull(.data=mtcars, var="cyl")

> dplyr::pull(.data=mtcars, var="cyl")
 [1] 6 6 4 6 8 6 8 4 4 6 6 8 8 8 8 8 8 4 4 4 4 8 8 8 8 4 4 4 8 6 8 4

グループ化、階層化された tibble の実際の形状を調べてみよう

Tidyverse における grouped tibble や nested tibble の構造は、普段はユーザーから隠蔽されており意識しないものだが、str(list) を用いて覗くことが可能だ。キーの名前まで覚える必要はないが、一度見ておくと良い。


data(mtcars)
g.mtcars <- dplyr::group_by(mtcars, cyl)
n.mtcars <- nest(g.mtcars)
str(mtcars)
str(g.mtcars)
str(n.mtcars)

> str(mtcars)
'data.frame':   32 obs. of  11 variables:
 $ mpg : num  21 21 22.8 21.4 18.7 18.1 14.3 24.4 22.8 19.2 ...
 $ cyl : num  6 6 4 6 8 6 8 4 4 6 ...
 $ disp: num  160 160 108 258 360 ...
 $ hp  : num  110 110 93 110 175 105 245 62 95 123 ...
 $ drat: num  3.9 3.9 3.85 3.08 3.15 2.76 3.21 3.69 3.92 3.92 ...
 $ wt  : num  2.62 2.88 2.32 3.21 3.44 ...
 $ qsec: num  16.5 17 18.6 19.4 17 ...
 $ vs  : num  0 0 1 1 0 1 0 1 1 1 ...
 $ am  : num  1 1 1 0 0 0 0 0 0 0 ...
 $ gear: num  4 4 4 3 3 3 3 4 4 4 ...
 $ carb: num  4 4 1 1 2 1 4 2 2 4 ...

グループ化された tibble の実際の形状


> str(g.mtcars)
Classes ‘grouped_df’, ‘tbl_df’, ‘tbl’ and 'data.frame':   32 obs. of  11 variables:
 $ mpg : num  21 21 22.8 21.4 18.7 18.1 14.3 24.4 22.8 19.2 ...
 $ cyl : num  6 6 4 6 8 6 8 4 4 6 ...
 $ disp: num  160 160 108 258 360 ...
 $ hp  : num  110 110 93 110 175 105 245 62 95 123 ...
 $ drat: num  3.9 3.9 3.85 3.08 3.15 2.76 3.21 3.69 3.92 3.92 ...
 $ wt  : num  2.62 2.88 2.32 3.21 3.44 ...
 $ qsec: num  16.5 17 18.6 19.4 17 ...
 $ vs  : num  0 0 1 1 0 1 0 1 1 1 ...
 $ am  : num  1 1 1 0 0 0 0 0 0 0 ...
 $ gear: num  4 4 4 3 3 3 3 4 4 4 ...
 $ carb: num  4 4 1 1 2 1 4 2 2 4 ...
 - attr(*, "groups")=Classes ‘tbl_df’, ‘tbl’ and 'data.frame':      3 obs. of  2 variables:
  ..$ cyl  : num  4 6 8
  ..$ .rows:List of 3
  .. ..$ : int  3 8 9 18 19 20 21 26 27 28 ...
  .. ..$ : int  1 2 4 6 10 11 30
  .. ..$ : int  5 7 12 13 14 15 16 17 22 23 ...
  ..- attr(*, ".drop")= logi TRUE

グループ化&階層化された tibble の実際の形状


> str(n.mtcars)
Classes ‘grouped_df’, ‘tbl_df’, ‘tbl’ and 'data.frame':   3 obs. of  2 variables:
 $ cyl : num  6 4 8
 $ data: list [1:3]
  ..$ :Classes ‘tbl_df’, ‘tbl’ and 'data.frame':    7 obs. of  10 variables:
  .. ..$ mpg : num  21 21 21.4 18.1 19.2 17.8 19.7
  .. ..$ disp: num  160 160 258 225 168 ...
  .. ..$ hp  : num  110 110 110 105 123 123 175
  .. ..$ drat: num  3.9 3.9 3.08 2.76 3.92 3.92 3.62
  .. ..$ wt  : num  2.62 2.88 3.21 3.46 3.44 ...
  .. ..$ qsec: num  16.5 17 19.4 20.2 18.3 ...
  .. ..$ vs  : num  0 0 1 1 1 1 0
  .. ..$ am  : num  1 1 0 0 0 0 1
  .. ..$ gear: num  4 4 3 3 4 4 5
  .. ..$ carb: num  4 4 1 1 4 4 6
  ..$ :Classes ‘tbl_df’, ‘tbl’ and 'data.frame':    11 obs. of  10 variables:
  .. ..$ mpg : num  22.8 24.4 22.8 32.4 30.4 33.9 21.5 27.3 26 30.4 ...
  .. ..$ disp: num  108 146.7 140.8 78.7 75.7 ...
  .. ..$ hp  : num  93 62 95 66 52 65 97 66 91 113 ...
  .. ..$ drat: num  3.85 3.69 3.92 4.08 4.93 4.22 3.7 4.08 4.43 3.77 ...
  .. ..$ wt  : num  2.32 3.19 3.15 2.2 1.61 ...
  .. ..$ qsec: num  18.6 20 22.9 19.5 18.5 ...
  .. ..$ vs  : num  1 1 1 1 1 1 1 1 0 1 ...
  .. ..$ am  : num  1 0 0 1 1 1 0 1 1 1 ...
  .. ..$ gear: num  4 4 4 4 4 4 3 4 5 5 ...
  .. ..$ carb: num  1 2 2 1 2 1 1 1 2 2 ...
  ..$ :Classes ‘tbl_df’, ‘tbl’ and 'data.frame':    14 obs. of  10 variables:
  .. ..$ mpg : num  18.7 14.3 16.4 17.3 15.2 10.4 10.4 14.7 15.5 15.2 ...
  .. ..$ disp: num  360 360 276 276 276 ...
  .. ..$ hp  : num  175 245 180 180 180 205 215 230 150 150 ...
  .. ..$ drat: num  3.15 3.21 3.07 3.07 3.07 2.93 3 3.23 2.76 3.15 ...
  .. ..$ wt  : num  3.44 3.57 4.07 3.73 3.78 ...
  .. ..$ qsec: num  17 15.8 17.4 17.6 18 ...
  .. ..$ vs  : num  0 0 0 0 0 0 0 0 0 0 ...
  .. ..$ am  : num  0 0 0 0 0 0 0 0 0 0 ...
  .. ..$ gear: num  3 3 3 3 3 3 3 3 3 3 ...
  .. ..$ carb: num  2 4 3 3 3 4 4 4 2 2 ...
  ..@ ptype:Classes ‘tbl_df’, ‘tbl’ and 'data.frame':       0 obs. of  10 variables:
  .. ..$ mpg : num
  .. ..$ disp: num
  .. ..$ hp  : num
  .. ..$ drat: num
  .. ..$ wt  : num
  .. ..$ qsec: num
  .. ..$ vs  : num
  .. ..$ am  : num
  .. ..$ gear: num
  .. ..$ carb: num
 - attr(*, "groups")=Classes ‘tbl_df’, ‘tbl’ and 'data.frame':      3 obs. of  2 variables:
  ..$ cyl  : num  4 6 8
  ..$ .rows:List of 3
  .. ..$ : int 2
  .. ..$ : int 1
  .. ..$ : int 3
  ..- attr(*, ".drop")= logi FALSE