あらまし

fuzzyjoinパッケージについて知ったのは r-wakalang で質問させていただいたことが縁になります。yutannihilation=サンにパッケージを教えていただいたのですが、本パッケージ、なぜか使ってみた系記事すらあまり引っかからないとのこと。これも縁で私が書こうと思っていたのですが、多忙から曖昧な関係にしていたのをこの気に成仏させようというのが今回の記事の背景です。本記事は Rアドベントカレンダー 23日目となります。

fuzzyjoin package

fuzzyjoinパッケージはR前処理の心の友であるdplyrの機能を補うパッケージです。 CRANに挙がっている一行パッケージ内容紹介には “Join Tables Together on Inexact Matching” とあり、完全には一致しないIDをキーにして、データをjoinするためのパッケージだとわかります。

完全に一致しないIDとはどのようなものでしょうか。例えば誤記などがあるでしょう。fuzzyjoinパッケージには代表的なスペルミスデータを集めたmisspellingsデータが含まれています (abboutとaboutなど)。このようなスペルミスをkeyにしてjoinすると、考えているのとは違うデータしか得られないことでしょう。 fuzzyjoinパッケージの関数には上記のようなミススペルのためだけではなく、目的に応じて以下のような関数が用意されています。

interval_hoge_join, regex_hoge_joinについてはyutannihilation=サンが以前の記事、 fuzzyjoinパッケージでいい感じにjoin で例をあげてくれているので、これらの紹介についてはリンクを張って済ませます。

hoge部分にはdplyrでよく使うようなinner, left, right, full, semi, antiが入ります。

見てるだけじゃ全くわからんのでそれぞれ具体的に見ていきましょう。 余裕がなかったのでパッケージのexamplesばっかりで申し訳ないです。。。

実例

difference_hoge_join

何はともあれパッケージを読み込んでおきましょう。未インストールの人はinstall.packages()しておきましょう。

library(dplyr)
## 
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union
library(DT)
library(fuzzyjoin)
library(sessioninfo)

今回はexampleがinner_joinで統一されているためinner_joinばかりを例にあげますが、必要なjoinに適宜置き換えてください。はじめに紹介するのはdifference_inner_joinです。以下のように、例にはみんな大好きirisデータを使います。

data(iris)
datatable(iris)

今回はSepal.Lengthkeyにして、新しい変数Typejoinすることを考えます。TypeデータはSepal.Lengthに対応したデータですが、Typeデータを格納しているデータフレームに格納されているSepal.Lengthは以下のように大雑把な値でしか得られていないとします。

sepal_lengths <- data_frame(Sepal.Length = c(5, 6, 7), Type = 1:3)
sepal_lengths
## # A tibble: 3 x 2
##   Sepal.Length  Type
##          <dbl> <int>
## 1            5     1
## 2            6     2
## 3            7     3

通常のjoinではこのようなデータの結合には対応できませんが、ここで “difference_hoge_join: 数値データ対象。値にちょっとしたブレがある時に使う。” の出番です。

通常のdplyr::inner_join()と全く同じノリで書いて行けるので違和感がなくていいですね。 max_distには許容できる値のブレの大きさを記述します。今回は0.5とします。適当なところを切り出して表示してみましょう。

temp <- iris %>% difference_inner_join(sepal_lengths, max_dist = .5)
## Joining by: "Sepal.Length"
datatable(temp[50:69, ])

ブレの大きさの範囲内に入った値でTypejoinされていることがわかります。大まかには他の関数もこのノリで使っていけます。

distance_hoge_join

別の指標に基づきデータをつなぐこともできます。次に示すのは “distance_hoge_join:複数列の距離に基づきデータをつなぎたい。” です。例はirisが続投です。

data(iris)
datatable(iris)

さっきのsepal_lengthsに似ていますが、データが新規のtypeではなくSepal.Widthになっている点に注目です。

sepal_lengths <- data_frame(Sepal.Length = c(5, 6, 7),
                            Sepal.Width = 1:3)

datatable(sepal_lengths)

距離をどのような指標で計算するかについてはmethodで指定することになっています。デフォルト設定はeuclideanです。結果を見るに、2変数のユークリッド距離が近い組み合わせでjoinされるようです。

temp <- iris %>% distance_inner_join(sepal_lengths, max_dist = 2)
## Joining by: c("Sepal.Length", "Sepal.Width")
datatable(temp[50:59, ])

stringdist_hoge_join

次に “stringdist_hoge_join: 文字列をコサイン距離などに基づいて操作したい。” を使ってみましょう。データはggplot2パッケージに含まれているdiamondsデータを利用します。

library(ggplot2)
data(diamonds)

ダイアモンドのカットについて新しくtypeというデータを作り、素のdiamondsデータにマージすることを考えます。しかし、diamondsデータのkeyに使う変数cut内の要素名がいまいち思い出せず曖昧になってしまったので新しいデータのkeyにはだいたいこんな名前 (approximate_name) という名前をつけておき、要素名も雑に入れておきます。雑さ加減がmaxなのでPremiumっぽい要素名が2つあったり、VeryGoodが2つある上にtypeも別だったりとハチャメチャですがあまり気にしないことにします。

d <- data_frame(approximate_name = c("Idea", "Premiums", "Premioom",
                                     "VeryGood", "VeryGood", "Faiir"),
                type = 1:6)
datatable(d)

で、これを普通のinner_joinで処理しようとしても当然だめなわけです。曖昧ですしね。

diamonds %>% inner_join(d, by = c(cut = "approximate_name"))
## Warning: Column `cut`/`approximate_name` joining factor and character
## vector, coercing into character vector
## # A tibble: 0 x 11
## # ... with 11 variables: carat <dbl>, cut <chr>, color <ord>,
## #   clarity <ord>, depth <dbl>, table <dbl>, price <int>, x <dbl>,
## #   y <dbl>, z <dbl>, type <int>

fuzzyjoinは曖昧さを許容します。文字列の距離をどのような指標で計算するかについてはmethodで指定することになっています。デフォルト設定が何なのかパッケージのexsamplesには乗っていませんでしたが、 stringdist packagestringdist-methodsを見ろとあったのでその設定どおりだとするとoptimal string alignment (OSA) ではないかと思われます。

temp <- diamonds %>% stringdist_inner_join(d, by = c(cut = "approximate_name"))
datatable(temp[, c(1:4, 11:12)])
## Warning in instance$preRenderHook(instance): It seems your data is too
## big for client-side DataTables. You may consider server-side processing:
## https://rstudio.github.io/DT/server.html

曖昧な指定でしたがなんとなくうまく結合してくれたようです。似たような名前、同じ名前だけど違うtypeを指定した変数については2, 3, 2, 3… のように固定の繰り返しでtypeが入力されるようです。

geo_hoge_join

次に紹介するのは “geo_hoge_join: 緯度経度にブレがあるけどつなぎたい。” です。 今回はdplyrパッケージ内のアメリカの州についてのデータを例に、州の中心から隣の州の中心まで200マイル以内の州を抜き出したいという例です。

library(dplyr)
data(state)

とりあえず州の名前と中心の緯度経度を抜き出しておきましょう。

states <- data_frame(state = state.name,
                     longitude = state.center$x,
                     latitude = state.center$y)
datatable(states)

続いて結合先と結合元のデータを作っておきます。名前は違いますが中身は同じです。

s1 <- rename(states, state1 = state)
s2 <- rename(states, state2 = state)

その後データをgeo_inner_join()で結合します。max_distで距離を、距離の単位はunitで指定できます。デフォルトは"miles"ですが、"km"に変えることもできます。最後にstate1と2の名前が違うところを抽出して終わりです。

pairs <- s1 %>%
 geo_inner_join(s2, max_dist = 200, unit = "miles") %>%
 filter(state1 != state2)
## Joining by: c("longitude", "latitude")
datatable(pairs)

地図を書くとお隣と200マイル以内のところが線でつながっていることがわかります。

library(ggplot2)
ggplot(pairs, aes(x = longitude.x, y = latitude.x,
                  xend = longitude.y, yend = latitude.y)) +
  geom_segment(color = "red") +
  borders("state") +
  theme_void()

以下のコードでそれぞれの距離についても抜き出すことができます。

temp <- s1 %>% geo_inner_join(s2, max_dist = 200, distance_col = "distance")
## Joining by: c("longitude", "latitude")
datatable(temp[, c(1, 4, 7)])

ついでなので"km"にした場合も見ておきましょう。

pairs2 <- s1 %>%
 geo_inner_join(s2, max_dist = 200, unit = "km") %>%
 filter(state1 != state2)
## Joining by: c("longitude", "latitude")
datatable(pairs)

ずいぶんさっぱりしましたね。

ggplot(pairs2, aes(x = longitude.x, y = latitude.x,
                  xend = longitude.y, yend = latitude.y)) +
  geom_segment(color = "red") +
  borders("state") +
  theme_void()

genome_hoge_join

最後は “genome_hoge_join: intervalをゲノム向けにしたやつ? chromosome IDとかに使う。” です。 interval_joinでもそうなのですが、BioconductorのIRangesパッケージがないと動かないので以下の感じでインストールしておいてください。

if (!requireNamespace("BiocManager", quietly = TRUE))
    install.packages("BiocManager")
BiocManager::install("IRanges", version = "3.8")

ゲノムについては研究がゲノムよりでないためあまりデータが意味するところをよく理解できていませんが、この解説 Tidy Genomics が参考になるかもしれません。

x1 <- data_frame(id1 = 1:4,
                 chromosome = c("chr1", "chr1", "chr2", "chr2"),
                 start = c(100, 200, 300, 400),
                 end = c(150, 250, 350, 450))

x2 <- data_frame(id2 = 1:4,
                 chromosome = c("chr1", "chr2", "chr2", "chr1"),
                 start = c(140, 210, 400, 300),
                 end = c(160, 240, 415, 320))
datatable(x1)
datatable(x2)
genome_inner_join(x1, x2, by = c("chromosome", "start", "end"))
## # A tibble: 2 x 8
##     id1 chromosome.x start.x end.x   id2 chromosome.y start.y end.y
##   <int> <chr>          <dbl> <dbl> <int> <chr>          <dbl> <dbl>
## 1     1 chr1             100   150     1 chr1             140   160
## 2     4 chr2             400   450     3 chr2             400   415

start, endがある程度近い値であればchromosomeで結合できるというでしょうか?これについてはinner_join以外の結合方法をいろいろ試したほうが良いのかもしれません。

再び動機

さて、私が r-wakalang で質問させていただいたのは以下のような内容でした。

df1 <- data.frame(
  A = c(2.310, 2.310, 3.309, 3.309),
  B = c(487.3, 504.3, 545.3, 546.4),
  C = c(73273, 105857, 831771, 449061)
)
df2 <- data.frame(
  A = c(2.311, 2.311, 3.310, 3.310),
  B = c(487.5, 504.5, 545.2, 546.3),
  C = c(83711, 104557, 541771, 356061)
)
df3 <- data.frame(
  A = c(2.390, 2.390, 3.311, 3.311),
  B = c(487.4, 504.2, 545.2, 546.1),
  C = c(173273, 112245, 531771, 4449061)
)

このdf1-3をAは2.3と3.3, Bは487, 504, 545, 546に丸めた上でキーとして以下のような形で出力したい。

      A   B    C1      C2       C3
1:  2.3 487 72373   83711   172373
2:  2.3 504 105857  104557  112245
3:  3.3 545 831771  541771  531771
4:  3.3 546 449061  356061  4449061

この際には nozma=サンに以下のような方法を教えていただきpurrr packageを勉強する動機をもらったわけなのですが、これと似たようなことをfuzzyjoinでもできるのでしょうか。

# data.frameのリストを作成しておく
df_list <- list(df1, df2, df3)

library(dplyr)
library(purrr)
## 
## Attaching package: 'purrr'
## The following object is masked from 'package:maps':
## 
##     map
map( # 最初の2列を丸めて文字列に変換
  df_list,
  function(x){
    # ※あえて文字列に変換しなくてもjoinはできます
    x[, 1] <- as.character(trunc(x[, 1]*10)/10)
    x[, 2] <- as.character(trunc(x[, 2]))
    x
  }
) %>%
  # 順番にjoin
  reduce(full_join, by = c("A", "B")) %>% 
  # 列名の調整
  set_names(c("A", "B", paste0("C", seq_along(df_list))))
##     A   B     C1     C2      C3
## 1 2.3 487  73273  83711  173273
## 2 2.3 504 105857 104557  112245
## 3 3.3 545 831771 541771  531771
## 4 3.3 546 449061 356061 4449061

ともあれやってみる。マージに使いたい指標が2つあるのでdistance_inner_joinですかね。 ABCのカラム名は共通してるので、by = c("A", "B") でつないでおきましょう。2つ目からはカラム名の強制変換で元の名前が変わっているので、by = c("A.x" = "A", "B.x" = "B") みたいな具合に書き換えておきます。

temp <- df1 %>% 
  distance_inner_join(df2, by = c("A", "B"), max_dist = 1) %>%
  distance_inner_join(df3, by = c("A.x" = "A", "B.x" = "B"), max_dist = 0.5)
  
datatable(temp)

なんとなくうまく行ってそうですね。ファイルが少し煩雑なのでいるところだけ切り出しておきましょう。

res <- temp %>%
  select("A", "B", starts_with("C")) 

datatable(res)

なんとかなったようです。良かったですね。

注意点と結び

よしなにデータを結合してくれるfuzzyjoinですが、何も考えずに使うと沼にハマる可能性もあります。たとえばさっきの私の例で、max_distがもっと大きい値の場合を考えてみましょう。

temp <- df1 %>% 
  distance_inner_join(df2, by = c("A", "B"), max_dist = 1) %>%
  distance_inner_join(df3, by = c("A.x" = "A", "B.x" = "B"), max_dist = 2)
  
res <- temp %>%
  select("A", "B", starts_with("C")) 

datatable(res)

余計なデータができてしまったことがわかります。文字列の距離で結合する場合には、似た単語があると気が付かないまま結合してしまうということもあると思われます。この辺についてはデータをしっかり把握しておく以外にこれと行った解決法があるわけでは無いので、副作用には注意しておきましょう。明日は24日だし一念発起して曖昧だった関係をはっきりさせようとしたら地雷を踏んだ、なんてのは笑えない話です。

ともあれRアドベントカレンダーも終盤ですね。明日はMed_KUさんです。楽しみですね。

参考資料

dplyr: A Grammar of Data Manipulation
dplyrを使いこなす!基礎編
fuzzyjoin at CRAN
fuzzyjoin at github
fuzzyjoinパッケージでいい感じにjoin
ggplot2: Create Elegant Data Visualisations Using the Grammar of Graphics
r-wakalang へようこそ
stringdist: Approximate String Matching and String Distance Functions
Tidy Genomics

実行環境

sessionInfo()
## R version 3.5.1 (2018-07-02)
## Platform: x86_64-apple-darwin15.6.0 (64-bit)
## Running under: macOS  10.14.2
## 
## Matrix products: default
## BLAS: /Library/Frameworks/R.framework/Versions/3.5/Resources/lib/libRblas.0.dylib
## LAPACK: /Library/Frameworks/R.framework/Versions/3.5/Resources/lib/libRlapack.dylib
## 
## locale:
## [1] ja_JP.UTF-8/ja_JP.UTF-8/ja_JP.UTF-8/C/ja_JP.UTF-8/ja_JP.UTF-8
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
## [1] purrr_0.2.5       maps_3.3.0        ggplot2_3.1.0     bindrcpp_0.2.2   
## [5] sessioninfo_1.1.1 fuzzyjoin_0.1.4   DT_0.5            dplyr_0.7.8      
## 
## loaded via a namespace (and not attached):
##  [1] stringdist_0.9.5.1  tidyselect_0.2.5    xfun_0.4           
##  [4] lattice_0.20-35     colorspace_1.3-2    stats4_3.5.1       
##  [7] htmltools_0.3.6     yaml_2.2.0          utf8_1.1.4         
## [10] rlang_0.3.0.1       later_0.7.5         pillar_1.3.1       
## [13] glue_1.3.0          withr_2.1.2         BiocGenerics_0.28.0
## [16] sp_1.3-1            bindr_0.1.1         plyr_1.8.4         
## [19] stringr_1.3.1       munsell_0.5.0       gtable_0.2.0       
## [22] htmlwidgets_1.3     evaluate_0.12       labeling_0.3       
## [25] knitr_1.21          IRanges_2.16.0      httpuv_1.4.5       
## [28] crosstalk_1.0.0     parallel_3.5.1      fansi_0.4.0        
## [31] Rcpp_1.0.0          xtable_1.8-3        geosphere_1.5-7    
## [34] promises_1.0.1      backports_1.1.2     scales_1.0.0       
## [37] S4Vectors_0.20.1    jsonlite_1.6        mime_0.6           
## [40] digest_0.6.18       stringi_1.2.4       shiny_1.2.0        
## [43] rprojroot_1.3-2     grid_3.5.1          cli_1.0.1          
## [46] tools_3.5.1         magrittr_1.5        lazyeval_0.2.1     
## [49] tibble_1.4.2        crayon_1.3.4        tidyr_0.8.2        
## [52] pkgconfig_2.0.2     assertthat_0.2.0    rmarkdown_1.10     
## [55] R6_2.3.0            compiler_3.5.1