tidyrの関数たちと向き合ってみる
いつもお世話になっているtidyrパッケージですが,いつもgather()
とspread()
あたりしか使っていませんでした。もっといろんな関数があるのは知っていたので調査してみました。
なお自分用のメモなので自分が既知のものや面倒なもの,_
の分は省略します。
tidyrの関数一覧
exportされてる分だけを対象にします。あと面倒なのでtidyverse
で関連するのをまとめて読み込みます:
library(tidyverse)
#> Loading tidyverse: ggplot2
#> Loading tidyverse: tibble
#> Loading tidyverse: tidyr
#> Loading tidyverse: readr
#> Loading tidyverse: purrr
#> Loading tidyverse: dplyr
#> Conflicts with tidy packages ----------------------------------------------
#> filter(): dplyr, stats
#> lag(): dplyr, stats
ls("package:tidyr")
#> [1] "%>%" "complete" "complete_"
#> [4] "crossing" "crossing_" "drop_na"
#> [7] "drop_na_" "expand" "expand_"
#> [10] "extract" "extract_" "extract_numeric"
#> [13] "fill" "fill_" "full_seq"
#> [16] "gather" "gather_" "nest"
#> [19] "nest_" "nesting" "nesting_"
#> [22] "population" "replace_na" "separate"
#> [25] "separate_" "separate_rows" "separate_rows_"
#> [28] "smiths" "spread" "spread_"
#> [31] "table1" "table2" "table3"
#> [34] "table4a" "table4b" "table5"
#> [37] "unite" "unite_" "unnest"
#> [40] "unnest_" "who"
このうち,以下のオブジェクトはデータセットです:
population
smiths
table1
table2
table3
table4a
table4b
table5
who
また,%>%
はパイプ演算子です。さらに名前の最後に_
が付いているものを除外すると,以下のような感じになります:
complete
crossing
drop_na
expand
extract
extract_numeric
fill
full_seq
gather
nest
nesting
replace_na
separate
separate_rows
spread
unite
unnest
このうち,gather
, spread
, unite
, separate
については,すでに以下でまとめたことがあるので省略します:
あと,extract_numeric
はDEPRECATEDで,readr::parse_number()
を使えとのことで除きます。
各関数のメモ
complete
ヘルプのDescriptionによると「implicitな欠損値をexplicitな欠損値として埋めるよ!」という関数です。ヘルプドキュメントの例を使いながら試します。以下のようなデータフレームがあったとします:
library(dplyr)
df <- data_frame(
group = c(1:2, 1),
item_id = c(1:2, 2),
item_name = c("a", "b", "b"),
value1 = 1:3,
value2 = 4:6
)
df
#> # A tibble: 3 × 5
#> group item_id item_name value1 value2
#> <dbl> <dbl> <chr> <int> <int>
#> 1 1 1 a 1 4
#> 2 2 2 b 2 5
#> 3 1 2 b 3 6
もし,本来ならgroupとitem_idの全ての組み合わせが存在したんだよって場合,このdf
ではgroup = 2, item_id = 1
の組み合わせがあったはずです。でも欠損のためここにはありません。これを埋め合わせるような仕事をこの関数はします。
組み合わせを出す列を,以下のように引数に指定すると以下のようになります:
df %>% complete(group, item_id)
#> # A tibble: 4 × 5
#> group item_id item_name value1 value2
#> <dbl> <dbl> <chr> <int> <int>
#> 1 1 1 a 1 4
#> 2 1 2 b 3 6
#> 3 2 1 <NA> NA NA
#> 4 2 2 b 2 5
このように,元のデータに存在しなかったgroup = 2, item_id = 1
の組み合わせを挿入してくれました。なるほどいい感じです。
しかしこの変数名でitem_idとitem_nameとあるように,実はこの2つはセットになっているのでitem_idに合わせてitem_nameも連動してほしいという場面もあるかと思います。その時は以下のようにします:
df %>% complete(group, nesting(item_id, item_name))
#> # A tibble: 4 × 5
#> group item_id item_name value1 value2
#> <dbl> <dbl> <chr> <int> <int>
#> 1 1 1 a 1 4
#> 2 1 2 b 3 6
#> 3 2 1 a NA NA
#> 4 2 2 b 2 5
このnesting
については後で説明します。また,欠損値を特定の値(0
とかmissing
とか)で埋めたい場合は,以下のようにfill = list()
と名前付きリストで指定します:
df %>% complete(group, nesting(item_id, item_name), fill = list(value1 = 0))
#> # A tibble: 4 × 5
#> group item_id item_name value1 value2
#> <dbl> <dbl> <chr> <dbl> <int>
#> 1 1 1 a 1 4
#> 2 1 2 b 3 6
#> 3 2 1 a 0 NA
#> 4 2 2 b 2 5
大抵はこれでOKかと。なお,このcomplete()
はexpand()
, left_join()
, そしてreplace_na
を組み合わせたwrapperとのことです。なるほど。
より詳細な説明は?tidyr::complete
で確認してください。
nest
, unnest
データフレームを入れ子にする/入れ子を解除する関数です。これらに関してはyutannihilationさんの以下の記事がわかりやすいので省略します:
expand
, crossing
, nesting
直積集合を返す関数です。これらに関してもyutannihilationさんの以下の記事がわかりやすいので省略します:
一応ちょいちょい試します。
mtcars
のvs(0,1)とcyl(4,6,8)で,ありうる全ての組み合わせをexpand()
は出力してきます:
expand(mtcars, vs, cyl)
#> # A tibble: 6 × 2
#> vs cyl
#> <dbl> <dbl>
#> 1 0 4
#> 2 0 6
#> 3 0 8
#> 4 1 4
#> 5 1 6
#> 6 1 8
ここで,vsとcylとで実際のデータに存在する組み合わせだけを取り出すにはnesting
を組み込みます:
expand(mtcars, nesting(vs, cyl))
#> # A tibble: 5 × 2
#> vs cyl
#> <dbl> <dbl>
#> 1 0 4
#> 2 0 6
#> 3 0 8
#> 4 1 4
#> 5 1 6
要するにnesting()
は引数に指定した変数で,実際のデータにある組み合わせを出力してきてくれます。また,crossing()
はexpand.grid()
とほぼ同様ですが,(1)文字列をfactorに変換しない,(2)付加的な属性を足さずにtbl_df
を返すなどが違うようです:
crossing(x = 1:2, y = letters[1:3]) %>% glimpse()
#> Observations: 6
#> Variables: 2
#> $ x <int> 1, 1, 1, 2, 2, 2
#> $ y <chr> "a", "b", "c", "a", "b", "c"
expand.grid(x = 1:2, y = letters[1:3]) %>% glimpse()
#> Observations: 6
#> Variables: 2
#> $ x <int> 1, 2, 1, 2, 1, 2
#> $ y <fctr> a, a, b, b, c, c
その他詳しいことは?expand
を参照してください。
drop_na
, replace_na
, fill
これらはNA処理をします。
drop_na()
drop_na()
はNA
がある行を削除します:
df <- data_frame(x = c(1, 2, NA), y = c("a", NA, "b"))
df %>% drop_na()
#> # A tibble: 1 × 2
#> x y
#> <dbl> <chr>
#> 1 1 a
また,引数に列を指定すると,指定した列でNA
がある行だけを削除します:
df %>% drop_na(x)
#> # A tibble: 2 × 2
#> x y
#> <dbl> <chr>
#> 1 1 a
#> 2 2 <NA>
この変数の指定方法はdplyr::select
での方法がまるっと使えます。
replace_na()
replace_na()
はNA
を別の値で置換します。ただし,名前付きリストで対象変数と置換値を指定する必要があります:
df %>% replace_na(list(x = 0))
#> # A tibble: 3 × 2
#> x y
#> <dbl> <chr>
#> 1 1 a
#> 2 2 <NA>
#> 3 0 b
もちろんlist内に複数詰め込んでもOKです:
df %>% replace_na(list(x = 0, y = "unknown"))
#> # A tibble: 3 × 2
#> x y
#> <dbl> <chr>
#> 1 1 a
#> 2 2 unknown
#> 3 0 b
fill()
欠損値を埋めるのですが,その前のエントリー値を利用して埋めます。以下のようなデータがあったとします:
df <- data.frame(Month = c(rep(1:3, 2), 1),
Year = c(2000, rep(NA, 2), 2001, rep(NA, 2), 2002)
)
df
#> Month Year
#> 1 1 2000
#> 2 2 NA
#> 3 3 NA
#> 4 1 2001
#> 5 2 NA
#> 6 3 NA
#> 7 1 2002
これにfillを適用すると以下のような感じになります:
df %>% fill(Year)
#> Month Year
#> 1 1 2000
#> 2 2 2000
#> 3 3 2000
#> 4 1 2001
#> 5 2 2001
#> 6 3 2001
#> 7 1 2002
また,この場合下へ降りていく感じですが,逆方向も可能です:
df %>% fill(Year, .direction = "up")
#> Month Year
#> 1 1 2000
#> 2 2 2001
#> 3 3 2001
#> 4 1 2001
#> 5 2 2002
#> 6 3 2002
#> 7 1 2002
full_seq()
欠損値補完の一種で,与えらたベクトルに対し,period
で指定した間隔について自動で補完するというものです。これは具体例をひたすら試したほうがわかりやいかと。
full_seq(c(1, 2, 4, 5, 10), 1)
#> [1] 1 2 3 4 5 6 7 8 9 10
full_seq(c(2, 4, 10), 2)
#> [1] 2 4 6 8 10
ここで与えるベクトルは,Date型やPOSIXct型でも対応しています。ただ,2017/04/08時点で少数やPOSIXctだとちょっと挙動があやしくなりそうですので注意してください。例えば以下は構文的にはいいのですがエラーが出ます:
# 評価させてません
full_seq(c(0, 0.3, 0.8), 0.1)
# エラーメッセージは以下のとおりです:
# エラー: `x` is not a regular sequence.
理由は省略しますが,ソースコードを確認して検証するとわかるかと思います。
extract
データフレームのある列と,複数の列へと抜き出してくる関数。抜き出すルールに正規表現が使えるってのが特徴かと。あとseparate()
だと区切りを指定するけど,これはあくまで正規表現にマッチしてるのをうまいこと持ってくる感じ,かな?
ヘルプドキュメントにある例を以下に貼り付けてます:
df <- data.frame(x = c(NA, "a-b", "a-d", "b-c", "d-e"))
df %>% extract(x, "A")
#> A
#> 1 <NA>
#> 2 a
#> 3 a
#> 4 b
#> 5 d
df %>% extract(x, c("A", "B"), "([[:alnum:]]+)-([[:alnum:]]+)")
#> A B
#> 1 <NA> <NA>
#> 2 a b
#> 3 a d
#> 4 b c
#> 5 d e
# If no match, NA:
df %>% extract(x, c("A", "B"), "([a-d]+)-([a-d]+)")
#> A B
#> 1 <NA> <NA>
#> 2 a b
#> 3 a d
#> 4 b c
#> 5 <NA> <NA>
詳しくは?extract
を参照してください。
雑感
gather
やspread
は頻繁に利用するのですが,それ以外にも色々と使えそうな関数を発掘できました。ただ結構高頻度で仕様や関数が変わるので備えましょう。
Enjoy!
sessionInfo
#> R version 3.3.3 (2017-03-06)
#> Platform: x86_64-pc-linux-gnu (64-bit)
#> Running under: Ubuntu 16.04.2 LTS
#>
#> locale:
#> [1] LC_CTYPE=ja_JP.UTF-8 LC_NUMERIC=C
#> [3] LC_TIME=ja_JP.UTF-8 LC_COLLATE=ja_JP.UTF-8
#> [5] LC_MONETARY=ja_JP.UTF-8 LC_MESSAGES=ja_JP.UTF-8
#> [7] LC_PAPER=ja_JP.UTF-8 LC_NAME=C
#> [9] LC_ADDRESS=C LC_TELEPHONE=C
#> [11] LC_MEASUREMENT=ja_JP.UTF-8 LC_IDENTIFICATION=C
#>
#> attached base packages:
#> [1] stats graphics grDevices utils datasets methods base
#>
#> other attached packages:
#> [1] dplyr_0.5.0.9001 purrr_0.2.2 readr_1.1.0 tidyr_0.6.1
#> [5] tibble_1.3.0 ggplot2_2.2.1 tidyverse_1.1.1
#>
#> loaded via a namespace (and not attached):
#> [1] Rcpp_0.12.10 plyr_1.8.4 forcats_0.2.0
#> [4] tools_3.3.3 digest_0.6.12 lubridate_1.6.0
#> [7] jsonlite_1.3 evaluate_0.10 nlme_3.1-131
#> [10] gtable_0.2.0 lattice_0.20-35 pkgconfig_2.0.1
#> [13] rlang_0.0.0.9006 psych_1.7.3.21 yaml_2.1.14
#> [16] parallel_3.3.3 haven_1.0.0 xml2_1.1.1
#> [19] stringr_1.2.0 httr_1.2.1 knitr_1.15.17
#> [22] hms_0.3 rprojroot_1.2 grid_3.3.3
#> [25] glue_0.0.0.9000 R6_2.2.0 readxl_0.1.1
#> [28] foreign_0.8-67 rmarkdown_1.4 modelr_0.1.0
#> [31] reshape2_1.4.2 magrittr_1.5 backports_1.0.5
#> [34] scales_0.4.1 htmltools_0.3.6 rvest_0.3.2
#> [37] assertthat_0.1 mnormt_1.5-5 colorspace_1.3-2
#> [40] stringi_1.1.3 lazyeval_0.2.0.9000 munsell_0.4.3
#> [43] broom_0.4.2