ホーム | 統計 Top | Tidyverseによるデータフレーム加工(01)tidyr による表の変形
それなりに大きなデータセットの解析を始める時、とりあえずRに放り込んでデータフレームにすることで、可視化や統計処理の各種関数に放り込む準備が整う。問題は誰かから貰った、あるいは自分で入力した表形式のデータが、複雑な構造をしており直ちに解析を始められない場合だ。小さければスプレッドシートの段階で手動修正してもよい。
すごく大きなデータだったり、フォーマットは揃っているが表の枚数が多すぎて手作業で直せないときはどうする? tidyr の出番である。
目次
- Tidyverse 以前:tidy data とデータクリーニング
- データが tidy であるということ
- データフレームの long <-> wide 形式を相互変換する gather と spread
- データフレームの列を集約して long 形式にする tidyr::gather 関数
- データフレームの列の内容を展開して wide 形式にする tidyr::spread 関数
- データ列ラベルを結合する unite と分割する separate / extract
- データ列ラベルを分割する tidyr::separate 関数
- データ列ラベルを高度な正規分割表現に従って分割する tidyr::extract 関数
- データ列ラベルを単純結合する tidyr::unite 関数
- (2020.07.23 追記) long <-> wide 形式を相互変換する新しい関数 pivot_longer と pivot_wider
- データフレームの列を集約して long 形式にする tidyr::pivot_longer 関数
- データフレームの列の内容を展開して wide 形式にする tidyr::pivot_wider 関数
本記事の続編 「Tidyverseによるデータフレーム加工(02)dplyr::*_join による複数データフレームの結合/欠損値補完/ルックアップ」 で、 dplyr::*_join 関数ファミリーを用いて複数の tidy なデータフレームの合意データを作る方法を解説しています。
Tidyverse 以前:tidy data とデータクリーニング
はじめに "tidyverse" とは、R界の木下藤吉郎こと Hadley Wickham と愉快な仲間たちによって構築が進む、Rを拡張する一連のパッケージ群である。データサイエンス界隈で数年前から耳にする "ggplot2" とか "dplyr" とかいったツールは、それぞれ独立したパッケージとして誕生したものであるが、現在は tidyverse における描画やデータフレーム加工等の機能を分担する関数群として理解されるようになっている。そして本ページで取り上げる tidyr は、基本的には1枚のデータフレームとして与えられた表形式データを対象として、行や列の配置を、解析に便利な形へとアレンジするための関数群だ。
Tidyverse の "tidy" は「整然とした」といった意味を持つ英単語である。いったい何が整然としているのか。幸いにも「整然データとは何か」という精緻な日本語解説がある。棒引きすると、tidy data = 整然データとは以下の4つの特徴を備えた表形式データである。
- 個々の変数 (variable) が1つの列 (column) をなす。
- 個々の観測 (observation) が1つの行 (row) をなす。
- 個々の観測の構成単位の類型 (type of observational unit) が1つの表 (table) をなす。
- 個々の値 (value) が1つのセル (cell) をなす。
データが tidy であるということ
実例を示すのが分かり易いだろう。これから用いるデータは、架空の殺虫剤試験において、Site 1から6までのほ場に薬剤処理区(Treat)と対照区(Control)を設置し、薬剤処理の前(Before)と後(After)の各時期、一定面積の区画にいる害虫数を数えたものである。数値自体はポアソン分布に従う乱数としてでっちあげた。なおこの最初の表は tidy ではないので注意。
August | September | |||||||
---|---|---|---|---|---|---|---|---|
Site | Treated | Control | Treated | Control | ||||
Before | After | Before | After | Before | After | Before | After | |
1 | 6 | 4 | 4 | 1 | 1 | 1 | 2 | 3 |
2 | 2 | 6 | 5 | 0 | 5 | 2 | 1 | 3 |
3 | 4 | 1 | 5 | 0 | 3 | 0 | 4 | 3 |
4 | 1 | 6 | 3 | 0 | 2 | 0 | 1 | 5 |
5 | 2 | 5 | 4 | 0 | 0 | 2 | 2 | 7 |
6 | 2 | NA | 8 | 1 | 5 | 1 | 4 | 5 |
たぶん、この入力方法でも「かなり上等」な部類であろう。大きな声では言えないが、実際の薬剤試験の報告書はもっと雑然としたテーブルを含んでいる。さて上記の表、このままでは R にインポートすらできない。ヘッダー部分のセルが結合されているためである。とりあえず手作業で修正してやったものが以下の tidypest01.csv データだ。
# だいぶマシになったが、厳密にはまだ tidy ではない R データフレームの例
tidypest.raw <- read.table("tidypest01.csv", header=TRUE, sep="," , as.is=TRUE ) # as.is
> tidypest.raw
Season Site Treat.Before Treat.After Control.Before Control.After
1 August 1 6 4 4 1
2 August 2 2 6 5 0
3 August 3 4 1 5 0
4 August 4 1 6 3 0
5 August 5 2 5 4 0
6 August 6 2 NA 8 1
7 September 1 1 1 2 3
8 September 2 5 2 1 3
9 September 3 3 0 4 3
10 September 4 2 0 1 5
11 September 5 0 2 2 7
12 September 6 5 1 4 5
手動での主な修正点として、調査シーズン(August or September)の各列を横に並べるのではなく、同じ列内の別行に入るように格納し直した。またヘッダーが複数行になるとコンピュータでの扱いが難しいため、"Treat.Before" などとして「試験処理」と「時間」の列ラベルを繋いである(なおコンマ,やハイフン-で分けるとRへの取り込み時に列ラベルの分割点として認識されてしまうので、ピリオド.かアンダーバー_を使おう)。
このデータ tidypest.raw は Hadley が言うところの "tidy" の条件を、部分的には満たしている。ただし、"Treat.Before" などは「2つの説明変数(要因)を無理やり1つの列名として繋げた」ものであり、この状況は、最初に述べた tidy data の1番目の要件から外れる。つまり完全な tidy data ではない。
データフレームの long <-> wide 形式を相互変換する gather と spread
さて R 環境に取り込めたはいいものの、tidypest.raw の表形式のままではデータの表示や加工が容易でない。まずはこれを、以下のような縦長の表構造に変形することを目指す。
Season Site TempKey1 TempValue
1 August 1 Treat.Before 6
2 August 2 Treat.Before 2
3 August 3 Treat.Before 4
4 August 4 Treat.Before 1
5 August 5 Treat.Before 2
6 August 6 Treat.Before 2
7 September 1 Treat.Before 1
8 September 2 Treat.Before 5
9 September 3 Treat.Before 3
10 September 4 Treat.Before 2
11 September 5 Treat.Before 0
12 September 6 Treat.Before 5
13 August 1 Treat.After 4
14 August 2 Treat.After 6
15 August 3 Treat.After 1
16 August 4 Treat.After 6
17 August 5 Treat.After 5
18 August 6 Treat.After NA
19 September 1 Treat.After 1
20 September 2 Treat.After 2
21 September 3 Treat.After 0
22 September 4 Treat.After 0
23 September 5 Treat.After 2
24 September 6 Treat.After 1
25 August 1 Control.Before 4
26 August 2 Control.Before 5
27 August 3 Control.Before 5
28 August 4 Control.Before 3
29 August 5 Control.Before 4
30 August 6 Control.Before 8
31 September 1 Control.Before 2
32 September 2 Control.Before 1
33 September 3 Control.Before 4
34 September 4 Control.Before 1
35 September 5 Control.Before 2
36 September 6 Control.Before 4
37 August 1 Control.After 1
38 August 2 Control.After 0
39 August 3 Control.After 0
40 August 4 Control.After 0
41 August 5 Control.After 0
42 August 6 Control.After 1
43 September 1 Control.After 3
44 September 2 Control.After 3
45 September 3 Control.After 3
46 September 4 Control.After 5
47 September 5 Control.After 7
48 September 6 Control.After 5
上記 tidypest.raw.long1$TempValue のように、観測データ(observation: 統計モデルに当てはめるならば目的変数に該当)が単一の列として格納される表構造を、データサイエンス界隈では long 形式と呼ぶ。これに対して tidypest.raw の 列 c("Treat.Before", "Treat.After", "Control.Before", "Control.After") のように、観測データが複数の列に分けて記載されているような表構造を、wide 形式という。ここでようやく、tidyverseパッケージの出番となる。
データフレームの列を集約して long 形式にする tidyr::gather 関数
# 上記の変換を行うコード
# install.packages('tidyverse', dependencies=TRUE) # 初回ならば要インストール
library(tidyverse) # load the package of "tidyverse"
# wide -> long には tidyverse:gather を使う。
tidypest.raw.long1 <- tidypest.raw %>%
tidyr::gather( key=TempKey1, value=TempValue,
Treat.Before, Treat.After, Control.Before, Control.After,
na.rm=FALSE, convert=FALSE, factor_key=FALSE )
tidypest.raw.long1
上記処理により、tidypest.raw に存在していた Treat.Before, Treat.After, Control.Before, Control.After という4列分のデータの中身が、新しい列 "TempValue" として一繋がりに格納される。それだけでは元々存在していた4つのうち、どの列から取り出されたデータなのかが判らないため、"TempKey1" という列を新たに作り、出自に相当する列の名前を文字列情報として格納している。
gather(data, key, value, ..., na.rm = FALSE, convert = FALSE, factor_key = FALSE)
省略不可な引数
data 処理対象のデータフレーム。
key 変換後のデータフレームに新規生成すべき key 列(=カテゴリのこと)の名前を指定。引用符で囲む必要はない。
value 変換後のデータフレームに新規生成すべき value 列(=データの値)の名前を指定。引用符で囲む必要はない。
... 変換前のデータフレームにある、Long形式にまとめてしまいたい列の名を(引用符なしで)1つ以上指定。
変換後のデータフレームにおいて、指定された列の列名が key 列に格納され、データが value 列に格納される。
変換前のデータフレームにある x から z までの間の全列を含めたい場合は x:z などとする。
列 y を除外したければ -y などとする。さらなる詳細は dplyr::select() のドキュメントを参照。
オプション扱いの引数
na.rm TRUE であるとき、value列が NA となるデータ行がもしあれば削除される。
convert TRUEであるとき、 key 列に含まれるデータを自動で型変換する。元の列名が numeric, integer, or logical であるときに便利。
factor_key デフォルトはFALSEで、key列の値は文字列ベクトルで格納される。TRUEであるとき、factor型に変換される。このとき元のデータフレームにおける列の順序が保存される。
ちなみに tidyr::gather という記法だが、「tidyr」というパッケージに含まれる「gather」という名前の関数を名指しで呼び出している。もしRの他のパッケージに、たまたまgatherという名前の関数があっても混同しないで済む。詳しくは「名前空間」で検索検索。
パイプ演算子 %>% のこと
なお上記の操作、通常のRでの演算と比べて癖があるので注意。最たるものは %>% という謎の演算子。そしてgather() 関数の第一引数に必須なはずの data が使われていない?この %>% は「パイプ演算子」といって、現在は tidyverse に統合済みの magrittr というパッケージにて提供されている(なおパイプ自体はR言語に限った機能ではない)。簡単にいうと、パイプ演算子の左辺にある tidypest.raw というデータを、右辺こと gather() 関数の第一引数 data に代入して実行しませい、という指示を出す。さらに <- 演算子が控えているから、gather() の実行結果は最終的には左辺の tidypest.raw.long1 という新しいオブジェクトに格納される。
蛇足:Rの計算処理は別にパイプ演算子を使わなくても書ける。というか処理関数が一つだけなら全くメリットはない。が、通常の代入 <- 演算子は必ず、左辺のオブジェクトに計算結果のデータを書き込む。これだと単一のデータに複数の関数を直列で適用し、最終結果だけを保存したい(=パイプライン処理)場合、メモリを無駄に消費してしまう。関数をネストする書き方(hoge <- exp(sum(c(1,2,3))) とか)でも対処できるが、階層が深くなるとコードの可読性が著しく下がり好ましくない。
データフレームの列の内容を展開して wide 形式にする tidyr::spread 関数
「列を集約」して long にする処理が tidyr::gather であった。その逆として tidyr::spread で「列の内容を展開して」wide に変形できると覚えておこう。
# 上記の逆変換を行うコード
# long -> wide には tidyverse::spread を使う。
tidypest.raw.inv <- tidypest.raw.long1 %>%
tidyr::spread( key=TempKey1, value=TempValue, fill="あらへん。" )
tidypest.raw.inv
spread(data, key, value, fill = NA, convert = FALSE, drop = TRUE, sep = NULL)
省略不可な引数
data データフレーム。
key 分類のキーとして用いる。元データに含まれる列を名前で指定。引用符で囲む必要はない。ここに格納されている値の水準数だけ、新しいデータ列が作られ、そこにvalueで指定したデータが格納される。
value 上記で分類したセルをpopulateするためのvalue。元データに含まれる列を名前で指定。引用符で囲む必要はない。
オプションの引数
fill 変換後のカテゴリに対応するデータ行が存在しないとき、ここに指定した値で置き換える。なお元データの該当行にNAが入っている場合も、ここで指定した値に置換される。
convert TRUEであるとき、分類の結果生成された各列に対して、type.convert() 関数が as.is=TRUE として実行される。元の列に含まれている各変数が、複数のデータ型を文字列にまとめたものである場合にとりわけ有用。もし元のクラスがfactorないしdateであった場合、新たに生成される列は自動では同じ型として保存されないので、いったんcharacterにまとめてから変換する必要がある。
drop FALSEであるとき、新しく生成されるfactor型のデータ列において存在しない水準は削除される。
sep NULLであるとき、新たに生成される列の名前を key 列に格納されている変数の値そのものとして生成する。NULL以外であれば、<key_name><sep><key_value> という命名規則で作られる。
上記処理により、data=tidypest.raw.long1 に存在していた value=TempValue という列のデータの中身が、key=TempKey1 のユニークな水準別に、新しい列を作って振り分けられる。
> tidypest.raw.inv
Season Site Control.After Control.Before Treat.After Treat.Before
1 August 1 1 4 4 6
2 August 2 0 5 6 2
3 August 3 0 5 1 4
4 August 4 0 3 6 1
5 August 5 0 4 5 2
6 August 6 1 8 あらへん。 2
7 September 1 3 2 1 1
8 September 2 3 1 2 5
9 September 3 3 4 0 3
10 September 4 5 1 0 2
11 September 5 7 2 2 0
12 September 6 5 4 1 5
spread() で新しく作られる列の名前は、デフォルトでは key 列の各水準がそのまま使われ、変換前の key の列名であった "TempKey1" は新規列名に盛り込まれない。変換後のデータフレームにおいて列名に統一性を持たせたければ、sep="." などとすると、生成される列名を "TempKey1.Control.After" などとできる。
データ列ラベルを結合する unite と分割する separate / extract
さて、上記の long <-> wide 変換が自在に出来るようになっただけでも随分な進歩なのだが、データラベルが「試験処理+時間」という 2-factor 構造を持つ欠陥は、tidypest.raw.long1 でもまだ解消されていない。なのでまだ真の tidy data とは呼べない。ここで tidypest.raw.long1$TempKey1 は Treat.Before みたく、"Treatment"+"."+"Time" という構造をもつ文字列である。つまり、ピリオドの前までを「処理」、ピリオドよりも後を「時間」として別々のデータ列に切り出すことができれば、所定の目標を達せられそうだ。
データ列ラベルを分割する tidyr::separate 関数
# データ列の分解には tidyr:separate を使うのが楽。
library(tidyverse) # load the package of "tidyverse"
tidypest.long <- tidypest.raw.long1 %>%
tidyr::separate( col=TempKey1, into=c("Treatment", "Time"), sep='[^[:alnum:]]',
remove=TRUE, convert=FALSE, extra='warn', fill='warn' )
tidypest.long
# tidypest.raw からの一連の処理をパイプライン化すると
tidypest.long <- tidypest.raw %>%
tidyr::gather( key=TempKey1, value=TempValue,
Treat.Before, Treat.After, Control.Before, Control.After,
na.rm=FALSE, convert=FALSE, factor_key=FALSE ) %>%
tidyr::separate( col=TempKey1, into=c("Treatment", "Time"), sep='[^[:alnum:]]',
remove=TRUE, convert=FALSE, extra='warn', fill='warn' )
tidypest.long
この段階で、視覚化も統計モデルへの当てはめもドンと来いの tidy data を得る。
> tidypest.long
Season Site Treatment Time TempValue
1 August 1 Treat Before 6
2 August 2 Treat Before 2
3 August 3 Treat Before 4
4 August 4 Treat Before 1
5 August 5 Treat Before 2
6 August 6 Treat Before 2
7 September 1 Treat Before 1
8 September 2 Treat Before 5
9 September 3 Treat Before 3
10 September 4 Treat Before 2
(以下省略)
separate( data, col, into, sep = "[^[:alnum:]]+", remove = TRUE,
convert = FALSE, extra = "warn", fill = "warn", ...)
引数
data データフレーム。
col 分割対象とするデータ列の名前、もしくはアドレス(何番目の列かを表す数値)。tidyselect::vars_pull() に渡される。引用符で囲む必要はない。
into 新しく作られる変数(複数ある)の名前を文字列ベクトルで指定。
sep データ列の分割場所となる文字(セパレータ)。分割規則を文字列で与えるか、何番目の文字までを切り取るかを整数ベクトルとして指定する。
文字列で与えた場合は正規表現として解釈される。デフォルトの "[^[:alnum:]]+" はアルファベットと半角数字以外、全ての文字で列を分割しようとする。
数値(須藤注:整数ベクトル)で与えた場合は、正数であれば左から数えて(負数であれば右から)n 字までを切り取り、n+1字目から次の列が始まるよう分割される。sep 引数のベクトル要素は into の要素数より1つだけ少なく与えられるべきものである。
オプションの引数
remove デフォルトは TRUE で、分割対象になった元のデータ列は関数の出力ファイルから取り除かれる。
convert デフォルトはFALSEだが、もしも TRUE ならば、変換生成後のデータ列に type.convert() を as.is = TRUE として掛ける。元のデータが integer, numeric or logical であるときに有用。
extra If sep is a character vector, this controls what happens when there are too many pieces. There are three valid options:
"warn" (the default): emit a warning and drop extra values.
"drop": drop any extra values without a warning.
"merge": only splits at most length(into) times
fill If sep is a character vector, this controls what happens when there are not enough pieces. There are three valid options:
"warn" (the default): emit a warning and fill from the right
"right": fill with missing values on the right
"left": fill with missing values on the left
... Additional arguments passed on to methods.
関数 spread と separate はどちらもデータ列を「分ける」処理なので紛らわしいが、得られる結果は全く異なる。spread は key 列が保持するデータを層別し、その水準の数だけ独立した列を作る。作られた列に入るデータ値は、引数 value で指定されたものである。つまり key は字義通り、分類のキーとして扱われている。
separate は対象の col 列が含む各データ要素を文字列として解釈し、その文字列を一定の規則 sep に基づいて分割する。分割された文字列が、引数 into に基づいて新設された、データ列たちの値として割り振られる。
引数 sep に数値を与えた場合の挙動がわかりにくいので実験しておこう。
hoge <- tidypest.raw.long1 %>%
tidyr::separate( col=TempKey1, into=c("Treatment", "Time", "Hoge", "Fuge"), sep=c(2, 10, 1),
remove=TRUE, convert=FALSE, extra='warn', fill='warn' )
> hoge
Season Site Treatment Time Hoge Fuge TempValue
1 August 1 Tr eat.Befo reat.Before 6
2 August 2 Tr eat.Befo reat.Before 2
3 August 3 Tr eat.Befo reat.Before 4
4 August 4 Tr eat.Befo reat.Before 1
5 August 5 Tr eat.Befo reat.Before 2
(以下省略)
hoge <- tidypest.raw.long1 %>%
tidyr::separate( col=TempKey1, into=c("Treatment", "Time", "Hoge", "Fuge"), sep=c(2, -2, -1),
remove=TRUE, convert=FALSE, extra='warn', fill='warn' )
> hoge
Season Site Treatment Time Hoge Fuge TempValue
1 August 1 Tr eat.Befo r e 6
2 August 2 Tr eat.Befo r e 2
3 August 3 Tr eat.Befo r e 4
4 August 4 Tr eat.Befo r e 1
5 August 5 Tr eat.Befo r e 2
(以下省略)
要するに、sep の i 番目の要素 s_i が正の数であれば、i 番目の要素は常に列ラベルの冒頭からの s_i 字を切り取ろうとする。この際、たとえば sep[1]==2 かつ sep[2]==2 だと、2番目の要素は自分が切り取った2文字をお兄ちゃんに持って行かれてしまい、取り分ゼロである。一方、sep の最後尾の要素は、独立して高い優先度を持つようだ。sep=c(2, 10, 1) とした最初の例では、最後尾の 1 により「1字目を除いた全部」が Fuge に与えられている。哀れ Hoge は何も貰えない。
データ列ラベルを高度な正規分割表現に従って分割する tidyr::extract 関数
データ列ラベルを分割する別の関数が extract である。separate の sep 引数を、regex すなわち正規表現に置き換えたものになる。
# 列ラベルの分割は、以下の関数 tidyr::extract でも可能。
tidypest.long <- tidypest.raw.long1 %>%
tidyr::extract( col=TempKey1, into=c("Treatment", "Time"),
regex="([[:alpha:]]+)\\.([[:alpha:]]+)",
remove=TRUE, convert=FALSE )
tidypest.long
# 以下のように書いても良い。
tidypest.long <- tidypest.raw.long1 %>%
tidyr::extract( col=TempKey1, into=c("Treatment", "Time"),
regex="([[:alpha:]]+)[[:punct:]]([[:alpha:]]+)",
remove=TRUE, convert=FALSE )
tidypest.long
正規表現は「RjpWiki: R における正規表現」 のページを参照のこと。[[:alpha:]]+ は「アルファベット(小文字および大文字)が1回以上続くパターン」にマッチし、 \\. はピリオド . という文字にマッチする。パターンマッチの中でエスケープさせるため、バックスラッシュを2回打つ必要がある。代替表記の [[:punct:]] は「パンクチュエーション文字 ! " # $ % & ' ( ) * + , - . /」にマッチする。。
extract(data, col, into, regex = "([[:alnum:]]+)", remove = TRUE, convert = FALSE, ...)
引数
data データフレーム。
col 分割対象とするデータ列の名前もしくは場所(何番目の列か)。tidyselect::vars_pull() に渡される。引用符で囲む必要はない。
into 新しく作られる変数(複数ある)の名前を文字列ベクトルで指定。
regex 望ましい名前ラベルを抽出するための正規表現を1つ指定する。この正規表現に含まれるグループ(正規表現では () で囲むことで定義される)が、引数 into に入れた各要素と一対一対応する必要がある。
オプションの引数
remove デフォルトは TRUE で、分割対象になった元のデータ列は関数の出力ファイルに含まれない。
convert デフォルトはFALSE。もしも TRUE ならば、変換生成後のデータ列に type.convert() を as.is = TRUE として掛ける。元のデータが integer, numeric or logical であるときに有用。
... 正規表現を処理するため、regexec() に渡したい追加の引数があれば。
separate と extract はどちらを使っても似たような仕事ができるが、データの分割位置が行ごとにバラバラで、単純な固定長での分割が難しい場合には、extract で書くほうが楽かもしれない。
データ列ラベルを単純結合する tidyr::unite 関数
列ラベルの分割には separate や extract が使えるように、unite 関数でその逆、つまりデータフレームに含まれる複数の列を単純結合できる。
# データ列の結合には tidyr:unite を使う。
library(tidyverse) # load the package of "tidyverse"
tidypest.long.2way <- tidypest.long %>%
tidyr::unite( col=TempKey2, Treatment, Time, sep="_", remove=TRUE )
tidypest.long.2way
> tidypest.long.2way
Season Site TempKey2 TempValue
1 August 1 Treat_Before 6
2 August 2 Treat_Before 2
3 August 3 Treat_Before 4
4 August 4 Treat_Before 1
5 August 5 Treat_Before 2
(以下省略)
unite(data, col, ..., sep = "_", remove = TRUE)
引数
data データフレーム。
col 結合して作られる新たな列名。文字列ないしシンボルで与える。"" で囲んでも囲まなくてもいい。rlang::quo_name() の規則に従って渡されるが、実際のオブジェクトに対応しないシンボル名での指定は現在の tidyverse では非推奨であり、後方互換性のためだけに残されている。
... 結合対象の列を指定。何も指定しない場合、全データ列を選択。列名は "" で囲んでも囲まなくてもいい。列 x と列 z の間にある全てを選びたければ x:z としてよい。-y とすれば列 y を除外する。さらに詳細は dplyr::select() のドキュメントを参照。
sep 結合したデータの間に入るセパレータ文字列。デフォルトは "_" で、たとえば "Control_Before" などとなる。
remove デフォルトは TRUE で、結合対象になった元のデータ列は関数の出力ファイルに含まれない。
では、今回はここまで。
# いったん、ここまでのデータを保存する。
today <- "180209"
save( list=ls(), file=paste("tidy", today, "dat", sep=".") ) # ワークスペースをバイナリ形式で
# よみこみ
today <- "180209"
load( file=paste("tidy", today, "dat", sep=".") )
# csvで保存
write_csv( tidypest.long, path=paste("tidypest", "long", "csv", sep="."))
(2020.07.23 追記) long <-> wide 形式を相互変換する新しい関数 pivot_longer と pivot_wider
最初にこの記事を書いた2018年8月の時点で、tibble の形式変換といえば gather/spread だった。その後 2019年9月にtidyr version 1.0 が登場し、幾つかの抜本的な変更がなされた。特に大きな変化が pivot_longer および pivot_wider への移行である(他の大きな変更は nest() 系である。既に執筆した続編は新しい tidyr の仕様に準拠している)。
データフレームの列を集約して long 形式にする tidyr::pivot_longer 関数
従来の gather および gather+separate に匹敵する機能を、一貫性のある表記で実現する新しい関数が pivot_wider() である。
# tidyr::gather に代えて tidyr::pivot_longer で wide -> long の変換(および列の分割)
library(tidyverse) # load the package of "tidyverse"
tidypest.long <- tidypest.raw %>%
tidyr::pivot_longer(cols=c("Treat.Before", "Treat.After", "Control.Before", "Control.After"),
names_to=c("Treatment", "Time"), names_sep="¥¥.", values_to="Value")
tidypest.long
# A tibble: 48 x 5
Season Site Treatment Time Value
<chr> <int> <chr> <chr> <int>
1 August 1 Treat Before 6
2 August 1 Treat After 4
3 August 1 Control Before 4
4 August 1 Control After 1
5 August 2 Treat Before 2
6 August 2 Treat After 6
7 August 2 Control Before 5
8 August 2 Control After 0
9 August 3 Treat Before 4
10 August 3 Treat After 1
# ... with 38 more rows
# 一度に分割しない方法も勿論可能である
tidypest.raw.long1 <- tidypest.raw %>%
tidyr::pivot_longer(cols=c("Treat.Before", "Treat.After", "Control.Before", "Control.After"),
names_to="TempKey1", values_to="TempValue")
tidypest.raw.long1
上記コードでは、names_to 引数に2つの新規列名を与え、かつ分割文字を names_sep 引数で与えることにより、"Treat.Before" といった「処理.時期」形式で格納されているデータの中身を、一度の操作で「処理の列」+「時期の列」に分割しつつ long への形式を行っている。
データフレームの列を集約して long 形式にする tidyr::pivot_longer 関数
https://tidyr.tidyverse.org/reference/pivot_longer.html
pivot_longer( data, cols, names_to = "name", names_prefix = NULL, names_sep = NULL,
names_pattern = NULL, names_ptypes = list(), names_transform = list(),
names_repair = "check_unique",
values_to = "value", values_drop_na = FALSE, values_ptypes = list(),
values_transform = list(), ... )
data 処理対象のデータフレーム。
cols longer 形式に変換したい列を <tidy-select> 方式で選択。Tidy-select って何さと思うが、要するに文字列ベクトルで直接指定してもいいし、starts_with("Treat") みたいに自動選択する企みをしてもいい。
####
names_to 上記で指定した cols の列名をデータとして取り込むべく、新規作成する列(ネーム列)に付ける列名。文字列で指定。
ネーム列を複数作りたい時は、names_to に複数の文字列をベクトルで与える。この場合は、cols の列名をどうやって複数の文字列に分割するかを指定するために names_sep もしくは names_pattern が必要。複数列に分割する場合、特別な値の指定方法が2通り存在する。一つは NA(クォーテーション付けない)で 、このとき当該列は作成されず、闇に消える。例えば以下のような使い方をする。
tidypest.raw %>%
tidyr::pivot_longer(cols=c("Treat.Before", "Treat.After", "Control.Before", "Control.After"),
names_to=c(NA, "Time"), names_sep="¥¥.", values_to="Value")
もう一つは ".value" である。部分的に long 変換したい場合に使える。説明めんどくさいので以下の例を見てほしい。
tidypest.raw %>%
tidyr::pivot_longer(cols=c("Treat.Before", "Treat.After", "Control.Before", "Control.After"),
names_to=c("Treatment", ".value"), names_sep="¥¥.")
# A tibble: 24 x 5
Season Site Treatment Before After
1 August 1 Treat 6 4
2 August 1 Control 4 1
3 August 2 Treat 2 6
4 August 2 Control 5 0
5 August 3 Treat 4 1
6 August 3 Control 5 0
7 August 4 Treat 1 6
8 August 4 Control 3 0
9 August 5 Treat 2 5
10 August 5 Control 4 0
# ... with 14 more rows
つまり names_to=c("Treatment", ".value") とすることで、前半の "Treatment" については伸ばしつつ、後半の Before/After については列構造を残せる。このとき、残る 2 列分の列名は不要であるから、もしも values_to があれば無視される。
####
names_prefix 作成されるネーム列において、cols に指定した各列の列名冒頭から除去したい文字があれば、正規表現で指定。挙動は以下のようになる。
tidypest.raw %>%
tidyr::pivot_longer(cols=c("Treat.Before", "Treat.After", "Control.Before", "Control.After"),
names_to=c("Treatment", "Time"), names_sep="¥¥.", names_prefix="..")
# A tibble: 48 x 5
Season Site Treatment Time value
1 August 1 eat Before 6
2 August 1 eat After 4
3 August 1 ntrol Before 4
4 August 1 ntrol After 1
5 August 2 eat Before 2
6 August 2 eat After 6
7 August 2 ntrol Before 5
8 August 2 ntrol After 0
9 August 3 eat Before 4
10 August 3 eat After 1
# ... with 38 more rows
これだけだと使い処が分からないだろうが、たとえば col の各列が "n_Treat.Before", "n_Treat.After", "n_Control.Before", "n_Control.After" 等となっていた場合に 冒頭の n_ を取り除く処理が可能である。
####
以下はそこそこ使用するオプション引数。
names_sep, names_pattern これらの引数は names_to が複数の値を含んでいた場合に使われ、col で指定した各列の中身を、どのようにネーム列の値へ分割するかを指定する。
names_sep は従来の separate() と同様の指定方法をサポートする。すなわち数値ベクトルで分割位置を指定するか、単一の正規表現文字列で分割文字を指定する。
names_pattern は従来の extract() と同様の指定方法をサポートする。すなわちマッチンググループを含む正規表現で指定する。
すごく複雑なパターンを処理する必要があり上2つで上手く行かない場合は、pivot_longer_spec() にて spec object を作る方法で手動制御する。
values_to 単一の文字列。col に指定した列たちの列名が、新規作成したネーム列(names_to で名前を指定する)の中身へまとめられるのに対して、col に指定した列たちのデータを格納するために新規作成されるデータ列の名前を指定するのが values_to である。
####
以下は、あまり使用機会がないオプションの引数。
names_ptypes, values_ptypes それぞれ新規作成されるネーム列、バリュー列の prototype をリストに入れて指定する。プロトタイプ(略して p type)とは、integer() とか numeric() といった長さ0のベクトルであり、あるベクトルのデータ形式、クラス、および属性を定義する。
基本的には、作成する列のデータ形式を指定するために使う。無指定の場合は names_to で作成する列のデータ形式は文字列になり、values_to で作成される列のデータ形式は、input columns のデータを表現可能な共通の形式が選ばれる(須藤注:たとえば double と integer を統合するならば double が共通形式になるし、数値と文字列を同じ列に押し込めるには、数値 1 も文字列 "1" として扱う必要がある)。
# 公式にも使用例がないが、以下のようなことができる。
tidypest.raw %>%
tidyr::pivot_longer(cols=c("Treat.Before", "Treat.After", "Control.Before", "Control.After"),
names_to=c("Treatment", "Time"), names_sep="¥¥.",
names_ptypes=list(Treatment=factor()))
# この場合、 Treatment 列が因子型になる。
names_transform, values_transform (須藤注:tidyr 1.1.0 以降でないと動かないっぽい)それぞれ新規作成されるネーム列、バリュー列の、データ形式を変換する関数をリストで指定する。指定した名前を持つ列について、データ形式を変換したい場合に使う。たとえば names_transform = list(week = as.integer) とすると、新規作成する week という名前の列の中身が整数形式に変換される。
# こちらも公式に使用例がないが、以下のコードで Treatment が因子型になる。
tidypest.raw %>%
tidyr::pivot_longer(cols=c("Treat.Before", "Treat.After", "Control.Before", "Control.After"),
names_to=c("Treatment", "Time"), names_sep="¥¥.",
names_transform=list(Treatment=as.factor))
names_repair 新規作成するネーム列が複数ある場合の列間での名前の重複や、既存列との名前の重複があった場合の回避処理。デフォルトは "check_unique" でエラーを返す。"minimal" は重複を許容する。"unique" は重複列の後に .1 とか .2 とか接尾辞を付加して重複を避ける。さらなるオプションは vctrs::vec_as_names() にある。
values_drop_na デフォルトは FALSE。TRUE であった場合、もしも新規作成列の全行が NA になるような列があれば削除される。
... 追加の引数があれば。
なお、上の解説で「ネーム列」とか「バリュー列」とか言っているのは、須藤の勝手な定義である。tidyr の公式ヘルプに概念が示されていないせいで説明しにくいのだ。たぶんちゃんと探せば誰かが定義しているのだろうけど。
データフレームの列の内容を展開して wide 形式にする tidyr::pivot_wider 関数
列の内容を展開して wide に変形する tidyr::spread の改良版が pivot_wider である。名前が対になっていることからも分かるように、pivot_longer のオプション指定方法との一貫が図られ、かなり使いやすくなっている。一応 spread も将来バージョンで削除はされないが、これ以上の機能拡充はされない。
# 上記の逆変換を行うコード
# long -> wide には tidyverse::pivot_wider を使う。
tidypest.raw.inv <- tidypest.long %>%
tidyr::pivot_wider( names_from=c("Treatment", "Time"), names_sep=".",
values_from="Value")
tidypest.raw.inv
個人的には上記の(パイプ処理の影に隠れている data および) names_from, names_sep, values_from あたりの引数を覚えておけば、殆どの事例に対応できると思う。高度なオプションについても一応以下で解説しておく。
データフレームの列の内容を展開して wide 形式にする tidyr::pivot_wider 関数
https://tidyr.tidyverse.org/reference/pivot_wider.html
pivot_wider(data, id_cols = NULL, names_from = name, names_prefix = "", names_sep = "_",
names_glue = NULL, names_sort = FALSE, names_repair = "check_unique",
values_from = value, values_fill = NULL, values_fn = NULL, ...)
data 処理対象のデータフレーム。
names_from, values_from それぞれ <tidy-select> 形式で列を指定する。前者は、出力列(output column)の列名を生成する際の、元ネタとして用いる列(1つでも複数でもいい)を指定する。後者は、出力列のデータ内容になるべき列(これまた複数でもよい)を指定する。
もしも values_from で複数の列を指定したならば、values_from の値が出力列の前に付加される。何が起こるかは以下の例を見てほしい。
hoge <- tidypest.long %>%
tidyr::pivot_wider( names_from="Treatment", values_from=c("Time", "Value"))
hoge[1, "Value_Control"][[1]]
# A tibble: 12 x 6
Season Site Time_Treat Time_Control Value_Treat Value_Control
<chr> <int> <list> <list> <list> <list>
1 August 1 <chr [2]> <chr [2]> <int [2]> <int [2]>
2 August 2 <chr [2]> <chr [2]> <int [2]> <int [2]>
以下省略
> hoge[1, "Value_Control"][[1]]
[[1]]
[1] 4 1
うん、わからん。
####
以下はそこそこ使用するオプション引数。
names_prefix 展開後に作成される出力列の列名の冒頭に、付けたい文字列があれば。よく使われるシチュエーションは、names_from の中身がいずれも数値である場合。
names_sep もしも names_from で複数の列を指定した場合、それらの列名を結合して新規列を命名するための繋ぎの文字を指定する。須藤注:pivot_longer の names_sep は正規表現だったが、pivot_wider では単なる文字列。たとえば "¥¥." とする必要はなく "." で、 Treat.Before みたいにピリオド1つの結合になる。
####
以下は、あまり使用機会がないオプションの引数。
id_cols <tidy-select> 形式で列を指定する。データセットの各列を実験計画上の要因として捉えるとき、id_cols で指定された列セットについて、各行がユニークな水準の組み合わせに対応していることを示す。デフォルトでは names_from および values_from に使われなかった全ての列が、自動的に選択される。
須藤注:公式ヘルプが難解だが実際何が起こるかというと、id_cols, names_from, values_from のいずれにも指定されなかった列は、pivot_wider の際に展開されずに残る。pivot_longer の cols 引数とは異なることに注意。これを変更すべきシチュエーションは多くない。以下を実行すると分かるのだが、
tidypest.long %>%
tidyr::pivot_wider(id_cols="Season", names_from=c("Treatment", "Time"), values_from="Value")
展開時に取り残された列は tibble の出力列内に入れ子構造として残る。上の例だと Site の 1:6 に相当するデータが、tibble の各セルに長さ6のベクトルとして格納される。Nested tibble の構造を知っている人なら扱えるが、初心者にはおすすめできない。
names_glue 列名の生成に names_sep や names_prefix を使う代わりに、生成規則を指定する glue specification を記述できる。たとえば以下
tidypest.long %>%
tidyr::pivot_wider( names_from=c("Treatment", "Time"),
names_glue="{Treatment}_{Time}.{.value}", values_from="Value")
は、 Treat_Before.Value Treat_After.Value みたいな列名を生成する。
なお、さらに複雑な生成規則が必要な場合は pivot_wider_spec() という関数も用意されている。
names_sort names_from に含まれるユニークな水準から列名を生成する際に、ソートするか?デフォルトは FALSE で、たとえば tidypest.long$Treatment 列における出現順が Treat → Control であれば、展開後の列も左から Treat → Control の順になる。
names_repair 生成される列名が無効である場合の処理。デフォルトは "check_unique" で、既存列名と被ったり、新規に2つ同じ名前の列が生成されてしまう場合はエラーを出す。列名被りを許容する場合は "minimal"、 .1 や .2 などと接尾辞を付けて強引に名前被りを回避したい場合は "unique" を指定する。さらなるオプションは vctrs::vec_as_names() にある。
values_fill 欠損値を埋める文字列(スカラー)をオプションで指定できる。なおvalues_from を複数指定した場合、 named list で個別に指定することも可能。
values_fn pivot_wider の操作結果が格納される各セルに、何らかの関数を適用するオプション。何でも id_cols と values_from に指定した列のコンビネーションが、ユニークな観察水準に対応しない場合に使うと良いらしい。
実際に公式ヘルプのページの最後に掲載されている例だと、 values_fn = mean を指定している。上にも書いたように id_cols に取りこぼしがあると、展開後の生成列の各セルが複数データ値のベクトルになってしまうのだが、そこにダメ押しで mean を適用することで、平均値という1つの値にまとめて(ネストされない平板な tibble で)出力できるという寸法だ。こちらも values_fill と同様、values_from の各列に対し、named list で個別に指定可能である。
... 追加の引数があれば。