須通り
Sudo Masaaki official site
For the reinstatement of
population ecology.

おまえが長くtidyverseを覗くならば、tidyverseもまた等しくおまえを見返すのだ。

ホーム | 統計 Top | Tidyverseによるデータフレーム加工(02)dplyr::*_join による複数データフレームの結合/欠損値補完/ルックアップ

dplyrHadley Wickham や、Rcpp の共同開発者でもある Romain Francois らによって開発されたデータフレームの高速変形ツールで、現在は tidyverse に統合されている。本記事の前編 「Tidyverseによるデータフレーム加工(01)tidyr による表の変形」 において、データフレームの一貫した変形手段を提供する tidyr パッケージ、とりわけその中核機能である gather, spread, unite, separate, extract 各関数について詳細な使用法を解説した。これら tidyr の関数群が表の形状を変形するのに対して、データフレーム中のデータそのものを書き換えたり、行ごとの一括処理を行ってその結果を取り出したりする機能が、主として dplyr パッケージで提供されている。

最も有名な dplyr の中核機能である、単一データフレームに基づく一括データ処理の手順については、スペースの都合から次の記事で扱う。dplyr の一部として提供されているもう一つの大きな機能が、複数のデータフレームを結合するための関数群 *_join() ファミリーである。こちらを本稿では解説する。

さらに多少変則的な扱いになるが、複数要因の試験において変数の組み合わせに欠損があり、一部の水準の組み合わせに対応する行がデータフレーム中に存在しない場合を考える。これを明示的に欠損データ(NA)として扱うために、データフレームの行を補完して全水準の組み合わせを含むようにする処理がある。これは tidyr の機能である complete() 関数、もしくは tidyr の expand() 関数と dplyr の left_join() 関数を組み合わせることで実現できるため、本稿後半で解説する。

なお上記の各処理は、Microsoft Excel 等の表計算ソフトにおいて「ルックアップ lookup」として知られる機能に近いことができる。そこで、いわゆるルックアップをR環境で行う方法のあれこれについても、本稿の最後で簡単にまとめておく。

目次

  1. 複数の tidy なデータ表を統合する
    • 複数の tidy なデータフレームの合意データを作る dplyr::*_join 関数ファミリー
  2. データフレームの欠損を含む行を補完する
    • 複数のデータ列のユニークな組み合わせを返す tidyr::expand 関数
    • tidyr::expand と dplyr::left_join の併せ技によるデータフレームの欠損組み合わせ補完
    • データフレームの変数組み合わせの欠損行をNA(or指定文字列)で補完する tidyr::complete 関数
  3. Rにおけるルックアップ処理
    • Tidyverse におけるルックアップ
    • 迷ったらfor文
    • あるいは dplyr::do で
    • 添字アクセスによるルックアップテーブルの実現
  4. おまけ:Set operations
    • 列の構成が等しい2つのデータフレームからの、一致行ないし非一致行の絞り込み
      観測データの積集合を返す dplyr::intersect(x, y) 関数
      観測データの和集合を返す dplyr::union(x, y) 関数
      x にはあるが y にない行を返す dplyr::setdiff(x, y) 関数

複数の tidy なデータ表を統合する

前稿 では単一のデータフレームの形状変更を解説した。次に押さえねばならないのは、主となるデータを格納した表以外にも対応表が存在する場合や、データシートが分割されている場合に、これら複数の tidy 形式の表データを統合する方法である。
例として、前に用いた架空の農薬試験データセット "tidypest01.csv" に対して、調査地点 Site を別表 "tidycity01.csv" に記載していたとしよう。


tidypest.raw <- read.table("tidypest01.csv", header=TRUE, sep="," , as.is=TRUE )
# たとえば、Site 1 ~ 6 が実際には以下の場所で、別表に記載されていたとする。
tidycity <- read.table("tidycity01.csv", header=TRUE, sep="," , as.is=TRUE )

> 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

> tidycity
  Site Site.name
1    1   Saitama
2    2    Nagano
3    3  Shizuoka
4    4     Osaka
5    5    Moscow
6    6 Melbourne

このとき、tidycity$Site を照合キーとして tidypest.raw の各行に、Site 1 から 6 に対応する都市名を格納したい。一番簡単な方法は、1~6 の順番が綺麗に一致したデータであることを利用し、そのまま

tidypest.raw2 <- tidypest.raw
tidypest.raw2$Site.name <- tidycity$Site.name

として代入してしまうことである。


> tidypest.raw2
      Season Site Treat.Before Treat.After Control.Before Control.After Site.name
1     August    1            6           4              4             1   Saitama
2     August    2            2           6              5             0    Nagano
3     August    3            4           1              5             0  Shizuoka
4     August    4            1           6              3             0     Osaka
5     August    5            2           5              4             0    Moscow
6     August    6            2          NA              8             1 Melbourne
7  September    1            1           1              2             3   Saitama
8  September    2            5           2              1             3    Nagano
9  September    3            3           0              4             3  Shizuoka
10 September    4            2           0              1             5     Osaka
11 September    5            0           2              2             7    Moscow
12 September    6            5           1              4             5 Melbourne

まあ、この tidypest.raw2 が今回の完成イメージだと思ってほしい。だが単純な代入は一般性を欠く。なので、正規な方法で両者の合意データを作りたい。まずヴァニラなR環境では、merge 関数を使う。以下の方法で出来るゾ。


tidypest.merged <- merge(x=tidypest.raw, y=tidycity, by="Site", all=TRUE) # このデータの場合 by="Site", all=TRUE は無くてもいいが、書いておいたほうが安全。
tidypest.merged

(上記の tidypest.raw2 と全く同じ見た目のデータができる)

複数の tidy なデータフレームの合意データを作る dplyr::*_join 関数ファミリー

merge より高速かつ詳細なオプション指定ができる(重複列の発生を回避できる)のが、dplyr の *_join を使う方法。たとえば dplyr: A Grammar of Data Manipulationdplyr — 高速data.frame処理 - Heavy Watal など参照。


# tidypest.raw の全行を保持しつつ tidycity の内容を結合する left_join() 処理
# install.packages('tidyverse', dependencies=TRUE) # 初回ならば要インストール
library(tidyverse) # load the package of "tidyverse"
tidypest.merged <- tidypest.raw %>%
    dplyr::left_join( tidycity, by=c("Site"="Site")   )
tidypest.merged

# なお慣行として by を具体的に指定することが好ましいが、一応以下でも処理できる。
tidypest.merged <- tidypest.raw %>%
    dplyr::left_join( tidycity )
tidypest.merged

さて *_join は、実際のところ以下の6つの関数を含むファミリーである。詳しくは知らないのだが元々、SQLのテーブル操作に由来する機能らしい。しっかり覚えて違いの分かる人になろう。


full_join(x, y, by = NULL, copy = FALSE, suffix = c(".x", ".y"), ...)
left_join(x, y, by = NULL, copy = FALSE, suffix = c(".x", ".y"), ...)
right_join(x, y, by = NULL, copy = FALSE, suffix = c(".x", ".y"), ...)
inner_join(x, y, by = NULL, copy = FALSE, suffix = c(".x", ".y"), ...)
semi_join(x, y, by = NULL, copy = FALSE, ...)
anti_join(x, y, by = NULL, copy = FALSE, ...)

省略不可な引数
x, y    結合したい2つのテーブル

オプションの引数
by    2つのテーブル間で共通の変数に対応する、列の名前を格納した文字列ベクトル。デフォルトは NULL であり、*_join() 関数はいわゆる "natural join"、すなわち2つのテーブルにおいて共通の列名が付いている変数全てを用いた、行のマッチングを実行する(須藤注:要するに、byに複数の列を指定したとき、値が完全一致する行同士を結合する)。byの中身を明示的に指定しなかった場合には、どの列を用いて join したかを通知するメッセージが出力される。
もしテーブル x と y で、同じ変数が異なる列名で保持されている場合は、たとえば by=c("a"="b") などとして名前付きベクトルで指定することで x.a と y.b をマッチさせることができる。

copy    もし x と y が異なるデータソース(これはRのインターフェースから SQL データベースに接続して使っている場合のことを指す)に基づいており、かつ copy=TRUE であるとき、 y の内容が x の属するデータベース(src)へコピーされる。これは潜在的に時間やメモリをバカ食いしうる操作であり、必要な場合だけ行うこと。

suffix    x, y に共通する名前の変数があったにもかかわらず by で指定されなかったとき、結合後のデータフレームにおける列名の重複を避けるために付ける接尾辞。長さ2の文字列ベクトルで指定。デフォルトでは出自に基づいて hoge -> hoge.x or hoge.y となる。

...    渡したい追加の引数があれば。

分かる人のための蛇足:もし x が dplyr::group_by() で層別された tibble である場合には、joining 最中には grouping は考慮されないが、最終的に生成されるデータには x に準拠した grouping が保持される。けっこう便利。

他に、よく引っかかる落とし穴として、by に指定した変数が片方のテーブルでは integer (例:Time=c(1, 2) とか)だが、もう片方では character (例:Time=c("1", "2") とか)になっていることがある。このとき、
Cant join on 'Time' x 'Time' because of incompatible types (integer / character)
と叱られてしまい、エラーで止まる。予めどちらかへデータ型を揃えておく必要がある。なお character vector と factor を合わせた場合は結合可能だが、character vector の方に揃えられる(うろ覚え→要検証)。

結合タイプについて

たとえば以下の2つのデータフレーム(tibble形式データフレーム)である band_members と band_instruments がある(dplyrヘルプドキュメントより)。


# いずれも tidyverse パッケージ組み込みのサンプルデータ
> band_members
# A tibble: 3 x 2
 name  band
 <chr> <chr>
1 Mick  Stones
2 John  Beatles
3 Paul  Beatles

> band_instruments
# A tibble: 3 x 2
 name  plays
 <chr> <chr>
1 John  guitar
2 Paul  bass
3 Keith guitar

両者の共通する列名は name であり、*_join() 関数の引数 by を指定しなければ、 by="name" あるいは by=c("name"="name") とした場合と同じことになる(これは幸運の結果に過ぎない。関数の挙動を制御するため、可能な限り by は明示すること)。

さて、*_join(x=band_members, y=band_instruments) という操作を考える。メンバー John と Paul は2つのデータの両方に登場する結合キーになるが、Mick と Keith はそれぞれ band_members と band_instruments にしか含まれない。*_join ファミリーの各関数は、これらのデータを受けて異なるタイプの結合を行う。


left_join()    列については「(xにある列)と(yにある列)の和集合」として全て保持される。行については、xの現存する行は全て保持される。byで指定した列の組み合わせが x⊃y であれば、対応する y 側データの無い行が発生することになり、そのようなセル(以下の例では plays の Mick)には NA が挿入される(須藤注: x⊂y であれば、y側にしか存在しない行のデータは挿入されない)。
もしも完全一致する行が複数存在する場合は、念のため (xのbyで指定されない列) と (yのbyで指定されない列) の全コンビネーションが作られる。

> band_members %>% left_join(band_instruments) # band_members が x
Joining, by = "name"
# A tibble: 3 x 3
 name  band    plays
 <chr> <chr>   <chr>
1 Mick  Stones  <NA>
2 John  Beatles guitar
3 Paul  Beatles bass

right_join()    left_join() の x と y の力関係を入れ替えたもの。
須藤注:だったら left_join() だけを、x と y を入れ替えて使えばいいじゃん?と思うかもしれない。まあ裸でこの関数を使う場合はその通り。だが実用的にはパイプライン中で、x を予め加工した後に %>% right_join(y) として流し込むことがあり、この用途は left_join() では代替できないのだ。

> band_members %>% right_join(band_instruments)
Joining, by = "name"
# A tibble: 3 x 3
 name  band    plays
 <chr> <chr>   <chr>
1 John  Beatles guitar
2 Paul  Beatles bass
3 Keith <NA>    guitar

full_join()    x および y に存在するデータ行および列を全て保持しつつ合意データを作る。対応する行がもう片方にない列については、NA で補完する。

> band_members %>% full_join(band_instruments)
Joining, by = "name"
# A tibble: 4 x 3
 name  band    plays
 <chr> <chr>   <chr>
1 Mick  Stones  <NA>
2 John  Beatles guitar
3 Paul  Beatles bass
4 Keith <NA>    guitar
> # "Filtering" joins keep cases from the LHS

inner_join()    列については「(xにある列)と(yにある列)の和集合」として全て保持される。行については(xのby指定列)と(yのby指定列)が完全一致する場合のみ、合併対象となる。もしも完全一致する行が複数存在する場合は、(xのbyで指定されない列)と(yのbyで指定されない列)の全コンビネーションが作られる。

> band_members %>% inner_join(band_instruments)
Joining, by = "name"
# A tibble: 2 x 3
 name  band    plays
 <chr> <chr>   <chr>
1 John  Beatles guitar
2 Paul  Beatles bass

以下に紹介する semi_join() と anti_join() は、正確にはデータの結合というよりも、データフレーム x に含まれる観測例を、y との共通項の有無でスクリーニングする処理。


semi_join()    xの行のうち、yにも共通列が存在するものだけを残す処理。y 由来のデータは保持されず、判定のみに用いられる。

> band_members %>% semi_join(band_instruments)
Joining, by = "name"
# A tibble: 2 x 2
 name  band
 <chr> <chr>
1 John  Beatles
2 Paul  Beatles

anti_join()    xの行のうち、yに対応行が存在しないものを抽出する処理。要するに semi_join() の y に関する余事象。

> band_members %>% anti_join(band_instruments)
Joining, by = "name"
# A tibble: 1 x 2
 name  band
 <chr> <chr>
1 Mick  Stones

なお dplyr の機能上の分類として、semi_join() と anti_join() は filtering joins と呼ばれ、出力オブジェクトには y 由来の列が含まれない(y のデータは判定材料としてのみ使われる)。一方 left_join(), right_join(), full_join(), inner_join() は mutating joins と呼ばれ、x 由来の列と y 由来の列の両方が保持される。

さらに inner_join() と semi_join() の違いとして、仮に x のある行に対応する y の行が複数存在したとき、inner_join() では考えられる全組み合わせを実現するように x の行が複製されるが、semi_join() ではそのようなことは起こらない。


例: ジョン・コルトレーンのデータ行を足してみる

band_instruments2 <- tibble(name=c("John", "Paul", "Keith", "John"), plays=c("guitar", "bass", "guitar", "Sax"))
band_members2 <- tibble(name=c("Mick", "John", "Paul", "John"), band=c("Stones", "Beatles", "Beatles", "Miles"))

> band_members2 %>% inner_join(band_instruments2, by=c("name"))
# A tibble: 5 x 3
 name  band    plays
 <chr> <chr>   <chr>
1 John  Beatles guitar
2 John  Beatles Sax
3 Paul  Beatles bass
4 John  Miles   guitar
5 John  Miles   Sax
# データ構造上、Beatles の John Lennon と Miles band の John Coltrane の区別ができないので、inner_join() では全コンビネーションを返すよう行が複製される。結果としてビートルズにもサックス吹きが在籍するという、妙な結果になってしまう。

> band_members2 %>% semi_join(band_instruments2, by=c("name"))
# A tibble: 3 x 2
 name  band
 <chr> <chr>
1 John  Beatles
2 Paul  Beatles
3 John  Miles

データフレームの欠損を含む行を補完する

やや応用的な手法として、あるデータフレームに格納されているデータにおいて、ファクター間の組み合わせに欠落があるとき、欠けている組み合わせに相当するデータ行を NA で保管して作る方法を紹介する。

前回記事で作った tidypest.long.csv(ダウンロード) データを使う。


name <- "tidypest.long"
assign(name, readr::read_csv(paste(name, "csv", sep=".")))
get(name)

# A tibble: 48 x 5
   Season     Site Treatment Time   TempValue
   <chr>     <int> <chr>     <chr>      <int>
 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
# ... with 38 more rows

# 上記は完全なデータだが、わざと組み合わせに欠損を作っておく。季節が August であるもののうち、Site が 1,6 であるか、または Time が Before である行だけを抽出。
tidypest.partial <- get(name) %>%
    dplyr::filter(Season=="August", (Site==c(1, 6) | Time=="Before"))
data.frame(tidypest.partial)

> data.frame(tidypest.partial)
   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  August    1     Treat  After         4
8  August    6     Treat  After        NA
9  August    1   Control Before         4
10 August    2   Control Before         5
11 August    3   Control Before         5
12 August    4   Control Before         3
13 August    5   Control Before         4
14 August    6   Control Before         8
15 August    1   Control  After         1
16 August    6   Control  After         1

さて、この欠損を含む tidypest.partial について、(紙面の都合で季節は August に限るが)Site の (1:6)、Treatment の (Control or Treat)、Time の (Before or After) の全組み合わせを再現し、対応する TempValue 列のデータ内容が存在しない行(=行が欠損している)は欠損値 NA として明示的に作る処理を行う。

複数のデータ列のユニークな組み合わせを返す tidyr::expand 関数


# まず expand() することで、補完対象列のユニークな組み合わせを取り出したラベルを作っておく。
tidypest.complete.labels <- tidypest.partial %>%
    tidyr::expand(Site, Treatment, Time)
print(tidypest.complete.labels, n=1000) # tibbleの全行を示すために print を使う

# A tibble: 24 x 3
    Site Treatment Time
   <int> <chr>     <chr>
 1     1 Control   After
 2     1 Control   Before
 3     1 Treat     After
 4     1 Treat     Before
 5     2 Control   After
 6     2 Control   Before
 7     2 Treat     After
 8     2 Treat     Before
 9     3 Control   After
10     3 Control   Before
11     3 Treat     After
12     3 Treat     Before
13     4 Control   After
14     4 Control   Before
15     4 Treat     After
16     4 Treat     Before
17     5 Control   After
18     5 Control   Before
19     5 Treat     After
20     5 Treat     Before
21     6 Control   After
22     6 Control   Before
23     6 Treat     After
24     6 Treat     Before

tidyr::expand と dplyr::left_join の併せ技によるデータフレームの欠損組み合わせ補完

expand はあくまでラベルを作る処理なので、非対象列のデータは含まれない。元のデータにあった列 TempValue を含む補完済みデータフレームをあらためて作るには、tidyr パッケージの expand() 関数と dplyr の left_join() 関数を併せて使う。


# ややトリッキーだが expand() した産物に対して、expand する前の自分自身を第2引数として left_join() を噛ませると、欠損データ行が補完できる。
tidypest.complete <- tidypest.partial %>%
    tidyr::expand(Site, Treatment, Time) %>%
    dplyr::left_join(tidypest.partial)
print(tidypest.complete, n=1000)

# A tibble: 24 x 5
    Site Treatment Time   Season TempValue
   <int> <chr>     <chr>  <chr>      <int>
 1     1 Control   After  August         1
 2     1 Control   Before August         4
 3     1 Treat     After  August         4
 4     1 Treat     Before August         6
 5     2 Control   After  <NA>          NA
 6     2 Control   Before August         5
 7     2 Treat     After  <NA>          NA
 8     2 Treat     Before August         2
 9     3 Control   After  <NA>          NA
10     3 Control   Before August         5
11     3 Treat     After  <NA>          NA
12     3 Treat     Before August         4
13     4 Control   After  <NA>          NA
14     4 Control   Before August         3
15     4 Treat     After  <NA>          NA
16     4 Treat     Before August         1
17     5 Control   After  <NA>          NA
18     5 Control   Before August         4
19     5 Treat     After  <NA>          NA
20     5 Treat     Before August         2
21     6 Control   After  August         1
22     6 Control   Before August         8
23     6 Treat     After  August        NA
24     6 Treat     Before August         2

# まあ以下でも同じ操作が出来ます。
dplyr::left_join(tidypest.complete.labels, tidypest.partial)

複数のデータ列のユニークな組み合わせを返す
expand(data, ...)

省略不可な引数
data    処理対象のデータフレーム。
...    どの列を expand するかの指定。

たとえば df という名のデータフレーム中に存在する列 x, y, z の各水準に対して、実際の df に欠けているものを含めて全てのユニークな組み合わせを作成するには
expand(df, x, y, z)
とすればいい。

####

サブ機能を提供する関数
crossing(...)
nesting(...)

crossing() は R の標準関数 expand.grid() とほぼ同じ機能を提供するが、文字列を factor 型データに変換することはしない。
nesting() は crossing() と相補的な働きをする関数で、crossing() が各変数の有する水準について、想定しうる全組み合わせを作るのに対し、nesting() は既にデータ中に存在する組み合わせのみを返す。

実際の df に含まれている x, y, z の組み合わせのみについて、重複を無くして列挙したいならば
expand(df, nesting(x, y, z))
として、対象の変数を nesting() で括る。

expand(df, nesting(x, y), z)
であれば、x と y については既存の組み合わせだけが抽出され、それらと z との間では想定しうる全組み合わせが作成される。

なお数値データ列に対しては、 year=2010:2020 とか year=full_seq(year, 1) などとして連続データを手動で割り付けることも可能。

このように、データフレームにおいて暗示的に含まれている欠損値(変数の組み合わせが抜けている行が、そもそも存在しないようなデータ)を、明示的な欠損値(抜けている組み合わせに対応する行が存在し、欠損値はNAとして格納されている)に変換するために、expand() はしばしば left_join() と一緒に使われる(古典的には expand.grid と merge の合わせ技でもできる)。ちなみに、どの組み合わせが抜けているかを視覚化する場合には anti_join() と繋げる方法も有用である。

データフレームの変数組み合わせの欠損行をNA(or指定文字列)で補完する tidyr::complete 関数

ただし、上記の補完目的では既に tidyr::complete() という専用の関数が用意されている。これは、tidyr::expand(), dplyr::left_join(), tidyr::replace_na() の各関数を繋げて作られたラッパー関数である。


# 欠損行を暗示的に含むデータフレーム(再掲)
> data.frame(tidypest.partial)
   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  August    1     Treat  After         4
8  August    6     Treat  After        NA
9  August    1   Control Before         4
10 August    2   Control Before         5
11 August    3   Control Before         5
12 August    4   Control Before         3
13 August    5   Control Before         4
14 August    6   Control Before         8
15 August    1   Control  After         1
16 August    6   Control  After         1

# ラッパー関数 complete() を使った自動補完。
# expand 対象に指定されなかった行は、引数 fill のリスト内要素に指定した文字列を用いて、欠損値が補完される。未指定ならば NA が入る。
# このうち Season は全て "August" であることが分かっているので、fill=list(Season="August") で補完する。
# いっぽう TempValue については fill=list(TempValue=0) として、不明な部分には便宜的にゼロを代入している。
tidypest.complete <- tidyr::complete(tidypest.partial, Site, Time, fill=list(Season="August", TempValue=0))
tidypest.complete

# A tibble: 20 x 5
    Site Time   Season Treatment TempValue
   <int> <chr>  <chr>  <chr>         <dbl>
 1     1 After  August Treat            4.
 2     1 After  August Control          1.
 3     1 Before August Treat            6.
 4     1 Before August Control          4.
 5     2 After  August <NA>             0.
 6     2 Before August Treat            2.
 7     2 Before August Control          5.
 8     3 After  August <NA>             0.
 9     3 Before August Treat            4.
10     3 Before August Control          5.
11     4 After  August <NA>             0.
12     4 Before August Treat            1.
13     4 Before August Control          3.
14     5 After  August <NA>             0.
15     5 Before August Treat            2.
16     5 Before August Control          4.
17     6 After  August Treat            0.
18     6 After  August Control          1.
19     6 Before August Treat            2.
20     6 Before August Control          8.

tidypest.complete <- tidyr::complete(tidypest.partial, Site, Treatment, Time, fill=list(Season="August", TempValue=NA))
print(tidypest.complete, n=10000)

# A tibble: 24 x 5
    Site Treatment Time   Season TempValue
    <int> <chr>  <chr>  <chr>         <dbl>
 1     1 Control   After  August         1
 2     1 Control   Before August         4
 3     1 Treat     After  August         4
 4     1 Treat     Before August         6
 5     2 Control   After  August        NA
 6     2 Control   Before August         5
 7     2 Treat     After  August        NA
 8     2 Treat     Before August         2
 9     3 Control   After  August        NA
10     3 Control   Before August         5
11     3 Treat     After  August        NA
12     3 Treat     Before August         4
13     4 Control   After  August        NA
14     4 Control   Before August         3
15     4 Treat     After  August        NA
16     4 Treat     Before August         1
17     5 Control   After  August        NA
18     5 Control   Before August         4
19     5 Treat     After  August        NA
20     5 Treat     Before August         2
21     6 Control   After  August         1
22     6 Control   Before August         8
23     6 Treat     After  August        NA
24     6 Treat     Before August         2

complete(data, ..., fill = list())

省略不可な引数
data    処理対象のデータフレーム。
...    どの列を expand するかの指定。指定方法は expand(data, ...) と同じ。

オプションの引数
fill    名前付きリスト。名称で指定した各変数に対して、データが存在しない組み合わせ=欠損値を埋めるための値を与える。与えなければ NA になる。

まあ実用的な補完処理には tidyr::complete() を使うのが楽。ただし expand の基本機能は理解しておいたほうがいいだろう。

Rにおけるルックアップ処理

データ整理や解析のための前処理では頻出するにもかかわらず、なぜかRでルックアップをやる方法は、まとまった日本語情報が乏しい。せっかくなので代表的なソリューションの幾つかを列挙しておこう。

Tidyverse におけるルックアップ

そもそもルックアップとは何か。A, Bという2つの表があり、両方に共通するデータ列がある(keyとする)。このとき、

  1. A$key 列の i 行目に存在するデータ A[i, "key"] について、等しいデータが存在する B$key 列の行を探す(仮に j 行目とすると、A[i, "key"] == B[j, "key"] を満たす)。
  2. 然る後に B[j, ] の任意列のデータを A[i, ] の任意列へコピーする。マッチする j がちょうど1つだけ存在しない場合は、対策を別途サブルーチンとして実装する必要がある。

という処理が、Excel 等で想定されているルックアップ処理である。

賢明な須通りキッズの皆は気づくだろう、これは本稿の冒頭で dplyr::left_join(A, B, by=c("key")) として紹介した関数の機能に他ならない。あるいは古典的な merge(x=A, y=B, by="key") でもいい。

迷ったらfor文

当然、上記の処理は for (i in 1:nrow(A)) { 処理 } として、愚直にループを回しつつ一致行を見つける方法でも実装できる。計算速度は left_join に比べて相当に遅いものの、仮にマッチするB側の行が複数あったり、欠損値の補完を複雑な規則でやらざるを得なかったりする場合には、実用的な解法である。

あるいは dplyr::do で

とはいえ left_join で書けない複雑な条件処理であっても、同じく dplyr の do() という関数を使って書けることがある。説明しにくい関数なので、詳細は次の次あたりの記事で。

添字アクセスによるルックアップテーブルの実現

ルックアップのようなものを簡易に実現するクイックハックとして、特別な関数を使わずに B の要素へ添字アクセスする方法がある。


tidypest.raw2 <- read.table("tidypest01.csv", header=TRUE, sep="," , as.is=TRUE )
tidycity <- read.table("tidycity01.csv", header=TRUE, sep="," , as.is=TRUE )

> tidypest.raw2
      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

> tidycity
  Site Site.name
1    1   Saitama
2    2    Nagano
3    3  Shizuoka
4    4     Osaka
5    5    Moscow
6    6 Melbourne

########

# tidycity$Site.name を 単純にベクトル化すると、上手いことに要素のインデックスが tidycity$Site と一致する。
vect.city <- tidycity$Site.name

> vect.city
[1] "Saitama"   "Nagano"    "Shizuoka"  "Osaka"     "Moscow"    "Melbourne"

# tidypest.raw2$Site が整数値であることを利用し、ルックアップテーブルこと
# vect.city の、当該アドレスに格納されている要素を取得してくる。
tidypest.raw2$Site.name <- vect.city[tidypest.raw2$Site]

> tidypest.raw2
      Season Site Treat.Before Treat.After Control.Before Control.After Site.name
1     August    1            6           4              4             1   Saitama
2     August    2            2           6              5             0    Nagano
3     August    3            4           1              5             0  Shizuoka
4     August    4            1           6              3             0     Osaka
5     August    5            2           5              4             0    Moscow
6     August    6            2          NA              8             1 Melbourne
7  September    1            1           1              2             3   Saitama
8  September    2            5           2              1             3    Nagano
9  September    3            3           0              4             3  Shizuoka
10 September    4            2           0              1             5     Osaka
11 September    5            0           2              2             7    Moscow
12 September    6            5           1              4             5 Melbourne

上記の例ではたまたま tidypest.raw2$Site および tidycity$Site が 1, 2, 3, ... という一繋がりの整数なので、前処理抜きに添字アクセスでデータを取得できた。とはいえ現実の局面では、as.factor(Site) を使って強引に自然数のインデックスを作るという手も使えるから、意外と本手法の応用範囲は広い。さらには WinBUGS や JAGS を用いて階層ベイズモデルのパラメータ推定を行う場合にも、カテゴリ変数を構造化する手法として類似の戦術が広く使われる。本サイトでも別の機会に解説する予定である。

おまけ:Set operations

今回紹介した関数は公式のビネットで Two-table verbs として解説されているツールと、大雑把に一致する。詳解したものの他にも、2つの(列の構成が等しい)データフレームからの、一致行ないし非一致行の絞り込み(filtering join, set operations と呼ばれる機能)ができるのが、以下の関数 intersect(), union(), setdiff() である。基本的には観測例を追加する等のシチュエーションに使う機能で、*_join() のように列(変数)の構成を弄ることはできない。cbind() に毛が生えたものと思えばいい。


intersect(x, y): # 観測データの積集合、すなわち両方のデータフレームに存在する行のみを抽出する
union(x, y): # 観測データの和集合、すなわち両データフレームを縦に結合してから、重複行を削除したものを出力する
setdiff(x, y): # x にはあるが y にない行を返す。これのみ引数の順番が結果に影響する。

# 実際何が起こるかというと、
band_members3 <- tibble(name=c("John", "Paul", "John"), band=c("Beatles", "Beatles", "Miles"))

> band_members
# A tibble: 3 x 2
 name  band
 <chr> <chr>
1 Mick  Stones
2 John  Beatles
3 Paul  Beatles

> band_members3
# A tibble: 3 x 2
  name  band
 <chr> <chr>
1 John  Beatles
2 Paul  Beatles
3 John  Miles

> intersect(band_members, band_members3)
# A tibble: 2 x 2
  name  band
 <chr> <chr>
1 John  Beatles
2 Paul  Beatles

> union(band_members, band_members3)
# A tibble: 4 x 2
  name  band
 <chr> <chr>
1 John  Miles
2 Paul  Beatles
3 John  Beatles
4 Mick  Stones

> setdiff(band_members, band_members3)
# A tibble: 1 x 2
  name  band
 <chr> <chr>
1 Mick  Stones

> setdiff(band_members3, band_members)
# A tibble: 1 x 2
  name  band
 <chr> <chr>
1 John  Miles


# 基本的に set operation は比較対象の列構成が一致していることが必要。
> intersect(band_members, band_instruments)
 intersect_data_frame(x, y) でエラー: not compatible:
- Cols in y but not x: `plays`.
- Cols in x but not y: `band`.

では、今回はここまで。


# いったん、ここまでのデータを保存する。
today <- "180902"
save( list=ls(), file=paste("tidy", today, "dat", sep=".") ) # ワークスペースをバイナリ形式で

# よみこみ
today <- "180902"
load( file=paste("tidy", today, "dat", sep=".") )

# csvで保存
write_csv( tidypest.merged, path=paste("tidypest", "merged", "csv", sep="."))