Heavy Watal

tidyr — シンプルなデータ変形ツール

data.frameを縦長・横広・入れ子に変形・整形するためのツール。 dplyrpurrr と一緒に使うとよい。 reshape2 を置き換えるべく再設計された改良版。

tidyverse に含まれているので、 install.packages("tidyverse") で一括インストール、 library(tidyverse) で一括ロード。

パイプ演算子 |> についてはdplyrを参照。

Pivoting: 縦長 ↔ 横広

https://tidyr.tidyverse.org/articles/pivot.html

tidyr::pivot_longer() で縦長にする

複数列のmatrix的にまたがっていた値を1列にまとめ、元の列名をその横に添えることで、 data.frameを横広(wide-format)から縦長(long-format)に変形する。 reshape2::melt(), tidyr::gather() の改良版。

tidyr::pivot_longer(data, cols, names_to = "name", ..., values_to = "value", ...)

cols
動かしたい値が含まれている列。 コロンで範囲指定、文字列、 selection helpersなども使える。 動かさない列を ! で反転指定するのほうが楽なことも多い。
names_to
元々列名だったものを入れる列の名前
values_to
値の移動先の列名
anscombe_long = anscombe |>
  tibble::rowid_to_column("id") |>
  print() |>
  pivot_longer(!id, names_to = "namae", values_to = "atai") |>
  print()
   id x1 x2 x3 x4    y1   y2    y3    y4
1   1 10 10 10  8  8.04 9.14  7.46  6.58
2   2  8  8  8  8  6.95 8.14  6.77  5.76
3   3 13 13 13  8  7.58 8.74 12.74  7.71
4   4  9  9  9  8  8.81 8.77  7.11  8.84
5   5 11 11 11  8  8.33 9.26  7.81  8.47
6   6 14 14 14  8  9.96 8.10  8.84  7.04
7   7  6  6  6  8  7.24 6.13  6.08  5.25
8   8  4  4  4 19  4.26 3.10  5.39 12.50
9   9 12 12 12  8 10.84 9.13  8.15  5.56
10 10  7  7  7  8  4.82 7.26  6.42  7.91
11 11  5  5  5  8  5.68 4.74  5.73  6.89
   id namae  atai
 1  1    x1 10.00
 2  1    x2 10.00
 3  1    x3 10.00
 4  1    x4  8.00
--               
85 11    y1  5.68
86 11    y2  4.74
87 11    y3  5.73
88 11    y4  6.89

tidyr::pivot_wider() で横広にする

1列にまとまっていた値を、別の変数に応じて複数の列に並べ直すことで、 data.frameを縦長(long-format)から横広(wide-format)に変形する。 reshape2::dcast(), tidyr::spread() の改良版。

tidyr::pivot_wider(data, ..., id_cols = NULL, names_from = name, values_from = value, values_fill = NULL, values_fn = NULL)

..., id_cols
ここで指定した列のユニークな組み合わせが変形後にそれぞれ1行になる。 !で反転指定、:で範囲指定、文字列、tidyselect関数なども使える。 デフォルトでは names_fromvalues_from で指定されなかった列すべて。
names_from
新しく列名になる列。“name” という列名なら省略可能。
values_from
動かしたい値が入っている列。“value” という列名なら省略可能。
values_fill
存在しない組み合わせのセルを埋める値。 列によって値を変えたい場合は名前付きリストで渡す。
values_fn
id_cols の組み合わせが一意に定まらず複数のvalueを1セルに詰め込む場合の処理関数。 デフォルトでは警告とともに list() が使われる。
anscombe_long |>
  pivot_wider(names_from = namae, values_from = atai) |>
  dplyr::select(!id)
   x1 x2 x3 x4    y1   y2    y3    y4
 1 10 10 10  8  8.04 9.14  7.46  6.58
 2  8  8  8  8  6.95 8.14  6.77  5.76
 3 13 13 13  8  7.58 8.74 12.74  7.71
 4  9  9  9  8  8.81 8.77  7.11  8.84
--                                   
 8  4  4  4 19  4.26 3.10  5.39 12.50
 9 12 12 12  8 10.84 9.13  8.15  5.56
10  7  7  7  8  4.82 7.26  6.42  7.91
11  5  5  5  8  5.68 4.74  5.73  6.89

カテゴリカル変数を指示変数(ダミー変数)に変換するのにも使える:

pg = PlantGrowth |> dplyr::slice(c(1, 2, 11, 12, 21, 22)) |> print()
  weight group
1   4.17  ctrl
2   5.58  ctrl
3   4.81  trt1
4   4.17  trt1
5   6.31  trt2
6   5.12  trt2
pg |> tibble::rowid_to_column("id") |>
  dplyr::mutate(name = group, value = 1L) |>
  tidyr::pivot_wider(values_fill = 0L) |>
  dplyr::select(!c(id, ctrl))
  weight group trt1 trt2
1   4.17  ctrl    0    0
2   5.58  ctrl    0    0
3   4.81  trt1    1    0
4   4.17  trt1    1    0
5   6.31  trt2    0    1
6   5.12  trt2    0    1

tidyr::pivot_* 関数のもっと高度なオプション

names_sepnames_pattern を指定して names_to, name_from に複数の値を渡すと tidyr::separate() / tidyr::unite() 的な操作も同時にやってしまえる:

anscombe |> tibble::rowid_to_column("id") |>
  pivot_longer(!id, names_to = c("axis", "group"), names_sep = 1L) |>
  print() |>
  pivot_wider(id_cols = id, names_from = c(axis, group), names_sep = "_")
   id axis group value
 1  1    x     1 10.00
 2  1    x     2 10.00
 3  1    x     3 10.00
 4  1    x     4  8.00
--                    
85 11    y     1  5.68
86 11    y     2  4.74
87 11    y     3  5.73
88 11    y     4  6.89
   id x_1 x_2 x_3 x_4   y_1  y_2   y_3   y_4
 1  1  10  10  10   8  8.04 9.14  7.46  6.58
 2  2   8   8   8   8  6.95 8.14  6.77  5.76
 3  3  13  13  13   8  7.58 8.74 12.74  7.71
 4  4   9   9   9   8  8.81 8.77  7.11  8.84
--                                          
 8  8   4   4   4  19  4.26 3.10  5.39 12.50
 9  9  12  12  12   8 10.84 9.13  8.15  5.56
10 10   7   7   7   8  4.82 7.26  6.42  7.91
11 11   5   5   5   8  5.68 4.74  5.73  6.89
VADeaths |>
  as.data.frame() |>
  print() |>
  rownames_to_column("age") |>
  pivot_longer(!age, names_to = c("region", "sex"), names_sep = " ", values_to = "death")
      Rural Male Rural Female Urban Male Urban Female
50-54       11.7          8.7       15.4          8.4
55-59       18.1         11.7       24.3         13.6
60-64       26.9         20.3       37.0         19.3
65-69       41.0         30.9       54.6         35.1
70-74       66.0         54.3       71.1         50.0
     age region    sex death
 1 50-54  Rural   Male  11.7
 2 50-54  Rural Female   8.7
 3 50-54  Urban   Male  15.4
 4 50-54  Urban Female   8.4
--                          
17 70-74  Rural   Male  66.0
18 70-74  Rural Female  54.3
19 70-74  Urban   Male  71.1
20 70-74  Urban Female  50.0

names_transform に関数を指定すると、 列名だったものに適用される。 例えば型変換に使える:

anscombe |>
  tibble::rowid_to_column("id") |>
  tidyr::pivot_longer(!id,
    names_to = c("axis", "group"),
    names_sep = 1L,
    names_transform = list(group = as.integer)) |>
  tidyr::pivot_wider(id_cols = c(id, group), names_from = axis) |>
  dplyr::select(!id) |>
  dplyr::arrange(group)
   group  x     y
 1     1 10  8.04
 2     1  8  6.95
 3     1 13  7.58
 4     1  9  8.81
--               
41     4 19 12.50
42     4  8  5.56
43     4  8  7.91
44     4  8  6.89

names_prefix を使えば、列名の頭に共通して付いてた文字を消せる:

anscombe |>
  dplyr::select(starts_with("x")) |>
  pivot_longer(everything(), names_prefix = "x")
   name value
 1    1    10
 2    2    10
 3    3    10
 4    4     8
--           
41    1     5
42    2     5
43    3     5
44    4     8

names_to".value" という特殊な値を渡すことで、 旧列名から新しい列名が作られ、複数列への縦長変形を同時にできる。 また names_to = c("name", NA) のように不要な列を捨てることもできる。

tidy_anscombe = anscombe |>
  pivot_longer(                       # 縦長に変形したい
    everything(),                     # すべての列について
    names_to = c(".value", "group"),  # x, yを列名に、1, 2, 3をgroup列に
    names_sep = 1L,                   # 切る位置
    names_transform = list(group = as.integer)   # 型変換
  ) |>
  dplyr::arrange(group) |>            # グループごとに並べる
  print()                             # ggplotしたい形!
   group  x     y
 1     1 10  8.04
 2     1  8  6.95
 3     1 13  7.58
 4     1  9  8.81
--               
41     4 19 12.50
42     4  8  5.56
43     4  8  7.91
44     4  8  6.89

See https://speakerdeck.com/yutannihilation/tidyr-pivot?slide=67 for details.

Nested data.frame — 入れ子構造

https://tidyr.tidyverse.org/articles/nest.html

tidyr::nest(data, ..., .by = NULL, .key = NULL, .names_sep = NULL)

data.frameをネストして(入れ子にして)、list of data.frames のカラムを作る。 内側のdata.frameに押し込むカラムを ... に指定するか、 外側に残すカラムを ! で反転指定する。

diamonds |> nest(NEW_COLUMN = !cut) |> dplyr::arrange(cut)
        cut           NEW_COLUMN
1      Fair  <tbl_df [1610 x 9]>
2      Good  <tbl_df [4906 x 9]>
3 Very Good <tbl_df [12082 x 9]>
4   Premium <tbl_df [13791 x 9]>
5     Ideal <tbl_df [21551 x 9]>
# equivalent to
diamonds |> nest(NEW_COLUMN = c(carat, color:z)) |> dplyr::arrange(cut)
diamonds |> nest(.by = cut, .key = "NEW_COLUMN") |> dplyr::arrange(cut)
diamonds |> dplyr::group_nest(cut, .key = "NEW_COLUMN")
diamonds |> dplyr::nest_by(cut, .key = "NEW_COLUMN") |> dplyr::ungroup()

.key を使う方法でそれを省略すると data という名前の列になる。

cf. Hadley Wickham: Managing many models with R (YouTube)

tidyr::unnest(data, cols, ...)

ネストされたdata.frameを展開してフラットにする。 list of data.framesだけでなく、list of vectorsとかでもよい。

ネストされた列が複数ある場合に曖昧なコードにならないよう cols を明示的に指定することが求められる。

diamonds |>
  nest(NEW_COLUMN = !cut) |>
  unnest(cols = NEW_COLUMN)

方向を明示する tidyr::unnest_longer(), tidyr::unnest_wider() もある。

その他の便利関数

tidyr::separate()

文字列カラムを任意のセパレータで複数カラムに分割。 tidyr::unite() の逆。 reshape2::colsplit() に相当。

tidyr::separate(data, col, into, sep = "[^[:alnum:]]", remove = TRUE, convert = FALSE, extra = "warn", fill = "warn", ...)

col
切り分けたい列の名前
into
切り分けたあとの新しい列名を文字列ベクタで
sep = "[^[:alnum:]]"
セパレータを正規表現で。デフォルトはあらゆる非アルファベット。
整数を渡すと位置で切れる。例えば A41L で切ると A4 に。
remove = TRUE
切り分ける前の列を取り除くかどうか
convert = FALSE
切り分け後の値の型変換を試みるか
extra = "warn"
列数が揃わないときにどうするか: warn, drop, merge
fill = "warn"
足りない場合にどっち側をNAで埋めるか: warn, right, left。 つまり、文字を左詰めにするにはrightが正解(紛らわしい)。
VADeaths |> as.data.frame() |>
  tibble::rownames_to_column("class") |>
  print() |>
  tidyr::separate(class, c("lbound", "ubound"), "-", convert = TRUE)
  class Rural Male Rural Female Urban Male Urban Female
1 50-54       11.7          8.7       15.4          8.4
2 55-59       18.1         11.7       24.3         13.6
3 60-64       26.9         20.3       37.0         19.3
4 65-69       41.0         30.9       54.6         35.1
5 70-74       66.0         54.3       71.1         50.0
  lbound ubound Rural Male Rural Female Urban Male Urban Female
1     50     54       11.7          8.7       15.4          8.4
2     55     59       18.1         11.7       24.3         13.6
3     60     64       26.9         20.3       37.0         19.3
4     65     69       41.0         30.9       54.6         35.1
5     70     74       66.0         54.3       71.1         50.0

行方向に分割する tidyr::separate_rows(data, ..., sep, convert) もある。

tidyr::extract(data, col, into, regex, ...) を使えば正規表現でもっと細かく指定できる。

名前の似てる tidyr::extract_numeric(x) は 文字列から数字部分をnumericとして抜き出す関数だったが今はdeprecatedなので、 新しいreadr::parse_number()を使うべし。

tidyr::unite(data, col, ..., sep = "_", remove = TRUE, na.rm = FALSE)

複数カラムを結合して1列にする。 tidyr::separate() の逆。

paste() とか stringr::str_c() でも似たようなことができるけど na.rm = TRUE の挙動が欲しいときに便利。

df = tibble(x = c("x", "x", NA), y = c("y", NA, "y"))
df |> tidyr::unite(z, c(x, y), sep = "_", remove = FALSE)
     z    x    y
1  x_y    x    y
2 x_NA    x <NA>
3 NA_y <NA>    y
df |> tidyr::unite(z, c(x, y), sep = "_", remove = FALSE, na.rm = TRUE)
    z    x    y
1 x_y    x    y
2   x    x <NA>
3   y <NA>    y
df |> dplyr::mutate(z = stringr::str_c(x, y, sep = "_"))
     x    y    z
1    x    y  x_y
2    x <NA> <NA>
3 <NA>    y <NA>
df |> dplyr::mutate(z = dplyr::coalesce(x, y))
     x    y z
1    x    y x
2    x <NA> x
3 <NA>    y y

tidyr::complete(data, ..., fill = list())

指定した列の全ての組み合わせが登場するように、 指定しなかった列に欠損値NA(あるいは任意の値)を補完した行を挿入する。

df |> complete(key1, key2, fill = list(val1 = 0, val2 = "-"))

tidyr::expand(data, ...)

指定した列の全ての組み合わせが登場するような新しいdata.frameを作る。 全ての列を指定すればcomplete()と同じ効果だが、 指定しなかった列が消えるという点では異なる。

crossing(...)はvectorを引数に取る亜種で、 tibble版expand.grid(...)のようなもの。

nesting(...)は存在するユニークな組み合わせのみ残す、 nest(data, ...) |> dplyr::select(!data)のショートカット。 この結果はexpand()complete()の引数としても使える。

数値vectorの補完にはfull_seq(x, period, tol = 1e-6)が便利。

tidyr::drop_na(data, ...)

complete()の逆。 指定した列にNAが含まれてる行を削除する。 何も指定しなければ標準の data[complete.cases(data),] と同じ。

tidyr::replace_na()

欠損値 NA を好きな値で置き換える。 これまでは mutate(x = ifelse(is.na(x), 0, x)) のようにしてたところを

df |> replace_na(list(x = 0, y = "unknown"))

逆に、特定の値をNAにしたい場合は dplyr::na_if()

tidyr::fill()

NA を、その列の直前の NA でない値で埋める。 えくせるでセルの結合とかやってしまって、 最初のセルにしか値が無いような場合に使うのかな?

関連書籍