Help us understand the problem. What is going on with this article?

tidyrの関数たちと向き合ってみる

More than 1 year has passed since last update.

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を参照してください。

雑感

gatherspreadは頻繁に利用するのですが,それ以外にも色々と使えそうな関数を発掘できました。ただ結構高頻度で仕様や関数が変わるので備えましょう。

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
Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away