雑訳vignette: Pivoting (tidyr 1.0.0)

by
カテゴリ:
タグ:

tiydr 1.0.0 で追加される pivot_longer()pivot_wider() の使い方を紹介する vignette を雑に訳した.まだ正式リリースしていないので,仕様もドキュメントも変わる可能性がある.これらの点に注意の上読んで欲しい.

実際,気付いたことをコメントした結果,変わってしまった仕様もある (#630).他にも仕様やドキュメントに関する Issues 4件 と typo に対する PR を2件挙げてしまったので,pivot_* はまだまだこれから感が強い.

また,翻訳にはこなれていない部分や,適切な訳が分からず英語が残された部分もある.是非みなさんのコメントで訳を育てて欲しい.質問・コメントは @Atsushi776 (Twitter) まで.

個人的には, @yutannihilation 氏によるスライド「idyr 1.0.0?の新機能 pivot_*()」の説明が非常に分かりやすいのでまず,こちらを読んで,更に深いところを vignette から学ぶことを薦める.
https://speakerdeck.com/yutannihilation/tidyr-pivot

英語の記事では “Pivoting data from columns to rows (and back!) in the tidyverse” も評判が良い.私はまだ読んでいない.
http://www.storybench.org/pivoting-data-from-columns-to-rows-and-back-in-the-tidyverse/

vignette 原文: https://tidyr.tidyverse.org/dev/articles/pivot.html

はじめに

この vignette では pivot_longer()pivot_wider() の使い方を紹介する.これらの関数は gather()spread() の機能を拡張し,他のパッケージから最新の機能を取り込むことを目指す.

spread()gather() には設計の基本的な部分に誤りがあった.これらの関数がどの方向にデータを広げたり集めたりするのか予想することも覚えることも難しかった.更に引数も非常に覚え辛く,開発者を含め多くの人が度々ドキュメントを参照する羽目になった.

そこで R におけるデータ整形を発展させる2つの重要な新機能が,他のパッケージを参考に実装された.

  • pivot_longer() は型の異なる複数の値を記録した変数を扱うことができる.この機能は Matt Dowle と Arun Srinivasan による data.table パッケージにおける改良型の melt()dcast() を参考にした.
  • pivot_longer()pivot_wider() はデータフレームに対し,列名に記録されたメタデータがどのようにデータ変数に変換されるべきか,あるいは逆変換されるべきかを的確に指定できる.この機能は John Mount と Nina Zumel による cdata パッケージから着想を得た.

この vignette では,pivot_longer()pivot_wider() の背景にある重要なアイディアを紹介する.これらのアイディアはこれまであなたが単純なものから複雑なものまで様々なデータ整形に取り組んできた中で用いてきたものだ.

始めに必要なパッケージを読み込む.実用的には library(tidyverse) で読み込むだろうが,tidyverse をパッケージの vignette で読み込むことはできない1

library(tidyr)
library(dplyr)
library(readr)

Longer

pivot_longer() はデータセットの行を増やし,列を減らすことで長く (longer) する.開発者は「長い形式 (long form)」という呼び方は合理的ではないと考えている.なぜなら,長さは相対的なもので例えばデータセットの A と B のどちらが長いといった言い方しかできないからだ.

pivot_longer() は野生のデータセットに対してよく適用されるもので,分析を楽にするためというよりは,データの入力や比較を適切に行うために用いられる.次節からは, pivot_longer() の使い方を現実にありうる様々なデータセットを用いて紹介する.

列の名前が文字列データに相当する場合

relig_income データセットは人々の宗教や年収について集計した結果をカウントデータとして記録している.

relig_income
#> # A tibble: 18 x 11
#>    religion `<$10k` `$10-20k` `$20-30k` `$30-40k` `$40-50k` `$50-75k`
#>    <chr>      <dbl>     <dbl>     <dbl>     <dbl>     <dbl>     <dbl>
#>  1 Agnostic      27        34        60        81        76       137
#>  2 Atheist       12        27        37        52        35        70
#>  3 Buddhist      27        21        30        34        33        58
#>  4 Catholic     418       617       732       670       638      1116
#>  5 Don’t k…      15        14        15        11        10        35
#>  6 Evangel…     575       869      1064       982       881      1486
#>  7 Hindu          1         9         7         9        11        34
#>  8 Histori…     228       244       236       238       197       223
#>  9 Jehovah…      20        27        24        24        21        30
#> 10 Jewish        19        19        25        25        30        95
#> # … with 8 more rows, and 4 more variables: `$75-100k` <dbl>,
#> #   `$100-150k` <dbl>, `>150k` <dbl>, `Don't know/refused` <dbl>

データセットには3つの変数が含まれる.

  • religion は行ごとに記録されている
  • income は複数の列の名前として記録されている
  • count はセルごとの値として記録されている

pivot_longer() を使って整形してみよう.

relig_income %>% 
  pivot_longer(-religion, names_to = "income", values_to = "count")
#> Warning: `vec_type_common()` has been renamed to `vec_ptype_common()`.
#> This warning is displayed once per session.
#> # A tibble: 180 x 3
#>    religion income             count
#>    <chr>    <chr>              <dbl>
#>  1 Agnostic <$10k                 27
#>  2 Agnostic $10-20k               34
#>  3 Agnostic $20-30k               60
#>  4 Agnostic $30-40k               81
#>  5 Agnostic $40-50k               76
#>  6 Agnostic $50-75k              137
#>  7 Agnostic $75-100k             122
#>  8 Agnostic $100-150k            109
#>  9 Agnostic >150k                 84
#> 10 Agnostic Don't know/refused    96
#> # … with 170 more rows
  • 第一引数は整形したいデータセットをとる (例えば relig_income).

  • 第二引数は整形対象となる列を指定する.今回は religion 以外の全ての列だ.

  • names_to 引数には,列の名前として記録されたデータに変数としての名前を与える.今回なら income だ.

  • values_to 引数にはセルの値として記録されているデータに変数としての名前を与える.今回なら count だ.

names_tovalues_to のどちらによって作られる列relig_income には含まれていないため,これらの引数には引用符で囲った文字列を指定する.

列の名前が数値データに相当する場合

billboard データセットは2000年のビルボードランキングを記録している.データの形式としては relig_income と似ているが,列の名前に記録されたデータは文字列ではなく数値そのものである.

billboard
#> # A tibble: 317 x 79
#>    artist track date.entered   wk1   wk2   wk3   wk4   wk5   wk6   wk7
#>    <chr>  <chr> <date>       <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#>  1 2 Pac  Baby… 2000-02-26      87    82    72    77    87    94    99
#>  2 2Ge+h… The … 2000-09-02      91    87    92    NA    NA    NA    NA
#>  3 3 Doo… Kryp… 2000-04-08      81    70    68    67    66    57    54
#>  4 3 Doo… Loser 2000-10-21      76    76    72    69    67    65    55
#>  5 504 B… Wobb… 2000-04-15      57    34    25    17    17    31    36
#>  6 98^0   Give… 2000-08-19      51    39    34    26    26    19     2
#>  7 A*Tee… Danc… 2000-07-08      97    97    96    95   100    NA    NA
#>  8 Aaliy… I Do… 2000-01-29      84    62    51    41    38    35    35
#>  9 Aaliy… Try … 2000-03-18      59    53    38    28    21    18    16
#> 10 Adams… Open… 2000-08-26      76    76    74    69    68    67    61
#> # … with 307 more rows, and 69 more variables: wk8 <dbl>, wk9 <dbl>,
#> #   wk10 <dbl>, wk11 <dbl>, wk12 <dbl>, wk13 <dbl>, wk14 <dbl>,
#> #   wk15 <dbl>, wk16 <dbl>, wk17 <dbl>, wk18 <dbl>, wk19 <dbl>,
#> #   wk20 <dbl>, wk21 <dbl>, wk22 <dbl>, wk23 <dbl>, wk24 <dbl>,
#> #   wk25 <dbl>, wk26 <dbl>, wk27 <dbl>, wk28 <dbl>, wk29 <dbl>,
#> #   wk30 <dbl>, wk31 <dbl>, wk32 <dbl>, wk33 <dbl>, wk34 <dbl>,
#> #   wk35 <dbl>, wk36 <dbl>, wk37 <dbl>, wk38 <dbl>, wk39 <dbl>,
#> #   wk40 <dbl>, wk41 <dbl>, wk42 <dbl>, wk43 <dbl>, wk44 <dbl>,
#> #   wk45 <dbl>, wk46 <dbl>, wk47 <dbl>, wk48 <dbl>, wk49 <dbl>,
#> #   wk50 <dbl>, wk51 <dbl>, wk52 <dbl>, wk53 <dbl>, wk54 <dbl>,
#> #   wk55 <dbl>, wk56 <dbl>, wk57 <dbl>, wk58 <dbl>, wk59 <dbl>,
#> #   wk60 <dbl>, wk61 <dbl>, wk62 <dbl>, wk63 <dbl>, wk64 <dbl>,
#> #   wk65 <dbl>, wk66 <lgl>, wk67 <lgl>, wk68 <lgl>, wk69 <lgl>,
#> #   wk70 <lgl>, wk71 <lgl>, wk72 <lgl>, wk73 <lgl>, wk74 <lgl>,
#> #   wk75 <lgl>, wk76 <lgl>

まずは relig_income dataset と同様に billboard データセットを整形してみよう.列名は week という変数に,セルの値は rank という変数にしよう.加えて,整形時に欠損値が出たらその行を消去するように values_drop_na を使おう.必ずしも全ての曲が76週間ランキング圏内にあるわけではないため,入力したデータをvalues_drop_naを使わず整形すると,不必要に自明な NA が強制的に生じてしまう.

billboard %>% 
  pivot_longer(
    cols = starts_with("wk"), 
    names_to = "week", 
    values_to = "rank",
    values_drop_na = TRUE
  )
#> # A tibble: 5,307 x 5
#>    artist  track                   date.entered week   rank
#>    <chr>   <chr>                   <date>       <chr> <dbl>
#>  1 2 Pac   Baby Don't Cry (Keep... 2000-02-26   wk1      87
#>  2 2 Pac   Baby Don't Cry (Keep... 2000-02-26   wk2      82
#>  3 2 Pac   Baby Don't Cry (Keep... 2000-02-26   wk3      72
#>  4 2 Pac   Baby Don't Cry (Keep... 2000-02-26   wk4      77
#>  5 2 Pac   Baby Don't Cry (Keep... 2000-02-26   wk5      87
#>  6 2 Pac   Baby Don't Cry (Keep... 2000-02-26   wk6      94
#>  7 2 Pac   Baby Don't Cry (Keep... 2000-02-26   wk7      99
#>  8 2Ge+her The Hardest Part Of ... 2000-09-02   wk1      91
#>  9 2Ge+her The Hardest Part Of ... 2000-09-02   wk2      87
#> 10 2Ge+her The Hardest Part Of ... 2000-09-02   wk3      92
#> # … with 5,297 more rows

各曲が何週間ランキング入りしていたか簡単に分かると嬉しいが,それには変数 week を整数に直す必要がある.これには2つの引数を追加で指定する必要がある.まず names_prefix 引数によって接頭辞の wk を消した上で,names_ptypes 引数によって week が整数であることを指定する2

billboard %>% 
  pivot_longer(
    cols = starts_with("wk"), 
    names_to = "week", 
    names_prefix = "wk",
    names_ptypes = list(week = integer()),
    values_to = "rank",
    values_drop_na = TRUE
  )
#> # A tibble: 5,307 x 5
#>    artist  track                   date.entered  week  rank
#>    <chr>   <chr>                   <date>       <int> <dbl>
#>  1 2 Pac   Baby Don't Cry (Keep... 2000-02-26       1    87
#>  2 2 Pac   Baby Don't Cry (Keep... 2000-02-26       2    82
#>  3 2 Pac   Baby Don't Cry (Keep... 2000-02-26       3    72
#>  4 2 Pac   Baby Don't Cry (Keep... 2000-02-26       4    77
#>  5 2 Pac   Baby Don't Cry (Keep... 2000-02-26       5    87
#>  6 2 Pac   Baby Don't Cry (Keep... 2000-02-26       6    94
#>  7 2 Pac   Baby Don't Cry (Keep... 2000-02-26       7    99
#>  8 2Ge+her The Hardest Part Of ... 2000-09-02       1    91
#>  9 2Ge+her The Hardest Part Of ... 2000-09-02       2    87
#> 10 2Ge+her The Hardest Part Of ... 2000-09-02       3    92
#> # … with 5,297 more rows

列名に複数の変数がある場合

列名に複数の変数が詰めこまれていると,整形はより大変になる.例えば who データセットを見てみよう.

who
#> # A tibble: 7,240 x 60
#>    country iso2  iso3   year new_sp_m014 new_sp_m1524 new_sp_m2534
#>    <chr>   <chr> <chr> <int>       <int>        <int>        <int>
#>  1 Afghan… AF    AFG    1980          NA           NA           NA
#>  2 Afghan… AF    AFG    1981          NA           NA           NA
#>  3 Afghan… AF    AFG    1982          NA           NA           NA
#>  4 Afghan… AF    AFG    1983          NA           NA           NA
#>  5 Afghan… AF    AFG    1984          NA           NA           NA
#>  6 Afghan… AF    AFG    1985          NA           NA           NA
#>  7 Afghan… AF    AFG    1986          NA           NA           NA
#>  8 Afghan… AF    AFG    1987          NA           NA           NA
#>  9 Afghan… AF    AFG    1988          NA           NA           NA
#> 10 Afghan… AF    AFG    1989          NA           NA           NA
#> # … with 7,230 more rows, and 53 more variables: new_sp_m3544 <int>,
#> #   new_sp_m4554 <int>, new_sp_m5564 <int>, new_sp_m65 <int>,
#> #   new_sp_f014 <int>, new_sp_f1524 <int>, new_sp_f2534 <int>,
#> #   new_sp_f3544 <int>, new_sp_f4554 <int>, new_sp_f5564 <int>,
#> #   new_sp_f65 <int>, new_sn_m014 <int>, new_sn_m1524 <int>,
#> #   new_sn_m2534 <int>, new_sn_m3544 <int>, new_sn_m4554 <int>,
#> #   new_sn_m5564 <int>, new_sn_m65 <int>, new_sn_f014 <int>,
#> #   new_sn_f1524 <int>, new_sn_f2534 <int>, new_sn_f3544 <int>,
#> #   new_sn_f4554 <int>, new_sn_f5564 <int>, new_sn_f65 <int>,
#> #   new_ep_m014 <int>, new_ep_m1524 <int>, new_ep_m2534 <int>,
#> #   new_ep_m3544 <int>, new_ep_m4554 <int>, new_ep_m5564 <int>,
#> #   new_ep_m65 <int>, new_ep_f014 <int>, new_ep_f1524 <int>,
#> #   new_ep_f2534 <int>, new_ep_f3544 <int>, new_ep_f4554 <int>,
#> #   new_ep_f5564 <int>, new_ep_f65 <int>, newrel_m014 <int>,
#> #   newrel_m1524 <int>, newrel_m2534 <int>, newrel_m3544 <int>,
#> #   newrel_m4554 <int>, newrel_m5564 <int>, newrel_m65 <int>,
#> #   newrel_f014 <int>, newrel_f1524 <int>, newrel_f2534 <int>,
#> #   newrel_f3544 <int>, newrel_f4554 <int>, newrel_f5564 <int>,
#> #   newrel_f65 <int>

country, iso2, iso3, そして year は既に変数であり,そのままにしておけばいい.しかし new_sp_m014 列から newrel_f65 列までは4種類の変数がそれぞれの列名に含まれている.

  • 接頭辞の new_/new は新しくカウントした症状であることを示す.このデータセットは新しい例しか含まない.定数に相当するため今回は無視する.

  • sp/rel/sp/ep は症状を診断した手法を示す.

  • m/f は性別を示す.

  • 014/1524/2535/3544/4554/65 は年齢層を示す3

今回は names_pattern を使うとしっくりくるだろう. names_patternextract と似た操作性を持ち, () を用いてグループ化した正規表現を指定することで,列名からグループとして各変数を取り出すことができる.

who %>% pivot_longer(
  cols = new_sp_m014:newrel_f65,
  names_to = c("diagnosis", "gender", "age"), 
  names_pattern = "new_?(.*)_(.)(.*)",
  values_to = "count"
)
#> # A tibble: 405,440 x 8
#>    country     iso2  iso3   year diagnosis gender age   count
#>    <chr>       <chr> <chr> <int> <chr>     <chr>  <chr> <int>
#>  1 Afghanistan AF    AFG    1980 sp        m      014      NA
#>  2 Afghanistan AF    AFG    1980 sp        m      1524     NA
#>  3 Afghanistan AF    AFG    1980 sp        m      2534     NA
#>  4 Afghanistan AF    AFG    1980 sp        m      3544     NA
#>  5 Afghanistan AF    AFG    1980 sp        m      4554     NA
#>  6 Afghanistan AF    AFG    1980 sp        m      5564     NA
#>  7 Afghanistan AF    AFG    1980 sp        m      65       NA
#>  8 Afghanistan AF    AFG    1980 sp        f      014      NA
#>  9 Afghanistan AF    AFG    1980 sp        f      1524     NA
#> 10 Afghanistan AF    AFG    1980 sp        f      2534     NA
#> # … with 405,430 more rows

更に一歩進んで genderage 列に型を指定してみよう.この作法は値の分かっているカテゴリカルな変数の操作に優れている.

who %>% pivot_longer(
  cols = new_sp_m014:newrel_f65,
  names_to = c("diagnosis", "gender", "age"), 
  names_pattern = "new_?(.*)_(.)(.*)",
  names_ptypes = list(
    gender = factor(levels = c("f", "m")),
    age = factor(
      levels = c("014", "1524", "2534", "3544", "4554", "5564", "65"), 
      ordered = TRUE
    )
  ),
  values_to = "count"
)
#> # A tibble: 405,440 x 8
#>    country     iso2  iso3   year diagnosis gender age   count
#>    <chr>       <chr> <chr> <int> <chr>     <fct>  <ord> <int>
#>  1 Afghanistan AF    AFG    1980 sp        m      014      NA
#>  2 Afghanistan AF    AFG    1980 sp        m      1524     NA
#>  3 Afghanistan AF    AFG    1980 sp        m      2534     NA
#>  4 Afghanistan AF    AFG    1980 sp        m      3544     NA
#>  5 Afghanistan AF    AFG    1980 sp        m      4554     NA
#>  6 Afghanistan AF    AFG    1980 sp        m      5564     NA
#>  7 Afghanistan AF    AFG    1980 sp        m      65       NA
#>  8 Afghanistan AF    AFG    1980 sp        f      014      NA
#>  9 Afghanistan AF    AFG    1980 sp        f      1524     NA
#> 10 Afghanistan AF    AFG    1980 sp        f      2534     NA
#> # … with 405,430 more rows

一行に複数の観測値がある場合4

これまで見てきたデータフレームでは,整形時にセルの値が入る列 (value column) は1つだった.しかし多くの重要な整形問題では,セルの値が複数列に入ることがある.このような問題は,整形後のデータフレームに欲しい列名が,整形前のデータフレームの列名の一部になっていることで発覚する.この節では,そんなデータをどうやって整形するか見ていく.

以下の例の出典は data.table vignette で,tidyr における解法を閃くきっかけとなった.

family <- tribble(
  ~family,  ~dob_child1,  ~dob_child2, ~gender_child1, ~gender_child2,
       1L, "1998-11-26", "2000-01-29",             1L,             2L,
       2L, "1996-06-22",           NA,             2L,             NA,
       3L, "2002-07-11", "2004-04-05",             2L,             2L,
       4L, "2004-10-10", "2009-08-27",             1L,             1L,
       5L, "2000-12-05", "2005-02-28",             2L,             1L,
)
family <- family %>% mutate_at(vars(starts_with("dob")), parse_date)
family
#> # A tibble: 5 x 5
#>   family dob_child1 dob_child2 gender_child1 gender_child2
#>    <int> <date>     <date>             <int>         <int>
#> 1      1 1998-11-26 2000-01-29             1             2
#> 2      2 1996-06-22 NA                     2            NA
#> 3      3 2002-07-11 2004-04-05             2             2
#> 4      4 2004-10-10 2009-08-27             1             1
#> 5      5 2000-12-05 2005-02-28             2             1

ここには子供たちそれぞれの genderdob (誕生日) の2種類の情報 (または値) が載っている.これらの情報を別々の列に分けなければならい.そこで names_to に複数の値を指定し,names_sep を使って変数の名前を切り分ける.ただし,特別な変数名である .value を使って pivot_longer() に列名の一部が整形後の value column になることを伝える.

family %>% 
  pivot_longer(
    -family, 
    names_to = c(".value", "child"),
    names_sep = "_", 
    values_drop_na = TRUE
  )
#> # A tibble: 9 x 4
#>   family child  dob        gender
#>    <int> <chr>  <date>      <int>
#> 1      1 child1 1998-11-26      1
#> 2      1 child2 2000-01-29      2
#> 3      2 child1 1996-06-22      2
#> 4      3 child1 2002-07-11      2
#> 5      3 child2 2004-04-05      2
#> 6      4 child1 2004-10-10      1
#> 7      4 child2 2009-08-27      1
#> 8      5 child1 2000-12-05      2
#> 9      5 child2 2005-02-28      1

values_drop_na = TRUE を使うことで,入力データ上に観測値のない部分が出力において明示的に欠測値になってしまうことを防いでいる点に注意されたい.

この問題は base R に含まれている anscombe データセットにも存在する.

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’s quartet を構成する4対の変数から成る (x1y1x2y2,など).これら4つのデータセットは内容が大きく異なるにも拘らず同じ要約統計量を示す(平均,標準偏差,相関など).これを set, x, y から成るデータセットに整形してみる.

anscombe %>% 
  pivot_longer(everything(), 
    names_to = c(".value", "set"), 
    names_pattern = "(.)(.)"
  ) %>% 
  arrange(set)
#> # A tibble: 44 x 3
#>    set       x     y
#>    <chr> <dbl> <dbl>
#>  1 1        10  8.04
#>  2 1         8  6.95
#>  3 1        13  7.58
#>  4 1         9  8.81
#>  5 1        11  8.33
#>  6 1        14  9.96
#>  7 1         6  7.24
#>  8 1         4  4.26
#>  9 1        12 10.8 
#> 10 1         7  4.82
#> # … with 34 more rows

パネルデータでも似たような状況に遭遇する.例えば Thomas Leeper によるデータセットを例にとってみよう.このデータの整形方法は anscombe に対して行ったものと同様だ.

pnl <- tibble(
  x = 1:4,
  a = c(1, 1,0, 0),
  b = c(0, 1, 1, 1),
  y1 = rnorm(4),
  y2 = rnorm(4),
  z1 = rep(3, 4),
  z2 = rep(-2, 4),
)

pnl %>% 
  pivot_longer(
    -c(x, a, b), 
    names_to = c(".value", "time"), 
    names_pattern = "(.)(.)"
  )
#> # A tibble: 8 x 6
#>       x     a     b time       y     z
#>   <int> <dbl> <dbl> <chr>  <dbl> <dbl>
#> 1     1     1     0 1      0.148     3
#> 2     1     1     0 2     -0.598    -2
#> 3     2     1     1 1      1.25      3
#> 4     2     1     1 2      0.171    -2
#> 5     3     0     1 1     -0.858     3
#> 6     3     0     1 2     -0.205    -2
#> 7     4     0     1 1      1.39      3
#> 8     4     0     1 2     -0.262    -2

列名が重複している場合

列名が重複したデータセットに遭遇することもあるだろう.一般的には R でこのようなデータセットを扱うのは難しい.なぜなら,名前で列を参照しようとすると,最初にマッチしたものしか参照できないからだ.列名の重複した tibble を作るには,そのようなデータセットの作成を防ぐ列名の修正機能を明示的に止めておく必要がある.

df <- tibble(x = 1:3, y = 4:6, y = 5:7, y = 7:9, .name_repair = "minimal")
df
#> # A tibble: 3 x 4
#>       x     y     y     y
#>   <int> <int> <int> <int>
#> 1     1     4     5     7
#> 2     2     5     6     8
#> 3     3     6     7     9

このようなデータを pivot_longer() で処理すると,出力には自動的に新しい列を追加される.

df %>% pivot_longer(-x, names_to = "name", values_to = "value")
#> Warning: Duplicate column names detected, adding .copy variable
#> # A tibble: 9 x 4
#>       x name  .copy value
#>   <int> <chr> <int> <int>
#> 1     1 y         1     4
#> 2     1 y         2     5
#> 3     1 y         3     7
#> 4     2 y         1     5
#> 5     2 y         2     6
#> 6     2 y         3     8
#> 7     3 y         1     6
#> 8     3 y         2     7
#> 9     3 y         3     9

Wider

pivot_wider()pivot_longer() の反対で,列数を増やして行数を減らすことで,データセットを広く (wider) する.pivot_wider() を使って整然データを作ることは珍しいが,pivot_wider() は発表用に要約した表を作る際やデータを他のツールに必要な形式に変換する際に役立つ.

Capture-recapture data

Myfanwy Johnston による fish_encounters データセットは,自動観測機が川を下る魚を検出したかをまとめている.

fish_encounters
#> # A tibble: 114 x 3
#>    fish  station  seen
#>    <fct> <fct>   <int>
#>  1 4842  Release     1
#>  2 4842  I80_1       1
#>  3 4842  Lisbon      1
#>  4 4842  Rstr        1
#>  5 4842  Base_TD     1
#>  6 4842  BCE         1
#>  7 4842  BCW         1
#>  8 4842  BCE2        1
#>  9 4842  BCW2        1
#> 10 4842  MAE         1
#> # … with 104 more rows

解析ツールの多くは,このデータが観測機ごとに列を成す形式に従っていることを求める.

fish_encounters %>% pivot_wider(names_from = station, values_from = seen)
#> # A tibble: 19 x 12
#>    fish  Release I80_1 Lisbon  Rstr Base_TD   BCE   BCW  BCE2  BCW2   MAE
#>    <fct>   <int> <int>  <int> <int>   <int> <int> <int> <int> <int> <int>
#>  1 4842        1     1      1     1       1     1     1     1     1     1
#>  2 4843        1     1      1     1       1     1     1     1     1     1
#>  3 4844        1     1      1     1       1     1     1     1     1     1
#>  4 4845        1     1      1     1       1    NA    NA    NA    NA    NA
#>  5 4847        1     1      1    NA      NA    NA    NA    NA    NA    NA
#>  6 4848        1     1      1     1      NA    NA    NA    NA    NA    NA
#>  7 4849        1     1     NA    NA      NA    NA    NA    NA    NA    NA
#>  8 4850        1     1     NA     1       1     1     1    NA    NA    NA
#>  9 4851        1     1     NA    NA      NA    NA    NA    NA    NA    NA
#> 10 4854        1     1     NA    NA      NA    NA    NA    NA    NA    NA
#> # … with 9 more rows, and 1 more variable: MAW <int>

このデータセットは,ある魚が観測機に観測された時だけ記録していて,観測されなかった時のことは記録していない (この種のデータでは一般的なことだ).従って出力は NA で埋められる.しかしこの場合,欠測が魚を見なかったことを意味すると分かっているので,pivot_wider() を使って欠測値を0で埋められる.

fish_encounters %>% pivot_wider(
  names_from = station, 
  values_from = seen,
  values_fill = list(seen = 0)
)
#> # A tibble: 19 x 12
#>    fish  Release I80_1 Lisbon  Rstr Base_TD   BCE   BCW  BCE2  BCW2   MAE
#>    <fct>   <int> <int>  <int> <int>   <int> <int> <int> <int> <int> <int>
#>  1 4842        1     1      1     1       1     1     1     1     1     1
#>  2 4843        1     1      1     1       1     1     1     1     1     1
#>  3 4844        1     1      1     1       1     1     1     1     1     1
#>  4 4845        1     1      1     1       1     0     0     0     0     0
#>  5 4847        1     1      1     0       0     0     0     0     0     0
#>  6 4848        1     1      1     1       0     0     0     0     0     0
#>  7 4849        1     1      0     0       0     0     0     0     0     0
#>  8 4850        1     1      0     1       1     1     1     0     0     0
#>  9 4851        1     1      0     0       0     0     0     0     0     0
#> 10 4854        1     1      0     0       0     0     0     0     0     0
#> # … with 9 more rows, and 1 more variable: MAW <int>

集約 (Aggregation)

また,pivot_wider() を使って単純な集約を行うこともできる.Base R に組込まれている warpbreaks データセットを例にとってみよう (出力の見栄えをよくするため tibble に変換しておく):

warpbreaks <- warpbreaks %>% as_tibble() %>% select(wool, tension, breaks)
warpbreaks
#> # A tibble: 54 x 3
#>    wool  tension breaks
#>    <fct> <fct>    <dbl>
#>  1 A     L           26
#>  2 A     L           30
#>  3 A     L           54
#>  4 A     L           25
#>  5 A     L           70
#>  6 A     L           52
#>  7 A     L           51
#>  8 A     L           26
#>  9 A     L           67
#> 10 A     M           18
#> # … with 44 more rows

これは,wool (AB) と tension (L, M, H) の全組み合わせに対し, 9回ずつ実験を繰り返した計画実験だ.

warpbreaks %>% count(wool, tension)
#> # A tibble: 6 x 3
#>   wool  tension     n
#>   <fct> <fct>   <int>
#> 1 A     L           9
#> 2 A     M           9
#> 3 A     H           9
#> 4 B     L           9
#> 5 B     M           9
#> 6 B     H           9

wool (訳注: factor 型) の水準を列に展開するとどうなるだろうか.

warpbreaks %>% pivot_wider(names_from = wool, values_from = breaks)
#> Warning: Values in `breaks` are not uniquely identified; output will contain list-cols.
#> * Use `values_fn = list(breaks = list)` to suppress this warning.
#> * Use `values_fn = list(breaks = length)` to identify where the duplicates arise
#> * Use `values_fn = list(breaks = summary_fun)` to summarise duplicates
#> # A tibble: 3 x 3
#>   tension           A           B
#>   <fct>   <list<dbl>> <list<dbl>>
#> 1 L               [9]         [9]
#> 2 M               [9]         [9]
#> 3 H               [9]         [9]

出力される各セルが入力された複数のセルに対応すると警告される.既定の挙動ではリストから成る列 (list-columns) を生成し,各要素は全ての個別の値を格納する.より便利な出力は要約統計量だろう.例えば mean によって,wooltention の組み合わせごとに平均値を算出できる.

warpbreaks %>% 
  pivot_wider(
    names_from = wool, 
    values_from = breaks,
    values_fn = list(breaks = mean)
  )
#> # A tibble: 3 x 3
#>   tension     A     B
#>   <fct>   <dbl> <dbl>
#> 1 L        44.6  28.2
#> 2 M        24    28.8
#> 3 H        24.6  18.8

より複雑な要約は,整形前に行っておくことを薦めるが,単純なものは pivot_wider() で済ましてしまう方が便利なことも多い.

複数の変数から複数の列名を生成する

あるデータセットが product,country,yearの組み合わせで構成されているとしよう (例えば http://stackoverflow.com/questions/24929954).整然とした形式では以下のようになる.

production <- expand_grid(
    product = c("A", "B"), 
    country = c("AI", "EI"), 
    year = 2000:2014
  ) %>%
  filter((product == "A" & country == "AI") | product == "B") %>% 
  mutate(production = rnorm(nrow(.)))
production
#> # A tibble: 45 x 4
#>    product country  year production
#>    <chr>   <chr>   <int>      <dbl>
#>  1 A       AI       2000     0.755 
#>  2 A       AI       2001    -0.730 
#>  3 A       AI       2002     0.0795
#>  4 A       AI       2003    -2.40  
#>  5 A       AI       2004    -1.20  
#>  6 A       AI       2005     0.751 
#>  7 A       AI       2006    -0.432 
#>  8 A       AI       2007     0.523 
#>  9 A       AI       2008    -0.522 
#> 10 A       AI       2009    -1.12  
#> # … with 35 more rows

このデータを広げて,列ごとに異なる productcountry の組み合わせを示すようにしよう.names_from に複数の変数を指定するのが鍵だ.

production %>% pivot_wider(
  names_from = c(product, country), 
  values_from = production
)
#> # A tibble: 15 x 4
#>     year    A_AI   B_AI   B_EI
#>    <int>   <dbl>  <dbl>  <dbl>
#>  1  2000  0.755  -0.107  1.32 
#>  2  2001 -0.730  -1.03   0.968
#>  3  2002  0.0795 -0.279  1.85 
#>  4  2003 -2.40    0.667  0.682
#>  5  2004 -1.20    0.100  0.215
#>  6  2005  0.751   1.77   1.57 
#>  7  2006 -0.432  -0.914 -0.145
#>  8  2007  0.523   0.363 -1.85 
#>  9  2008 -0.522   0.337 -0.748
#> 10  2009 -1.12   -0.336 -0.491
#> # … with 5 more rows

整然とした国勢調査 (Tidy census)

us_rent_income データセットは,2017年のアメリカ合衆国における年収と家賃の中央値を州ごとに集計している (the American Community Survey から tidycensus パッケージによって入手した).

us_rent_income
#> # A tibble: 104 x 5
#>    GEOID NAME       variable estimate   moe
#>    <chr> <chr>      <chr>       <dbl> <dbl>
#>  1 01    Alabama    income      24476   136
#>  2 01    Alabama    rent          747     3
#>  3 02    Alaska     income      32940   508
#>  4 02    Alaska     rent         1200    13
#>  5 04    Arizona    income      27517   148
#>  6 04    Arizona    rent          972     4
#>  7 05    Arkansas   income      23789   165
#>  8 05    Arkansas   rent          709     5
#>  9 06    California income      29454   109
#> 10 06    California rent         1358     3
#> # … with 94 more rows

estimatemoe は両方とも values columns であるため,values_from に指定する.

us_rent_income %>% 
  pivot_wider(names_from = variable, values_from = c(estimate, moe))
#> # A tibble: 52 x 6
#>    GEOID NAME             estimate_income estimate_rent moe_income moe_rent
#>    <chr> <chr>                      <dbl>         <dbl>      <dbl>    <dbl>
#>  1 01    Alabama                    24476           747        136        3
#>  2 02    Alaska                     32940          1200        508       13
#>  3 04    Arizona                    27517           972        148        4
#>  4 05    Arkansas                   23789           709        165        5
#>  5 06    California                 29454          1358        109        3
#>  6 08    Colorado                   32401          1125        109        5
#>  7 09    Connecticut                35326          1123        195        5
#>  8 10    Delaware                   31560          1076        247       10
#>  9 11    District of Col…           43198          1424        681       17
#> 10 12    Florida                    25952          1077         70        3
#> # … with 42 more rows

出力される列には変数名が自動的に追記されることに注意されたい.

連絡帳

最後に取り組む問題は Jiena Gu に影響されたものだ.以下のようにウェブサイトからコピー&ペーストした連絡帳があるとしよう.

contacts <- tribble(
  ~field, ~value,
  "name", "Jiena McLellan",
  "company", "Toyota", 
  "name", "John Smith", 
  "company", "google", 
  "email", "[email protected]",
  "name", "Huxley Ratcliffe"
)

このデータセットではどの値が同じ組になるか分からないことが課題だ.全ての連絡先が名前 (name) から始まっていることに気付けば,この課題を解決できる.field 列に “name” が現れる度に固有の id を割り当てよう.

contacts <- contacts %>% 
  mutate(
    person_id = cumsum(field == "name")
  )
contacts
#> # A tibble: 6 x 3
#>   field   value            person_id
#>   <chr>   <chr>                <int>
#> 1 name    Jiena McLellan           1
#> 2 company Toyota                   1
#> 3 name    John Smith               2
#> 4 company google                   2
#> 5 email   [email protected]          2
#> 6 name    Huxley Ratcliffe         3

これで各人に対し,固有な識別子が割り当てられたので,fieldvalue を列に展開できる.

contacts %>% 
  pivot_wider(names_from = field, values_from = value)
#> # A tibble: 3 x 4
#>   person_id name             company email          
#>       <int> <chr>            <chr>   <chr>          
#> 1         1 Jiena McLellan   Toyota  <NA>           
#> 2         2 John Smith       google  [email protected]
#> 3         3 Huxley Ratcliffe <NA>    <NA>

Longer にしてから wider にする

一方向に pivot するだけでは問題が解決しないことがある.本節ではpivot_longer()pivot_wider() を組み合わせて複雑な問題を解決する例を紹介する.

世界銀行

world_bank_pop は世界銀行による2000年から2018年までの各国の人口に関するデータが記録されている.

world_bank_pop
#> # A tibble: 1,056 x 20
#>    country indicator `2000` `2001` `2002` `2003`  `2004`  `2005`   `2006`
#>    <chr>   <chr>      <dbl>  <dbl>  <dbl>  <dbl>   <dbl>   <dbl>    <dbl>
#>  1 ABW     SP.URB.T… 4.24e4 4.30e4 4.37e4 4.42e4 4.47e+4 4.49e+4  4.49e+4
#>  2 ABW     SP.URB.G… 1.18e0 1.41e0 1.43e0 1.31e0 9.51e-1 4.91e-1 -1.78e-2
#>  3 ABW     SP.POP.T… 9.09e4 9.29e4 9.50e4 9.70e4 9.87e+4 1.00e+5  1.01e+5
#>  4 ABW     SP.POP.G… 2.06e0 2.23e0 2.23e0 2.11e0 1.76e+0 1.30e+0  7.98e-1
#>  5 AFG     SP.URB.T… 4.44e6 4.65e6 4.89e6 5.16e6 5.43e+6 5.69e+6  5.93e+6
#>  6 AFG     SP.URB.G… 3.91e0 4.66e0 5.13e0 5.23e0 5.12e+0 4.77e+0  4.12e+0
#>  7 AFG     SP.POP.T… 2.01e7 2.10e7 2.20e7 2.31e7 2.41e+7 2.51e+7  2.59e+7
#>  8 AFG     SP.POP.G… 3.49e0 4.25e0 4.72e0 4.82e0 4.47e+0 3.87e+0  3.23e+0
#>  9 AGO     SP.URB.T… 8.23e6 8.71e6 9.22e6 9.77e6 1.03e+7 1.09e+7  1.15e+7
#> 10 AGO     SP.URB.G… 5.44e0 5.59e0 5.70e0 5.76e0 5.75e+0 5.69e+0  4.92e+0
#> # … with 1,046 more rows, and 11 more variables: `2007` <dbl>,
#> #   `2008` <dbl>, `2009` <dbl>, `2010` <dbl>, `2011` <dbl>, `2012` <dbl>,
#> #   `2013` <dbl>, `2014` <dbl>, `2015` <dbl>, `2016` <dbl>, `2017` <dbl>

このデータセットを,それぞれの変数が一つの列に納ままるように整形しよう.この段階ではどのような手順を踏めばいいか分からないが,まず最も明らかな問題である,複数列にまたがる yaer を修正しよう.

pop2 <- world_bank_pop %>% 
  pivot_longer(`2000`:`2017`, names_to = "year", values_to = "value")
pop2
#> # A tibble: 19,008 x 4
#>    country indicator   year  value
#>    <chr>   <chr>       <chr> <dbl>
#>  1 ABW     SP.URB.TOTL 2000  42444
#>  2 ABW     SP.URB.TOTL 2001  43048
#>  3 ABW     SP.URB.TOTL 2002  43670
#>  4 ABW     SP.URB.TOTL 2003  44246
#>  5 ABW     SP.URB.TOTL 2004  44669
#>  6 ABW     SP.URB.TOTL 2005  44889
#>  7 ABW     SP.URB.TOTL 2006  44881
#>  8 ABW     SP.URB.TOTL 2007  44686
#>  9 ABW     SP.URB.TOTL 2008  44375
#> 10 ABW     SP.URB.TOTL 2009  44052
#> # … with 18,998 more rows

次に, indicator 列を見てみよう.

pop2 %>% count(indicator)
#> # A tibble: 4 x 2
#>   indicator       n
#>   <chr>       <int>
#> 1 SP.POP.GROW  4752
#> 2 SP.POP.TOTL  4752
#> 3 SP.URB.GROW  4752
#> 4 SP.URB.TOTL  4752

SP.POP.GROW は人口増加を, SP.POP.TOTL は総人口を, そして SP.URB.* は都会における人口増加と総人口を示す.これらを観測値が全域から得たものか都会から得たものか示す area 列と,観測値が総人口と人口増加のどちらを記録しているか示す variable 列の2列に分けよう.

pop3 <- pop2 %>% 
  separate(indicator, c(NA, "area", "variable"))
pop3
#> # A tibble: 19,008 x 5
#>    country area  variable year  value
#>    <chr>   <chr> <chr>    <chr> <dbl>
#>  1 ABW     URB   TOTL     2000  42444
#>  2 ABW     URB   TOTL     2001  43048
#>  3 ABW     URB   TOTL     2002  43670
#>  4 ABW     URB   TOTL     2003  44246
#>  5 ABW     URB   TOTL     2004  44669
#>  6 ABW     URB   TOTL     2005  44889
#>  7 ABW     URB   TOTL     2006  44881
#>  8 ABW     URB   TOTL     2007  44686
#>  9 ABW     URB   TOTL     2008  44375
#> 10 ABW     URB   TOTL     2009  44052
#> # … with 18,998 more rows

最後に variable 列と value 列を展開し,TOTL 列と GROW 列にしよう.

pop3 %>% 
  pivot_wider(names_from = variable, values_from = value)
#> # A tibble: 9,504 x 5
#>    country area  year   TOTL    GROW
#>    <chr>   <chr> <chr> <dbl>   <dbl>
#>  1 ABW     URB   2000  42444  1.18  
#>  2 ABW     URB   2001  43048  1.41  
#>  3 ABW     URB   2002  43670  1.43  
#>  4 ABW     URB   2003  44246  1.31  
#>  5 ABW     URB   2004  44669  0.951 
#>  6 ABW     URB   2005  44889  0.491 
#>  7 ABW     URB   2006  44881 -0.0178
#>  8 ABW     URB   2007  44686 -0.435 
#>  9 ABW     URB   2008  44375 -0.698 
#> 10 ABW     URB   2009  44052 -0.731 
#> # … with 9,494 more rows

複数回答

Maxime Wack による提案を受けて https://github.com/tidyverse/tidyr/issues/384),よくある複数回答データをどのように扱えばいいかを最後の例としよう.複数回答データは以下のような形式であることが多い.

multi <- tribble(
  ~id, ~choice1, ~choice2, ~choice3,
  1, "A", "B", "C",
  2, "C", "B",  NA,
  3, "D",  NA,  NA,
  4, "B", "D",  NA
)

しかし,実際の順番は重要ではなく,id ごとの解答内容を示したいとしよう5.望んだ形式への変換は2つの操作で完了する.まず,データを longer 形式にし,自明な NA を消去し,選択肢から回答を得られたかを示す列を追加する.

multi2 <- multi %>% 
  pivot_longer(-id, values_drop_na = TRUE) %>% 
  mutate(checked = TRUE)
multi2
#> # A tibble: 8 x 4
#>      id name    value checked
#>   <dbl> <chr>   <chr> <lgl>  
#> 1     1 choice1 A     TRUE   
#> 2     1 choice2 B     TRUE   
#> 3     1 choice3 C     TRUE   
#> 4     2 choice1 C     TRUE   
#> 5     2 choice2 B     TRUE   
#> 6     3 choice1 D     TRUE   
#> 7     4 choice1 B     TRUE   
#> 8     4 choice2 D     TRUE

次にデータを wider 型にし,無回答部分を FALSE で埋める.

multi2 %>% 
  pivot_wider(
    id_cols = id,
    names_from = value, 
    values_from = checked, 
    values_fill = list(checked = FALSE)
  )
#> # A tibble: 4 x 5
#>      id A     B     C     D    
#>   <dbl> <lgl> <lgl> <lgl> <lgl>
#> 1     1 TRUE  TRUE  TRUE  FALSE
#> 2     2 FALSE TRUE  TRUE  FALSE
#> 3     3 FALSE FALSE FALSE TRUE 
#> 4     4 FALSE TRUE  FALSE TRUE

Manual specs

pivot_longer()pivot_wider() の引数は,様々なデータセットの整形を可能とする.しかし人々がデータ構造に対して発揮する想像力は無尽蔵にも見えるので,一見 pivot_longer()pivot_wider() でもどうしようもない問題に遭遇することもありえる.より柔軟な整形を行うためには,仕様となるデータフレームを作り,厳格に列名ごとに格納されるデータがどのようにあるべきか,あるいはある種のデータが格納される列の名前がどのようにあるべきか,決めておける.本節では,どのようにデータ構造の仕様を定めればよいか紹介し, pivot_longer()pivot_wider() が不十分な場合にその仕様をどう示せばいいのかを紹介する.

Longer

仕様の策定方法を学ぶにあたって, relig_income データセットを用いたもっとも単純な整形をやり直してみよう.今回は整形に2段階の手順を踏む.まず,仕様となるオブジェクトを pivot_longer_spec() を用いて作り,整形時に仕様を利用する.

spec <- relig_income %>% pivot_longer_spec(
  cols = -religion, 
  names_to = "income",
  values_to = "count"
)
relig_income %>% pivot_longer(spec = spec)
#> # A tibble: 180 x 3
#>    religion income             count
#>    <chr>    <chr>              <dbl>
#>  1 Agnostic <$10k                 27
#>  2 Agnostic $10-20k               34
#>  3 Agnostic $20-30k               60
#>  4 Agnostic $30-40k               81
#>  5 Agnostic $40-50k               76
#>  6 Agnostic $50-75k              137
#>  7 Agnostic $75-100k             122
#>  8 Agnostic $100-150k            109
#>  9 Agnostic >150k                 84
#> 10 Agnostic Don't know/refused    96
#> # … with 170 more rows

(コードは増えたが結果は以前と同じだ.この場合, spec を定める必要はないが,用例を示す単純な例として紹介した.)

spec の中身はデータフレームで,各行は元のデータフレームの各列に対応しており6. から始まる2つの特別な列を持つ.

  • .name は列名を示す.
  • .value はセルの移動先となる列の名前を示す.
spec
#> # A tibble: 10 x 3
#>    .name              .value income            
#>    <chr>              <chr>  <chr>             
#>  1 <$10k              count  <$10k             
#>  2 $10-20k            count  $10-20k           
#>  3 $20-30k            count  $20-30k           
#>  4 $30-40k            count  $30-40k           
#>  5 $40-50k            count  $40-50k           
#>  6 $50-75k            count  $50-75k           
#>  7 $75-100k           count  $75-100k          
#>  8 $100-150k          count  $100-150k         
#>  9 >150k              count  >150k             
#> 10 Don't know/refused count  Don't know/refused

Wider

以下では us_rent_incomepivot_wider() によって広げている.良い結果が得られているが改善の余地があるだろう.

us_rent_income %>% 
  pivot_wider(names_from = variable, values_from = c(estimate, moe))
#> # A tibble: 52 x 6
#>    GEOID NAME             estimate_income estimate_rent moe_income moe_rent
#>    <chr> <chr>                      <dbl>         <dbl>      <dbl>    <dbl>
#>  1 01    Alabama                    24476           747        136        3
#>  2 02    Alaska                     32940          1200        508       13
#>  3 04    Arizona                    27517           972        148        4
#>  4 05    Arkansas                   23789           709        165        5
#>  5 06    California                 29454          1358        109        3
#>  6 08    Colorado                   32401          1125        109        5
#>  7 09    Connecticut                35326          1123        195        5
#>  8 10    Delaware                   31560          1076        247       10
#>  9 11    District of Col…           43198          1424        681       17
#> 10 12    Florida                    25952          1077         70        3
#> # … with 42 more rows

手製の仕様 (manual spec) を使って列名を rentrent_moeincomeincome_moe に改善しよう.現在の仕様は以下の通りだ.

us_rent_income %>% 
  pivot_wider_spec(names_from = variable, values_from = c(estimate, moe))
#> # A tibble: 4 x 3
#>   .name           .value   variable
#>   <chr>           <chr>    <chr>   
#> 1 estimate_income estimate income  
#> 2 estimate_rent   estimate rent    
#> 3 moe_income      moe      income  
#> 4 moe_rent        moe      rent

まず,.valuevariable の組み合わせを全て作り,注意深く列名を決める7

spec <- us_rent_income %>% 
  expand(variable, .value = c("estimate", "moe")) %>% 
  mutate(
    .name = paste0(variable, ifelse(.value == "moe", "_moe", ""))
  )
spec
#> # A tibble: 4 x 3
#>   variable .value   .name     
#>   <chr>    <chr>    <chr>     
#> 1 income   estimate income    
#> 2 income   moe      income_moe
#> 3 rent     estimate rent      
#> 4 rent     moe      rent_moe

この仕様を pivot_wider() に与えると,望み通りのデータフレームが得られる.

us_rent_income %>% pivot_wider(spec = spec)
#> # A tibble: 52 x 6
#>    GEOID NAME                 income income_moe  rent rent_moe
#>    <chr> <chr>                 <dbl>      <dbl> <dbl>    <dbl>
#>  1 01    Alabama               24476        136   747        3
#>  2 02    Alaska                32940        508  1200       13
#>  3 04    Arizona               27517        148   972        4
#>  4 05    Arkansas              23789        165   709        5
#>  5 06    California            29454        109  1358        3
#>  6 08    Colorado              32401        109  1125        5
#>  7 09    Connecticut           35326        195  1123        5
#>  8 10    Delaware              31560        247  1076       10
#>  9 11    District of Columbia  43198        681  1424       17
#> 10 12    Florida               25952         70  1077        3
#> # … with 42 more rows

By hand

時には仕様を算出させるのが不可能な場合や不都合な場合があり,自前 (“by hand”) で仕様を定めた方が都合がいいこともある.construction データを例に見てみよう.このデータは下記 URL の表5 “completions” を少し改造したものだ https://www.census.gov/construction/nrc/index.html

construction
#> # A tibble: 9 x 9
#>    Year Month `1 unit` `2 to 4 units` `5 units or mor… Northeast Midwest
#>   <dbl> <chr>    <dbl> <lgl>                     <dbl>     <dbl>   <dbl>
#> 1  2018 Janu…      859 NA                          348       114     169
#> 2  2018 Febr…      882 NA                          400       138     160
#> 3  2018 March      862 NA                          356       150     154
#> 4  2018 April      797 NA                          447       144     196
#> 5  2018 May        875 NA                          364        90     169
#> 6  2018 June       867 NA                          342        76     170
#> 7  2018 July       829 NA                          360       108     183
#> 8  2018 Augu…      939 NA                          286        90     205
#> 9  2018 Sept…      835 NA                          304       117     175
#> # … with 2 more variables: South <dbl>, West <dbl>

このようなデータは政府機関が発行するものにありがちで,列名が色々な変数に属している.今回の場合は色々なユニットごと (1, 2–4, 5+) に要約した列と,国内の地方ごと (NE, NW, midwest, S, W) の要約した列がある.このようなデータを整形する際の仕様は tibble を使って簡単に記述できる.

spec <- tribble(
  ~.name,            ~.value, ~units,  ~region,     
  "1 unit",          "n",     "1",     NA,          
  "2 to 4 units",    "n",     "2-4",   NA,          
  "5 units or more", "n",     "5+",    NA,          
  "Northeast",       "n",     NA,      "Northeast", 
  "Midwest",         "n",     NA,      "Midwest",   
  "South",           "n",     NA,      "South",     
  "West",            "n",     NA,      "West",      
)

この仕様を用いると,以下のような longer な形式になる.

construction %>% pivot_longer(spec = spec)
#> # A tibble: 63 x 5
#>     Year Month    units region        n
#>    <dbl> <chr>    <chr> <chr>     <dbl>
#>  1  2018 January  1     <NA>        859
#>  2  2018 January  2-4   <NA>         NA
#>  3  2018 January  5+    <NA>        348
#>  4  2018 January  <NA>  Northeast   114
#>  5  2018 January  <NA>  Midwest     169
#>  6  2018 January  <NA>  South       596
#>  7  2018 January  <NA>  West        339
#>  8  2018 February 1     <NA>        882
#>  9  2018 February 2-4   <NA>         NA
#> 10  2018 February 5+    <NA>        400
#> # … with 53 more rows

units 変数と region 変数には重複するとこがないことに注意されたい.今回の場合はデータを独立した二つの表に分けるのが最も自然だ.

理論

spec の良いところは,pivot_longer()pivot_wider() に同じものを与えられることだ.これにより,二つの操作の対称性が明確になる.

construction %>% 
  pivot_longer(spec = spec) %>% 
  pivot_wider(spec = spec)
#> # A tibble: 9 x 9
#>    Year Month `1 unit` `2 to 4 units` `5 units or mor… Northeast Midwest
#>   <dbl> <chr>    <dbl>          <dbl>            <dbl>     <dbl>   <dbl>
#> 1  2018 Janu…      859             NA              348       114     169
#> 2  2018 Febr…      882             NA              400       138     160
#> 3  2018 March      862             NA              356       150     154
#> 4  2018 April      797             NA              447       144     196
#> 5  2018 May        875             NA              364        90     169
#> 6  2018 June       867             NA              342        76     170
#> 7  2018 July       829             NA              360       108     183
#> 8  2018 Augu…      939             NA              286        90     205
#> 9  2018 Sept…      835             NA              304       117     175
#> # … with 2 more variables: South <dbl>, West <dbl>

整形の仕様を定めることで,pivot_longer(df, spec = spec) がどのように df を整形するかより明瞭で厳密にできる.出力結果は nrow(df) * nrow(spec) に相当する行数と, ncol(df) - nrow(spec) + ncol(spec) - 2 に相当する列数を持つ.


  1. 訳注: tidyverse を vignette で利用すると,最低でも Suggeststidyverse を加えなければならない.しかし tidyverse は多くのパッケージを纏めたメタパッケージなので,不要なパッケージにも依存することになり,パッケージがいたずらに肥大化するのでよくない. (参考: “The tidyverse is for EDA, not packages” https://www.tidyverse.org/articles/2018/06/tidyverse-not-for-packages/)↩︎

  2. 訳注: ptype は prototype の略称.本来のタイプくらいの意味合いだろうか. @yutannihilation 氏によるスライドも参考にして欲しい (https://speakerdeck.com/yutannihilation/tidyr-pivot?slide=51).↩︎

  3. 訳注: 014なら0歳から14歳,1524なら15歳から24歳であることを示す.↩︎

  4. 訳注: 直感的な見出しではないと思う.かと言って良い対案も思いついていない.暫定版は整形対象となる列の値に複数種の変数がある場合,くらいだろうか.↩︎

  5. 訳注: 原文は “you’d prefer to have the individual questions in the columns.” であるが,“questions” ではなく “answers” が正しい気がする.↩︎

  6. 訳注: 原文では元のデータフレームとまで書いていないが伝わらないと思ったので追記した. pivot_wider_spec() の場合は .name 列の値が出力のデータフレームの各列に対応するので注意.↩︎

  7. 訳注: expand %>% mutate よりも pivot_wider_spec %>% mutate の方が,行や列の順序が pivot_wider_spec の結果と整合的で比べやすく,安全だと思う (#666).

    us_rent_income %>%
      pivot_wider_spec(names_from = variable, values_from = c(estimate, moe)) %>%
      mutate(.name = paste0(variable, ifelse(.value == "moe", "_moe", "")))
    #> # A tibble: 4 x 3
    #>   .name      .value   variable
    #>   <chr>      <chr>    <chr>   
    #> 1 income     estimate income  
    #> 2 rent       estimate rent    
    #> 3 income_moe moe      income  
    #> 4 rent_moe   moe      rent
    ↩︎