Tidyr 1.0.0 で追加される pack を使えば見せる用の表が簡単に作れるかも

カテゴリ: r

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)

これまでは階層性のないデータフレームを kablegt に与えてから弄くり回していたのに対し,これからは階層性のあるデータフレーム (= 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_spannerfor で繰り返し呼べばいい.

なんかごちゃごちゃした説明文になってしまったが,コードは非常にシンプルだ.

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

Enjoy!