fuzzyjoin
パッケージについて知ったのは r-wakalang で質問させていただいたことが縁になります。yutannihilation=サンにパッケージを教えていただいたのですが、本パッケージ、なぜか使ってみた系記事すらあまり引っかからないとのこと。これも縁で私が書こうと思っていたのですが、多忙から曖昧な関係にしていたのをこの気に成仏させようというのが今回の記事の背景です。本記事は Rアドベントカレンダー 23日目となります。
fuzzyjoin
パッケージはR前処理の心の友であるdplyr
の機能を補うパッケージです。 CRANに挙がっている一行パッケージ内容紹介には “Join Tables Together on Inexact Matching” とあり、完全には一致しないIDをキーにして、データをjoinするためのパッケージだとわかります。
完全に一致しないIDとはどのようなものでしょうか。例えば誤記などがあるでしょう。fuzzyjoin
パッケージには代表的なスペルミスデータを集めたmisspellings
データが含まれています (abboutとaboutなど)。このようなスペルミスをkey
にしてjoin
すると、考えているのとは違うデータしか得られないことでしょう。 fuzzyjoin
パッケージの関数には上記のようなミススペルのためだけではなく、目的に応じて以下のような関数が用意されています。
difference_hoge_join
: 数値データ対象。値にちょっとしたブレがある時に使う。distance_hoge_join
:複数列の距離に基づきデータをつなぎたい。stringdist_hoge_join
: 文字列をコサイン距離などに基づいて操作したい。regex_hoge_join
: 正規表現に一致するやつを取り扱いたい。geo_hoge_join
: 緯度経度にブレがあるけどつなぎたい。interval_hoge_join
: 一定範囲の値のやつをつなぎたい。genome_hoge_join
: intervalをゲノム向けにしたやつ? chromosome IDとかに使う。interval_hoge_join
, regex_hoge_join
についてはyutannihilation=サンが以前の記事、 fuzzyjoinパッケージでいい感じにjoin で例をあげてくれているので、これらの紹介についてはリンクを張って済ませます。
hoge部分にはdplyr
でよく使うようなinner
, left
, right
, full
, semi
, anti
が入ります。
見てるだけじゃ全くわからんのでそれぞれ具体的に見ていきましょう。 余裕がなかったのでパッケージのexamplesばっかりで申し訳ないです。。。
何はともあれパッケージを読み込んでおきましょう。未インストールの人は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.Length
をkey
にして、新しい変数Type
をjoin
することを考えます。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, ])
ブレの大きさの範囲内に入った値でType
が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
: 文字列をコサイン距離などに基づいて操作したい。” を使ってみましょう。データは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 package のstringdist-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
: 緯度経度にブレがあるけどつなぎたい。” です。 今回は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
: 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