fuzzyjoinパッケージでいい感じにjoin

dplyrパッケージの*_join()は、キーとなる列の値が完全に一致する必要があります。しかし、世の中のデータはそんなにきれいに一致はしていなくて、微妙に表記が違うとか微妙に値がずれているみたいなことがよくあります。

そんなときは、fuzzyjoinパッケージです。

fuzzyjoinには以下の4種類のjoinがあります。

  • difference_*_join(): 値の差が許容値以内のものは同じとみなしてjoin
  • stringdist_*_join(): 文字列距離でjoin
  • regex_*_join(): 片方のデータの正規表現でもう片方の文字列をマッチさせてjoin(これはたぶん名前から予想するのと違う)
  • distance_*_join(): 幾何学的な距離でjoin
  • geo_*_join(): 地理的な距離でjoin
  • interval_*_join(): 値の範囲でjoin
  • genome_*_join(): interval_*_join()の遺伝子版

stringdist_*_join()vignetteに詳しいので、ここでは、意外と便利そうなinterval_*_join()と、ちょっとわかりづらいregex_*_join()について取り上げます。geo_*_join()は今だとsfでもっと楽にできるのかな?と思ったけど分からなかった。

interval_*_join()

interval_*_join()は、値の範囲が重なるデータをjoinしたい、というときに便利です。

x1 <- data.frame(id1 = 1:3, start = c(1, 5, 10), end = c(3, 7, 15))
x2 <- data.frame(id2 = 1:3, start = c(2, 4, 16), end = c(4, 8, 20))

interval_*_join()を使うには、Bioconductor上にあるIRangeというパッケージが必要です。まずはBioconductor - Installの指示に従ってBioconductorを使う準備をしましょう。biocLite()を実行すればIRangeパッケージは自動でインストールされるはずです。

準備ができたらさっそくinterval_inner_join()を使ってみましょう。

library(fuzzyjoin)

interval_inner_join(x1, x2)
#> Joining by: c("start", "end")
#>   id1 start.x end.x id2 start.y end.y
#> 1   1       1     3   1       2     4
#> 2   2       5     7   2       4     8

1行目は[1, 3]という区間[2,4]という区間、2行目は[2, 5]という区間[2, 4]という区間に重なりがあるためjoinの対象となっています。

これは、数値だけでなく日付型にも使うことができます。上で説明しませんでしたが、byには区間の始点の列と終点の列を指定します。指定しない場合は、startという列とendという列がそれぞれ使われます(なので同名の列がデータにないとエラーになる)。

library(dplyr, warn.conflicts = FALSE)

x3 <- mutate_at(x1, vars(-id1), funs(date = Sys.Date() + .))
x4 <- mutate_at(x2, vars(-id2), funs(date = Sys.Date() + .))

interval_inner_join(x3, x4, by = c("start_date", "end_date"))
#>   id1 start.x end.x start_date.x end_date.x id2 start.y end.y start_date.y
#> 1   1       1     3   2017-11-24 2017-11-26   1       2     4   2017-11-25
#> 2   2       5     7   2017-11-28 2017-11-30   2       4     8   2017-11-27
#>   end_date.y
#> 1 2017-11-27
#> 2 2017-12-01

Intervalにも使えると嬉しいんですが、それはちょっとまた別なのかもしれません。

regex_*_join()

例えば、こういうデータがあるとします。

data <- data.frame(
  id = c("uri", "u_ribo", "uribo", "hoxo_m", "hoxo-m", "hoxo_m"),
  venue = c("GitHub", "Twitter", "Qiita", "Twitter", "GitHub", "Qiita"),
  stringsAsFactors = FALSE
)

お分かりのように、uriu_ribouriboというのは同一人物です。ですが、IDに微妙にゆれがあり、このままではうまくjoinすることができません。 そんなとき、あらかじめ正規表現を用意しておけば、その正規表現を使ってデータをjoinすることができる、というのがregex_*_join()です。

catalog <- data.frame(
  name = c("uribo", "hoxo-m"),
  role = c("Chief Globalization Officer", "President"),
  regex = c("u_?ri(bo)?", "hoxo[_\\-]m"),
  stringsAsFactors = FALSE
)

regex_inner_join(data, catalog, by = c(id = "regex"))
#>       id   venue   name                        role       regex
#> 1    uri  GitHub  uribo Chief Globalization Officer  u_?ri(bo)?
#> 2 u_ribo Twitter  uribo Chief Globalization Officer  u_?ri(bo)?
#> 3  uribo   Qiita  uribo Chief Globalization Officer  u_?ri(bo)?
#> 4 hoxo_m Twitter hoxo-m                   President hoxo[_\\-]m
#> 5 hoxo-m  GitHub hoxo-m                   President hoxo[_\\-]m
#> 6 hoxo_m   Qiita hoxo-m                   President hoxo[_\\-]m

あらかじめ正規表現の列を作っておくというのがやや面倒ですが、まあ手軽に使うには便利そうですね(ちゃんとやるなら、case_when()なりrecode()なりであらかじめ共通のIDに変換してからやるべきだと思います)。

まとめ

stringdist_*_join()しか知らなかったんですが、意外と便利っぽいです。