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

by
カテゴリ:
タグ:

tidyr 1.0.0 では新たに pack, unpack という関数が追加される.

これにより tidyverse で data frame column を扱えるようになる.

イメージとしてはこんな感じの階層性のあるデータを表現できる.

XY
y1y2
147
258
369

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.LengthSepal.WidthPetal.LengthPetal.Width
setosa
5.13.51.40.2
4.93.01.40.2
versicolor
7.03.24.71.4
6.43.24.51.5
virginica
6.33.36.02.5
5.82.75.11.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"))
SepalPetal
Sepal.LengthSepal.WidthPetal.LengthPetal.Width
setosa
5.13.51.40.2
4.93.01.40.2
versicolor
7.03.24.71.4
6.43.24.51.5
virginica
6.33.36.02.5
5.82.75.11.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
SepalPetal
Sepal.LengthSepal.WidthPetal.LengthPetal.Width
setosa
5.13.51.40.2
4.93.01.40.2
versicolor
7.03.24.71.4
6.43.24.51.5
virginica
6.33.36.02.5
5.82.75.11.9

Enjoy!