tidyr 1.0.0 では新たに pack
, unpack
という関数が追加される.
これにより tidyverse で data frame column を扱えるようになる.
イメージとしてはこんな感じの階層性のあるデータを表現できる.
X | Y | |
---|---|---|
y1 | y2 | |
1 | 4 | 7 |
2 | 5 | 8 |
3 | 6 | 9 |
tidyr の pack
関数で簡単に作れるので, data frame column を持つデータフレームを packed data frame と呼ぶことにする,
この pack
,せっかく実装されるくせに,ドキュメントでは packed data frame を扱える関数がまだほとんどないから,使い道も特にないとされている.
不憫だ.
しかしまさに上述の表を作る時に役立つ.既に gt
パッケージの issues で提案済みだ (#314)
これまでは階層性のないデータフレームを kable
や gt
に与えてから弄くり回していたのに対し,これからは階層性のあるデータフレーム (= packed data frame) を関数に与えれば一撃になると素敵だ.
これには2つのメリットがあると思っている.
- 可視化したいデータと可視化した結果が同じ構造を持つので,可視化の段階でデータをこねくり回すような操作がなくなる
- より上流側の関数を覚えておけば,同じことを実現するために用意された下流の関数を覚える必要が減る
では実際に packded data frame の作り方と可視化方法を検討してみよう.
packed data frame を作る
base
base で作るならこんな感じで,データフレームを入れ子にする.親階層と子階層のデータフレームが同じ行数であることがポイントだ.
df <- data.frame(X = 1:3)
df$Y <- data.frame(y1 = 4:6, y2 = 7:9)
# 一見 flatten された data.frame に見えるが
print(df)
#> X Y.y1 Y.y2
#> 1 1 4 7
#> 2 2 5 8
#> 3 3 6 9
# 構造状はデータフレームが入れ子になっている.
str(df)
#> 'data.frame': 3 obs. of 2 variables:
#> $ X: int 1 2 3
#> $ Y:'data.frame': 3 obs. of 2 variables:
#> ..$ y1: int 4 5 6
#> ..$ y2: int 7 8 9
ちなみに子階層に親階層と異なる行数のデータフレームを代入するとエラーになる.
df$A <- data.frame(a = 1)
#> Error in `$<-.data.frame`(`*tmp*`, A, value = structure(list(a = 1), class = "data.frame", row.names = c(NA, : replacement has 1 row, data has 3
tibble
tibble
でも packed data frame を作ることができる.
base
と異なり,tibble
関数一発で作れる.また,親階層と子階層の名前は $
で隔たれており, .
よりも階層性が明瞭だ.
tibble(X = 1:3, Y = data.frame(y1 = 4:6, y2 = 7:9))
#> # A tibble: 3 x 2
#> X Y$y1 $y2
#> <int> <int> <int>
#> 1 1 4 7
#> 2 2 5 8
#> 3 3 6 9
tidyr
何もないところから packed data frame を作るなら tibble
がいいだろうが,既存のデータフレームから指定した列ごとにグループに分けて packed data frame 化したい時は tidyr
の出番だ.
tidyr
なら unpack
により, packed data frame を通常のデータフレームに戻すこともできる.
例えば先程の例なら,以下のように, Y
列に pack
したい列を select
のセマンティクスで選べばいい.
library(tidyr)
# pack
df <- data.frame(X = 1:3, y1 = 4:6, y2 = 7:9) %>%
pack(Y = c(y1, y2))
df
#> X Y.y1 Y.y2
#> 1 1 4 7
#> 2 2 5 8
#> 3 3 6 9
元に戻す時は unpack
関数で, unpack
したい列を選ぶ.
# unpack
unpack(df, Y)
#> # A tibble: 3 x 3
#> X y1 y2
#> <int> <int> <int>
#> 1 1 4 7
#> 2 2 5 8
#> 3 3 6 9
select のセマンティクスが有効なので, starts_with
などによるマッチングができる.また,複数の data frame column を作ることもできる.みんな大好き iris
もこの通り.
iris[1:3, ] %>%
pack(Sepal = starts_with("Sepal"), Petal = starts_with("Petal")) %>%
as_tibble
#> # A tibble: 3 x 3
#> Species Sepal$Sepal.Length $Sepal.Width Petal$Petal.Length $Petal.Width
#> <fct> <dbl> <dbl> <dbl> <dbl>
#> 1 setosa 5.1 3.5 1.4 0.2
#> 2 setosa 4.9 3 1.4 0.2
#> 3 setosa 4.7 3.2 1.3 0.2
ただし,見栄えの問題で as_tibble
しておくのが無難だ.
gt で見せる用の表を作る
Display table (見せる用の表) は gt
パッケージが定義している用語で,R 内部で扱うための表 (data frame) に対して,人に見せるための表くらいの認識でいいと思う.
gt
は非常に優秀で,例えば grouped data frame を一発で良い感じにしてくれる.
packed data frame もこれくらいのノリで扱えると最高だ.
library(gt) # インストールするには source("https://install-github.me/rstudio/gt")
library(dplyr)
mini_iris <- iris %>%
group_by(Species) %>%
slice(1:2)
gt(mini_iris)
Sepal.Length | Sepal.Width | Petal.Length | Petal.Width |
---|---|---|---|
setosa | |||
5.1 | 3.5 | 1.4 | 0.2 |
4.9 | 3.0 | 1.4 | 0.2 |
versicolor | |||
7.0 | 3.2 | 4.7 | 1.4 |
6.4 | 3.2 | 4.5 | 1.5 |
virginica | |||
6.3 | 3.3 | 6.0 | 2.5 |
5.8 | 2.7 | 5.1 | 1.9 |
通常の data frame + gt で列の階層性を表現する
これには gt_tbl
オブジェクトを tab_spanner()
関数にパイプする.
グループの数に応じて tab_spanner
を呼ぶ必要があり,更に各グループに入る列を標準評価で (つまり文字列ベクトル) で指定しなければならないのが面倒だ.
mini_iris %>%
gt %>%
tab_spanner("Sepal", c("Sepal.Length", "Sepal.Width")) %>%
tab_spanner("Petal", c("Petal.Length", "Petal.Width"))
Sepal | Petal | ||
---|---|---|---|
Sepal.Length | Sepal.Width | Petal.Length | Petal.Width |
setosa | |||
5.1 | 3.5 | 1.4 | 0.2 |
4.9 | 3.0 | 1.4 | 0.2 |
versicolor | |||
7.0 | 3.2 | 4.7 | 1.4 |
6.4 | 3.2 | 4.5 | 1.5 |
virginica | |||
6.3 | 3.3 | 6.0 | 2.5 |
5.8 | 2.7 | 5.1 | 1.9 |
packed data frame + gt で列の階層性を表現する
packed data frame のどの列が data frame column か調べておき,data frame column の名前と,各 data frame column を構成する列の名前を元に tab_spanner
を for
で繰り返し呼べばいい.
なんかごちゃごちゃした説明文になってしまったが,コードは非常にシンプルだ.
gt_packable <- function(x) {
df_col <- names(x)[vapply(x, is.data.frame, TRUE)]
.gt <- gt(unpack(x, !!df_col))
for (i in df_col) {
.gt <- tab_spanner(.gt, label = i, names(x[[i]]))
}
.gt
}
これに列単位のグルーピングを pack
で,行単位のグルーピングを group_by
で表現してやれば,以下の表が簡単にできる.
mini_iris %>%
pack(Sepal = starts_with("Sepal"), Petal = starts_with("Petal")) %>%
group_by(Species) %>%
gt_packable
Sepal | Petal | ||
---|---|---|---|
Sepal.Length | Sepal.Width | Petal.Length | Petal.Width |
setosa | |||
5.1 | 3.5 | 1.4 | 0.2 |
4.9 | 3.0 | 1.4 | 0.2 |
versicolor | |||
7.0 | 3.2 | 4.7 | 1.4 |
6.4 | 3.2 | 4.5 | 1.5 |
virginica | |||
6.3 | 3.3 | 6.0 | 2.5 |
5.8 | 2.7 | 5.1 | 1.9 |