Heavy Watal

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

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

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

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

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

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

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

cols
動かしたい値が含まれている列。 マイナスで除外指定、コロンで範囲指定、文字列、tidyselect関数なども使える。
names_to
元々列名だったものを入れる列の名前
values_to
値の移動先の列名
iris %>% as_tibble()
#> # tbl_df [150 x 5]
#>     Sepal.Length Sepal.Width Petal.Length Petal.Width   Species
#>            <dbl>       <dbl>        <dbl>       <dbl>     <fct>
#>   1          5.1         3.5          1.4         0.2    setosa
#>   2          4.9         3.0          1.4         0.2    setosa
#>   3          4.7         3.2          1.3         0.2    setosa
#>   4          4.6         3.1          1.5         0.2    setosa
#>  --
#> 147          6.3         2.5          5.0         1.9 virginica
#> 148          6.5         3.0          5.2         2.0 virginica
#> 149          6.2         3.4          5.4         2.3 virginica
#> 150          5.9         3.0          5.1         1.8 virginica

iris_long = iris %>%
  tibble::rowid_to_column("id") %>%
  pivot_longer(c(-id, -Species), names_to = "namae", values_to = "atai") %>%
  print()
#> # tbl_df [600 x 4]
#>        id   Species        namae  atai
#>     <int>     <fct>        <chr> <dbl>
#>   1     1    setosa Sepal.Length   5.1
#>   2     1    setosa  Sepal.Width   3.5
#>   3     1    setosa Petal.Length   1.4
#>   4     1    setosa  Petal.Width   0.2
#>  --
#> 597   150 virginica Sepal.Length   5.9
#> 598   150 virginica  Sepal.Width   3.0
#> 599   150 virginica Petal.Length   5.1
#> 600   150 virginica  Petal.Width   1.8

# iris %>% gather("namae", "atai", -Species)

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
新しく列名になる列
values_from
動かしたい値が入っている列
values_fill
存在しない組み合わせのセルを埋める値
values_fn
1つのセルに複数の値が重なってしまう場合の処理関数。例えば平均を取るとか。
iris_long %>%
  pivot_wider(names_from = namae, values_from = atai) %>%
  dplyr::select(-id)
#> # tbl_df [150 x 5]
#>       Species Sepal.Length Sepal.Width Petal.Length Petal.Width
#>         <fct>        <dbl>       <dbl>        <dbl>       <dbl>
#>   1    setosa          5.1         3.5          1.4         0.2
#>   2    setosa          4.9         3.0          1.4         0.2
#>   3    setosa          4.7         3.2          1.3         0.2
#>   4    setosa          4.6         3.1          1.5         0.2
#>  --
#> 147 virginica          6.3         2.5          5.0         1.9
#> 148 virginica          6.5         3.0          5.2         2.0
#> 149 virginica          6.2         3.4          5.4         2.3
#> 150 virginica          5.9         3.0          5.1         1.8

# iris_long %>% spread(namae, atai) %>% dplyr::select(-id)

iris_long %>%
  pivot_wider(Species, names_from = namae, values_from = atai,
              values_fn = list(atai = mean))
#> # tbl_df [3 x 5]
#>      Species Sepal.Length Sepal.Width Petal.Length Petal.Width
#>        <fct>        <dbl>       <dbl>        <dbl>       <dbl>
#> 1     setosa        5.006       3.428        1.462       0.246
#> 2 versicolor        5.936       2.770        4.260       1.326
#> 3  virginica        6.588       2.974        5.552       2.026

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

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

iris_long = iris %>%
  tibble::rowid_to_column("id") %>%
  pivot_longer(c(-id, -Species), names_to = c("part", "axis"), names_sep = "\\.") %>%
  print()
#> # tbl_df [600 x 5]
#>        id   Species  part   axis value
#>     <int>     <fct> <chr>  <chr> <dbl>
#>   1     1    setosa Sepal Length   5.1
#>   2     1    setosa Sepal  Width   3.5
#>   3     1    setosa Petal Length   1.4
#>   4     1    setosa Petal  Width   0.2
#>  --
#> 597   150 virginica Sepal Length   5.9
#> 598   150 virginica Sepal  Width   3.0
#> 599   150 virginica Petal Length   5.1
#> 600   150 virginica Petal  Width   1.8

iris_long %>%
  pivot_wider(c(id, Species), names_from = c(part, axis), names_sep = ".") %>%
  dplyr::select(-id)
#> # tbl_df [150 x 5]
#>       Species Sepal.Length Sepal.Width Petal.Length Petal.Width
#>         <fct>        <dbl>       <dbl>        <dbl>       <dbl>
#>   1    setosa          5.1         3.5          1.4         0.2
#>   2    setosa          4.9         3.0          1.4         0.2
#>   3    setosa          4.7         3.2          1.3         0.2
#>   4    setosa          4.6         3.1          1.5         0.2
#>  --
#> 147 virginica          6.3         2.5          5.0         1.9
#> 148 virginica          6.5         3.0          5.2         2.0
#> 149 virginica          6.2         3.4          5.4         2.3
#> 150 virginica          5.9         3.0          5.1         1.8
VADeaths
#>       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

VADeaths %>%
  as.data.frame() %>%
  rownames_to_column("age") %>%
  pivot_longer(-age, names_to = c("region", "sex"), names_sep = " ", values_to = "death")
#> # tbl_df [20 x 4]
#>      age region    sex death
#>    <chr>  <chr>  <chr> <dbl>
#>  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_ptypes に指定すると、列名だった変数の移動後の型変換を試みる:

anscombe
#>    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
#> 5  11 11 11  8  8.33 9.26  7.81  8.47
#> 6  14 14 14  8  9.96 8.10  8.84  7.04
#> 7   6  6  6  8  7.24 6.13  6.08  5.25
#> 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

anscombe %>%
  tibble::rowid_to_column("id") %>%
  tidyr::pivot_longer(-id,
    names_to = c("axis", "group"),
    names_sep = 1L,
    names_ptypes = list(group = integer())) %>%
  tidyr::pivot_wider(c(id, group), names_from = axis) %>%
  dplyr::select(-id) %>%
  dplyr::arrange(group)
#> # tbl_df [44 x 3]
#>    group     x     y
#>    <int> <dbl> <dbl>
#>  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")) %>%
  tidyr::pivot_longer(everything(), names_prefix = "x")
#> # tbl_df [44 x 2]
#>     name value
#>    <chr> <dbl>
#>  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" という特殊な値を渡すことで、 旧列名から新しい列名が作られ、複数列への縦長変形を同時にできる。

tidy_anscombe = anscombe %>%
  tidyr::pivot_longer(                # 縦長に変形したい
    everything(),                     # すべての列について
    names_to = c(".value", "group"),  # x, yを列名に、1, 2, 3をgroup列に
    names_sep = 1L,                   # 切る位置
    names_ptypes = list(group = integer())) %>%   # 型変換
  dplyr::arrange(group) %>%           # グループごとに並べる
  print()                             # ggplotしたい形!
#> # tbl_df [44 x 3]
#>    group     x     y
#>    <int> <dbl> <dbl>
#>  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 — 入れ子構造

tidyr::nest(data, ..., .key = data)

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

iris %>% nest(NEW_COLUMN = -Species)
#> # tbl_df [3 x 2]
#>      Species        NEW_COLUMN
#>        <fct>   <vctrs_list_of>
#> 1     setosa <tbl_df [50 x 4]>
#> 2 versicolor <tbl_df [50 x 4]>
#> 3  virginica <tbl_df [50 x 4]>

# equivalent to
iris %>% dplyr::group_nest(Species, .key = "NEW_COLUMN")
iris %>% nest(NEW_COLUMN = matches("Length$|Width$"))

なんでもかんでもフラットなdata.frameにして dplyrを駆使する時代は終わり、 ネストしておいてpurrrを適用するのが tidyverse時代のクールなやり方らしい。

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

tidyr::unnest(data, ..., .drop = NA, id = NULL, .sep = NULL, .preserve = NULL)

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

ネストされた列が複数ある場合は .preserve オプションを使って1列ずつ開いていける。

その他の便利関数

tidyr::separate()

文字列カラムを任意のセパレータで複数カラムに分割。 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が正解(紛らわしい)。
va_deaths = VADeaths %>% as.data.frame() %>% tibble::rownames_to_column("class") %>% print()
#>   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

va_deaths %>%
  tidyr::separate(class, c("lbound", "ubound"), "-", convert = TRUE)
#>   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::unite(data, col, ..., sep = "_", remove = TRUE)

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

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

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

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 でない値で埋める。 えくせるでセルの結合とかやってしまって、 最初のセルにしか値が無いような場合に使うのかな?

関連書籍