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

既存の tidyverse に関する解説文では「正しく動作するコード例」は載せてあっても、「意図通りに動かないコード例」と「なぜ意図通りに動かないかの説明」が圧倒的に不足していた。

今回の記事では特に、初心者目線で間違ったコードをどのように書き、どのように直すかという途中過程を審らかにし、モダンな tidyverse 流の一括処理フローに親しんでもらうことを主眼としている。

ホーム | 統計 Top | Tidyverseによるデータフレーム加工(06)グループごとに一括処理する:その 3 nest された tibble からのデータ取り出しと階層解除

今回のシリーズでは dplyr や purrr といった Tidyverse のパッケージを利用して、巨大なデータフレームに含まれているある変数列の水準別に、データを加工したり集計したり統計モデルに突っ込んだりといった、複雑な一括処理を行う手順を解説している。前々編 「Tidyverseによるデータフレーム加工(04)グループごとに一括処理する:その 1 group_by + mutate, summarise の利用」 および前編 「Tidyverseによるデータフレーム加工(05)グループごとに一括処理する:その 2 nest および map 系関数」 では、グループ化/階層化した tibble のグループごとに処理を適用し、計算結果を nest された tibble に格納した。

本記事では、字数の関係で前回紹介できなかった、nested 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
(以下省略)

nest で階層化された tibble の要素へのアクセス

前回、グループごとに lm で線形モデルに当てはめるスタンダードな方法として、nest してから 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>

nested tibble へ格納した list 内の要素へのアクセス

まず為すべきことは、上記 tib.lm.map の lm 列に入っているデータの形状の調査である。以下、幾つかのコマンドを淡々と貼っていくので、結果を見比べてほしい。


# よくある lm 変数列へのアクセス
tib.lm.map$lm

> tib.lm.map$lm
[[1]]

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

Coefficients:
(Intercept)         disp
 19.081987     0.003605


[[2]]

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

Coefficients:
(Intercept)         disp
   40.8720      -0.1351


[[3]]

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

Coefficients:
(Intercept)         disp
  22.03280     -0.01963

# 前回紹介したように、 pull を使って変数列を引っこ抜くこともできる。
dplyr::pull(tib.lm.map, var="lm")
# pluck 関数もある。この例では pull と同じ結果になる。
purrr::pluck(tib.lm.map, "lm")

# いずれも実行結果は上記の tib.lm.map$lm と等しい。

ベクトルないし環境から単一の要素を取ってくる pluck/chuck 関数

要素を取ってくる [[]] の強力版として、 purrr で pluck() および chuck() という関数が整備された。


https://purrr.tidyverse.org/reference/pluck.html

pluck()(摘み取る)および chuck()(摘み出す)はデータ構造の深いところへ、インデックスを用いて柔軟にアクセスするための、[[]] の一般化された機能を提供する。関数としての機能の違いは、前者は指定された要素が存在しない場合 NULL (ないし .default に与えた値)を返すが、後者はエラーを返すことである。

pluck(.x, ..., .default = NULL)
chuck(.x, ...)
# 位置を指定して値を代入することもできるぞ
pluck(.x, ...) <- value

省略不可な引数
.x, x    ベクトルもしくは環境

...    .x で指定したオブジェクト内のインデックスとして、取り出したいデータの位置を指定する、アクセサー accessors からなるリスト。整数で(ベクトル内の)データの位置を指定することも出来るし、文字列 "" で名前を指定してもいいし、 accessor function を書いてもいい(ただし結果的に位置や名前でインデックスを返すものに限る)。もしインデックス対象のオブジェクトが S4 クラスだった場合、名前でアクセスすると対応する slot が返る。
これらの ... のドットは tidy dots feature をサポートしている。特に accessor がリスト内に格納されている場合、 !!! を用いて splice することが可能。

(須藤注: tidy-dots も初出の概念だが、以下に解説がある。)
https://rlang.r-lib.org/reference/tidy-dots.html

.default    上で指定したターゲットが空ないし存在しなかった場合に返す値(上述どおり pluck の場合の対応。chuck だとエラーになる)。

value    pluck の構文を用いて、 .x の指定箇所に値を代入することも出来る。

Details
pluck を用いることで、[[]] や $ で指定するよりも可読性の高いコードになる。ただし、 $ と違って部分一致はできない。例えば mtcars$di と書くと、他に di で始まる変数列がないので mtcars$disp のことだと判断してくれるが、pluck(mtcars, "di") は NULL になる。

実際、取り出す対象が存在しない場合の挙動を除き、pluck と chuck はほとんど同じ関数だとみなせる。


> mtcars$di
 [1] 160.0 160.0 108.0 258.0 360.0 225.0 360.0 146.7 140.8 167.6 167.6 275.8 275.8 275.8 472.0 460.0 440.0  78.7  75.7  71.1 120.1
[22] 318.0 304.0 350.0 400.0  79.0 120.3  95.1 351.0 145.0 301.0 121.0

> pluck(mtcars, "di")
NULL

> pluck(mtcars, "disp")
 [1] 160.0 160.0 108.0 258.0 360.0 225.0 360.0 146.7 140.8 167.6 167.6 275.8 275.8 275.8 472.0 460.0 440.0  78.7  75.7  71.1 120.1
[22] 318.0 304.0 350.0 400.0  79.0 120.3  95.1 351.0 145.0 301.0 121.0

> chuck(mtcars, "di")
 エラー: Can’t find name `di` in vector

> chuck(mtcars, "disp")
 [1] 160.0 160.0 108.0 258.0 360.0 225.0 360.0 146.7 140.8 167.6 167.6 275.8 275.8 275.8 472.0 460.0 440.0  78.7  75.7  71.1 120.1
[22] 318.0 304.0 350.0 400.0  79.0 120.3  95.1 351.0 145.0 301.0 121.0

テーブルから単一の値を引き出す pull 関数

要素を取ってくる [[]] の強力版として、 dplyr には pull() という関数もある。


https://dplyr.tidyverse.org/reference/pull.html

pull() 関数はローカル環境上のデータフレームから、 [[]] に似た形で要素を引き出す。リモートにある複数の data table が対象の場合は、インデックス前にそれらのテーブルを統合する。

pull(.data, var = -1)

引数
.data    データが入ったテーブル構造

var    以下のいずれかの形で指定される変数
1. 変数名の文字列
2. インデックスの左(冒頭)からの位置を示す正の整数
3. インデックスの右(末尾)からの位置を示す負の整数

デフォルトは -1 で、最後の列を取得する(ユーザーが作った最新のデータは右端列にあることが多いので)。
引数 var は expression として関数に渡され、quasiquotation をサポートしているので、列名や位置には "" を付けても付けなくてもいい。

多くのケースでは [[]] や $ でもアクセス可能だが、時々 $ 型のアクセスが許されない(たとえばこんなエラーが出る:object of type 'closure' is not subsettable)操作があるので、pluck やpull の書き方を覚えておくと便利である。

部分的な tibble へのアクセスと要素の取り出しは異なる

上のコマンドは、いずれも tib.lm.map の lm 変数列の「中身」をリストとして取り出すものであった。一方 tibble に対して [行アドレス, 列アドレス] の形でアクセスすると、中身ではなく「該当アドレスが切り取られた tibble」 が返るので、それに続く処理の種類によっては、データ形式が合わないと叱られることがある。この辺 base R の data frame では融通が聞くのだが、 tibble は厳格である。


# tib.lm.map の当該アドレスを 1×1 の tibble として取り出している。
tib.lm.map[1, "lm"]
tib.lm.map[1, 3] # こちらも同じ結果を返す

> tib.lm.map[1, "lm"]
# A tibble: 1 x 1
 lm
 <list>
1 <lm>

mtcars[1, 1] # 単なる data frame のアドレスにアクセスすると、ベクトルになる

[1] 21

なので、そもそもアドレスを使って当該要素の中身にアクセスするには、tibble では一手間必要である。さらに厄介なことに、当てはめ結果である lm クラスのオブジェクトは、"lm"変数列の実体であるリストの要素において、もう一段階入れ子になっている。


# 当該アドレスの「リスト要素」を取り出すには以下。
tib.lm.map[1, "lm"][[1]][[1]]
str(tib.lm.map[1, "lm"][[1]][[1]])

> tib.lm.map[1, "lm"][[1]][[1]]

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

Coefficients:
(Intercept)         disp
  19.081987     0.003605

# いやらしいことにリストが二重化されている
class(tib.lm.map[1, "lm"][[1]])
class(tib.lm.map[1, "lm"][[1]][[1]])

> class(tib.lm.map[1, "lm"][[1]])
[1] "list"
> class(tib.lm.map[1, "lm"][[1]][[1]])
[1] "lm"

nested tibble に格納された lm オブジェクトを用いて predict する

ここまでの検討を踏まえて、当てはめ済みの lm を用いた予測を一括処理として実施したい。まず R の predict.lm 関数の基本的な使い方 なのだが、 predict(object, newdata=NULL) が最低限必要な情報である。object 部分は lm クラスのオブジェクトを突っ込む。当てはめに用いたデータ自体に対する予測値を計算したければ、 newdata には何も指定する必要はない。新規データに予測式を適用したければ、予測に用いたデータと同じ名前の変数列をもつ data frame を newdata に指定する。


# とりあえず最初のグループについて、予測値を計算。
predict( tib.lm.map[1, "lm"][[1]][[1]] )

> predict(tib.lm.map[1, "lm"][[1]][[1]])
       1        2        3        4        5        6        7
19.65881 19.65881 20.01211 19.89314 19.68621 19.68621 19.60473

最低限 predict の文法が確認できたので、この操作を tib.lm.map の全てのグループについて適用し、結果を tib.lm.map の新しい変数列として格納したい。まずは適当に mutate で操作してみる。


# nested tibble に対し一括で予測値を計算。まずは適当に mutate 内へ predict を突っ込んでみる
tib.lm.map %>%
    dplyr::mutate(lm.pred=predict( tib.lm.map[, "lm"][[1]][[1]] ))

> tib.lm.map %>%
+     dplyr::mutate(lm.pred=predict( tib.lm.map[, "lm"][[1]][[1]] ))
 エラー: Column `lm.pred` must be length 1 (the group size), not 7

predict までの操作はできてるっぽいが、データの長さが 7 ではなく 1 でないと lm.pred 列に受け入れられへんで、と叱られる。これは既に述べたように、mutate や summarise といった dplyr の関数が、原則的に「グループごとに要素長1にまとまる」データしか受け入れないためである。つまり、以下のように predict の結果を list で括れば、一応の解決を見る。


# とりあえず predict() の出力をリストで括って長さを合わせたゾ
tib.lm.pred.nest <- tib.lm.map %>%
    dplyr::mutate(lm.pred=list(predict( tib.lm.map[, "lm"][[1]][[1]] )))
tib.lm.pred.nest

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

だがよく見てほしい。 "data" 列に入っているリストのデータ数は cyl == 6, 4, 8 に対して 7, 11, 14 だったはず。なのに "lm.pred" 列に格納されたデータは全て要素数 7 である。つまり、一括処理が正しく行われておらず、最初の要素に対する計算を 3 回繰り返しただけになっている。明らかに tib.lm.map[, "lm"][[1]][[1]] の指定方法が間違っていたのである。

グループに対して正しく一括処理する

修正方針は mutate の基本に立ち戻れば単純だ。 grouped tibble に対する mutate では、操作対象列 lm を裸で書けば、当該グループのデータ要素だけが見えている形になり、自然に一括処理される。これを下手に .$lm とすると、「. は操作対象オブジェクトの全体」なので、他のグループも含めて tib.lm.map の lm 列が全て見えてしまう。


# 正しく一括処理するための、変数列の引っ張り方は?
tib.lm.pred.nest <- tib.lm.map %>%
    dplyr::mutate(lm.pred=list(predict(  object=lm[[1]]  ))) # これでよい

tib.lm.pred.nest <- tib.lm.map %>%
    dplyr::mutate(lm.pred=list(predict(  object=pluck(lm, 1)  ))) # これでもよい
#    dplyr::mutate(lm.pred=list(predict(  object=pluck(.$lm, 1)  ))) # ダメ例:cyl==6 を反復

tib.lm.pred.nest

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

上記は newdata=NULL なので、実のところ lm に既に入っている予測値を呼び出しているに過ぎない。このデータの実体は "lm" の "fitted.values" という、一段奥まった場所にある。こいつに直接アクセスするコードを base R で書くのはちょっと厄介だが、tib.lm.map がグループ化された tibble であることを利用すると、結構簡単に書ける。そう、mutate ならね。


# lm クラスオブジェクトには、以下のような場所に予測値が埋め込まれている。
> pluck(tib.lm.map$lm[[1]], "fitted.values")
       1        2        3        4        5        6        7
19.65881 19.65881 20.01211 19.89314 19.68621 19.68621 19.60473

> pluck(tib.lm.map$lm[[2]], "fitted.values")
       1        2        3        4        5        6        7        8        9       10       11
26.27664 21.04665 21.84399 30.23629 30.64172 31.26337 24.64142 30.19575 24.61440 28.01997 24.51980

# tib.lm.map はグループ化されているので、mutate ならば深い階層へのアクセスも並列化できるぞ。
tib.lm.map %>%
    mutate(lm.pred=list(pluck(lm[[1]], "fitted.values")))

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

ここまで分かれば、newdata を指定して predict するコードも難しくない。


tib.lm.pred2.nest <- tib.lm.map %>%
    dplyr::mutate(lm.pred=list(predict(  object=lm[[1]], newdata=data[[1]]  )))
tib.lm.pred2.nest

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

もちろん「list に対し element-wise に関数適用する」処理なので、前稿の通り purrr::map や map2 を使って書くこともできる。が、モデルオブジェクトや説明変数と同じグループ:行に、対応して結果が配置される mutate と違い、map 系は予測値が裸で出てくるので、データ管理の一貫性という点では不便である。


# あえて map2 で実装
tib.lm.pred.nest <- purrr::map2(.x=tib.lm.map$lm, .y=tib.lm.map$data, .f=predict)
tib.lm.pred.nest

[[1]]
       1        2        3        4        5        6        7
19.65881 19.65881 20.01211 19.89314 19.68621 19.68621 19.60473

[[2]]
       1        2        3        4        5        6        7        8        9       10       11
26.27664 21.04665 21.84399 30.23629 30.64172 31.26337 24.64142 30.19575 24.61440 28.01997 24.51980

[[3]]
       1        2        3        4        5        6        7        8        9       10       11       12       13       14
14.96452 14.96452 16.61772 16.61772 16.61772 12.76551 13.00112 13.39380 15.78916 16.06403 15.16087 14.17916 15.14123 16.12294

nest で階層化された tibble の階層解除

ここからは、各種の操作後に nested tibble を階層解除する方法を検討する。ただし操作の結果生まれた雑多な長さのオブジェクトを、リストに無理やりまとめて新規列に代入してあり、一発で unnest() できないものとする。

すでに前回述べたように、階層解除は unnest(data, cols) 関数で行うのが基本。だが単純に unnest(tib.lm.map) で折り畳みを戻そうとすると、新たに作られた lm 列の各要素であるリストの行数が、各グループのデータ点数(行数すなわち 7, 11, 14)と一致しないため、エラーになる。


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

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

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

以下の例のようにスカラーであれば、値が使い回されるので unnest() 可能である。


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

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

unnest(nested, cols=c(data, meandisp))

もちろん要素数が一致すれば、問題なく unnest() できる。


# unnest 時に面倒なので lm は除いてある。
tib.lm.pred.nest <- tib.lm.map %>%
    dplyr::mutate(lm.pred=list(predict(  object=lm[[1]]  ))) %>%
    dplyr::select(-lm)
unnest(tib.lm.pred.nest, cols=c(data, lm.pred))

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

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

(旧バージョン)unnest(.preserve) で複雑な要素を温存して階層解除する

サイズが合わなくて取り出せない場合、サイズの合わない変数列にはあえて「手を付けない」設定で unnest() するという発想もある。これを実現していたのが unnest(data, .preserve=c(温存したい列名)) というオプション指定である。

なお現在の unnest は再設計されたものであり、cols=c(展開すべき列名) として明示的に指定した列以外は展開させない、保守的な変形方針に変化した。これに伴い .preserve オプション自体は tidyr version 1.0.0 で deprecated とされたが、機能が廃止されたわけではなくむしろ強化されたと言える。ちなみに、最初に本機能が実装された経緯については Hadley のこちらの会話が参考になる。


# 古い tidyr の書き方。 .preserve オプションで温存列を指定。
unnest(tib.lm.map, .preserve=lm)

> unnest(tib.lm.map, .preserve=lm)
# A tibble: 32 x 12
# Groups:   cyl [3]
     cyl   mpg  disp    hp  drat    wt  qsec    vs    am  gear  carb lm
   <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <list>
 1     6  21    160    110  3.9   2.62  16.5     0     1     4     4 <lm>
 2     6  21    160    110  3.9   2.88  17.0     0     1     4     4 <lm>
 3     6  21.4  258    110  3.08  3.22  19.4     1     0     3     1 <lm>
 4     6  18.1  225    105  2.76  3.46  20.2     1     0     3     1 <lm>
 5     6  19.2  168.   123  3.92  3.44  18.3     1     0     4     4 <lm>
 6     6  17.8  168.   123  3.92  3.44  18.9     1     0     4     4 <lm>
 7     6  19.7  145    175  3.62  2.77  15.5     0     1     5     6 <lm>
 8     4  22.8  108     93  3.85  2.32  18.6     1     1     4     1 <lm>
 9     4  24.4  147.    62  3.69  3.19  20       1     0     4     2 <lm>
10     4  22.8  141.    95  3.92  3.15  22.9     1     0     4     2 <lm>
# … with 22 more rows
 警告メッセージ:
1: The `.preserve` argument of `unnest()` is deprecated as of tidyr 1.0.0.
All list-columns are now preserved
This warning is displayed once per session.
Call `lifecycle::last_warnings()` to see where this warning was generated.
2: `cols` is now required.
Please use `cols = c(data)`

Tidyr version 1.0.0 の unnest() では cols=c(展開したい列名を裸で列挙) として、どの列を unnest するかを必ず明示する。逆にいうと、cols に指定しなかった列は .preserve したのと同じ効果になる。


# tidyr v1.0.0 以降の書き方
unnest(tib.lm.map, cols = c(data))

nested tibble に格納された lm オブジェクトから残差を取り出しつつ unnest する

いま、nest 前の本来のデータ行数と一致するベクトルデータが、tibble の列たるリスト内の、一要素として埋め込まれているとする。こいつを取り出しつつ unnest して、折りたたみを解除した元の形状の tibble に新規列として格納する方法を考える。たとえば lm で線形モデルを当てはめたオブジェクトには、object$residuals として観測データに対応する、モデル当てはめ時の残差が格納されている。試しにこいつを取り出してみよう。


# 各観測データについて、予測値との残差が格納されている
tib.lm.map[1, "lm"][[1]][[1]]$residuals

> tib.lm.map[1, "lm"][[1]][[1]]$residuals
         1          2          3          4          5          6          7
 1.3411936  1.3411936  1.3878920 -1.7931391 -0.4862053 -1.8862053  0.0952704

すでに述べたように、lm は他の列と全く要素長が異なるため、そのまま unnest するのは無理。


unnest(tib.lm.map)

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

レクタングリング:hoist および unnest_wider

そこで Hadley は、強力な代替ツールを用意した。tidyr 1.0.0 で導入された unnest_wider(data, 展開したい列名) および hoist(.data, 展開したい列名) である。なお英語ではこの手の操作を rectangling という


レクタングリングは、幾重にも折りたたまれたリスト構造を、行や列からなる整然データに均すプログラミング技巧である。このために tidyr 1.0.0 で4つの関数が新規導入された。
unnest_wider() は(それ自体が tibble の列である)リストの要素であるベクトルを tibble のトップレベルの列へと展開する。
unnest_longer() はリストの要素であるベクトルを tibble のトップレベルの行へと展開する。
unnest_auto() はある種のヒューリスティックな方法で、ユーザーが unnest_longer() と unnest_wider() のどちらの操作を望んでいるか自動判断する。
hoist() は unnest_wider() に似ているが、リストの中に埋め込まれている列を明示的に選んで取り出し、tibble のトップレベルの列に配置できる。purrr::pluck() と同じ構文が使える。

hoist(.data, .col, ..., .remove=TRUE, .simplify=TRUE, .ptype=list())

unnest_longer(  data, col, values_to=NULL, indices_to=NULL, indices_include=NULL,
                names_repair="check_unique", simplify=TRUE, ptype=list())

unnest_wider(   data, col, names_sep=NULL, simplify=TRUE,
                names_repair="check_unique", ptype=list())

unnest_auto(data, col)


省略不可な引数
.data, data    データフレーム。hoist() のみ . がつく。

.col, col    取得したい要素が含まれている、data 中の列

...    .col の中で、新規列として取って来るべき要素を、 col_name="pluck_specification" の形で指定する。文字列ベクトルとして名前を指定してもよいし、整数ベクトルで場所を指定してもよいし、それらをリストにまとめてもよい。詳細は purrr::pluck() の指定方法に従う。

省略可能な引数
.remove    デフォルトは TRUE で、取り出されたコンポーネントは元の .col からは消去され、データフレーム内で1つのデータが重複して存在しないことが保証される。

.simplify    デフォルトは TRUE で、出力がリストとなり、全要素が単一の値しか含まない場合、これを atomic vector に単純化してくれる。

.ptype    Optionally, a named list of prototypes declaring the desired output type of each component.

values_to    Name of column to store vector values. Defaults to col.

indices_to    A string giving the name of column which will contain the inner names or position (if not named) of the values. Defaults to col with _id suffix

indices_include     Add an index column? Defaults to TRUE when col has inner names.

names_repair    Used to check that output data frame has valid names. Must be one of the following options:

    "minimal": no name repair or checks, beyond basic existence,
    "unique": make sure names are unique and not empty,
    "check_unique": (the default), no name repair, but check they are unique,
    "universal": make the names unique and syntactic
    a function: apply custom name repair.
    tidyr_legacy: use the name repair from tidyr 0.8.
    a formula: a purrr-style anonymous function (see rlang::as_function())

See vctrs::vec_as_names() for more details on these terms and the strategies used to enforce them.

simplify    If TRUE, will attempt to simplify lists of length-1 vectors to an atomic vector

ptype    Optionally, supply a data frame prototype for the output cols, overriding the default that will be guessed from the combination of individual values.

names_sep    If NULL, the default, the names of new columns will come directly from the inner data frame.

If a string, the names of the new columns will be formed by pasting together the outer column name with the inner names, separated by names_sep.

須藤注:設計方針としてはむしろ、「unnest_wider:行方向へ展開させない nest」、「unnest_longer:列方向へ展開させない nest」という具合に「展開方向を制限」する意図らしい。Tidyr v1.0.0 が「安全・堅実」な設計になっていることが、ここからも読み取れる。

hoist による取り出し

さっそく hoist() で "lm" の "residuals" を取り出してみる。なお下の節と見比べれば分かるが、"tib.lm.map" の "lm" の "residuals" を決め打ちして取り出せるのは hoist() だけであり、unnest_wider と unnest_longer は "lm" の「全要素」を操作対象にしてしまうため、lm クラスのような複雑な解析結果オブジェクトの加工には力不足である。


# hoist で取り出し
# 第1引数は .data として操作対象のデータフレーム。なお ungroup しないとエラーになる
# 第2引数は .col として、.data 中の操作したい変数列
# 第3引数から 取り出した後の新規列の名前=".colの中の抽出対象となる要素の名称"

tib.lm.hoi <- tidyr::hoist(ungroup(tib.lm.map), .col=lm, resid="residuals")
tib.lm.hoi

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

デフォルトは remove=TRUE なので lm から residuals が消える。lm クラスも外れる。


names(tib.lm.map$lm[[1]])
names(tib.lm.hoi$lm[[1]])

> names(tib.lm.map$lm[[1]])
 [1] "coefficients"  "residuals"     "effects"       "rank"          "fitted.values" "assign"        "qr"            "df.residual"
 [9] "xlevels"       "call"          "terms"         "model"
> names(tib.lm.hoi$lm[[1]])
 [1] "coefficients"  "effects"       "rank"          "fitted.values" "assign"        "qr"            "df.residual"   "xlevels"
 [9] "call"          "terms"         "model"

なお、 hoist は ungroup してから行う必要がある。


# なお取り出し対象の要素名をスペルミスすると以下のように叱られる
tidyr::hoist(ungroup(tib.lm.map), .col=lm, resid="residual")

 エラー: `x` must be a vector, not a `lm` object
Run `rlang::last_error()` to see where the error occurred.

# ungroup() しない grouped_df のままだと以下のエラーが出る。
tidyr::hoist(tib.lm.map, .col=lm, resid="residuals")

 エラー: `.data` is a corrupt grouped_df, the `"groups"` attribute must be a data frame

もしも grouped tibble のまま操作したければ、先ほど述べたように mutate を使っても書ける。この場合は hoist(remove=FALSE) と同等である。


tib.lm.map %>%
    mutate(resid=list(pluck(lm[[1]], "residuals")))

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

ここまでくれば unnest の見通しが立つ。


tib.lm.hoi <- tidyr::hoist(ungroup(tib.lm.map), .col=lm, resid="residuals")
# lm を残す必要がない場合
tib.lm.unnest <- unnest(dplyr::select(tib.lm.hoi, -lm), c(data, resid))
tib.lm.unnest

# A tibble: 32 x 12
     cyl   mpg  disp    hp  drat    wt  qsec    vs    am  gear  carb   resid
   <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  1.34
 2     6  21    160    110  3.9   2.88  17.0     0     1     4     4  1.34
 3     6  21.4  258    110  3.08  3.22  19.4     1     0     3     1  1.39
 4     6  18.1  225    105  2.76  3.46  20.2     1     0     3     1 -1.79
 5     6  19.2  168.   123  3.92  3.44  18.3     1     0     4     4 -0.486
 6     6  17.8  168.   123  3.92  3.44  18.9     1     0     4     4 -1.89
 7     6  19.7  145    175  3.62  2.77  15.5     0     1     5     6  0.0953
 8     4  22.8  108     93  3.85  2.32  18.6     1     1     4     1 -3.48
 9     4  24.4  147.    62  3.69  3.19  20       1     0     4     2  3.35
10     4  22.8  141.    95  3.92  3.15  22.9     1     0     4     2  0.956
# … with 22 more rows

# なお、lm を残したければ以下のコードもあり(lm の各行は重複した内容だが)
tib.lm.unnest <- unnest(tib.lm.hoi, c(data, resid))
tib.lm.unnest

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

後は、unlist もしくは c してやると全区間のデータが連結されて取りだせる。ただし、unnest したときに cyl の水準別に行が並べられているため、最初の mtcars の行順とは一致しなくなっていることに注意。


unlist(tib.lm.unnest$resid)
c(tib.lm.unnest$resid)

> unlist(tib.lm.unnest$resid)
 [1]  1.3411936  1.3411936  1.3878920 -1.7931391 -0.4862053 -1.8862053  0.0952704 -3.4766393  3.3533489  0.9560122  2.1637055
[12] -0.2417200  2.6366277 -3.1414234 -2.8957520  1.3856050  2.3800312 -3.1197958  3.7354753 -0.6645247 -0.2177155  0.6822845
[23] -1.4177155 -2.3655061 -2.6011153  1.3062028 -0.2891567 -0.8640341 -1.8608657  5.0208391  0.6587684 -1.1229363

unnest_wider および unnest_longer による取り出し

次に unnest_wider で "lm" の "residuals" を取り出そうとするが、残念ながら上手くいかない。


# unnest_wider で取り出し
tidyr::unnest_wider(ungroup(tib.lm.map), col=lm)

> tidyr::unnest_wider(ungroup(tib.lm.map), col=lm)
 エラー: Input must be list of vectors

というのも unnest_wider が上手く動作するのは、col が「ベクトルを束ねたリスト」である場合に限られる。たとえば以下であれば動く。


mtcars.nest <- mtcars %>%
    select(mpg, cyl, disp) %>%
    nest(-cyl) %>%
    dplyr::mutate(data2=as.list(data))
mtcars.nest
mtcars.nest$data2

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

# 強引に unnest_wider で展開してみる
tidyr::unnest_wider(mtcars.nest, col=data2)

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

# 強引に unnest_longer で展開してみる
tidyr::unnest_longer(mtcars.nest, col=data2)

# A tibble: 32 x 3
     cyl           data data2$mpg $disp
   <dbl> <list<df[,2]>>     <dbl> <dbl>
 1     6        [7 × 2]      21    160
 2     6        [7 × 2]      21    160
 3     6        [7 × 2]      21.4  258
 4     6        [7 × 2]      18.1  225
 5     6        [7 × 2]      19.2  168.
 6     6        [7 × 2]      17.8  168.
 7     6        [7 × 2]      19.7  145
 8     4       [11 × 2]      22.8  108
 9     4       [11 × 2]      24.4  147.
10     4       [11 × 2]      22.8  141.
# … with 22 more rows

# unnest_auto で展開してみる。
tidyr::unnest_auto(mtcars.nest, col=data2)

Using `unnest_wider(data2)`; elements have 2 names in common
# A tibble: 3 x 4
    cyl           data mpg        disp
  <dbl> <list<df[,2]>> <list>     <list>
1     6        [7 × 2] <dbl [7]>  <dbl [7]>
2     4       [11 × 2] <dbl [11]> <dbl [11]>
3     8       [14 × 2] <dbl [14]> <dbl [14]>

というわけで、感想としては hoist がメチャクチャ便利なので使っていこう。

       / /  ./  ,ィ          ヽ ヽ_
        / /  ./  //   /!  |l!   .lY'::::::::::)
      ; i  くlハ //,ィ  / .|  リ! j  l }::::::::::l!
      |イl!  ' _`Vメ、 l  / __.! ./_l/__ ノ l::::::i='ヽ
      ゝゝ| ;´んィ:!`    =j/__ノノイ /¨T ヽヽ
      ||  l 弋_丿     'んィ:!.ヽ// ,'   !  } }
      ||  l 、、、     弋_丿 // .,ヘ  .!   j/  / ̄ \
      ||  l     '   、、、 // ./イ  |     |  ア  |
      || ::ゝ.    __     // ./. !   |      |  リ  .!
      ||  | l > ´‐-'   _イ//∥| l  |    <.  で  |
      |l!. l_L:;ノ:.ト!¨  T¨ェ:://.∥ll! l  |     .|  す  !
      l|-、 ヽ: : : :.l! ̄` |:.:.// /l!ll| .!   |     .!  ね .!
     /-、:::ヽ ヽ: : : l ̄ ̄l:.// /: :ヽ! .!   !    \__/
.    / | >ヽ ヽ:.:.:l    l;'///: :/\ .|   |
.     /  l . /ヽ:ヽ ';.:ヽ /:::////、   \  |
   人.. V    } :!:ヽV/'/l;;;_/  Y ..人 !
.  /  ヽl     l  ! [__] / .l     i/  ヽ|