![](/img/trans.png)
[英]Fuzzy matching duplicates in postgresql and move the duplicates to a new table
[英]Removing Fuzzy Duplicates in R
我在 R 中有一個看起來像這樣的數據集:
address = c("882 4N Road River NY, NY 12345", "882 - River Road NY, ZIP 12345", "123 Fake Road Boston Drive Boston", "123 Fake - Rd Boston 56789")
name = c("ABC Center Building", "Cent. Bldg ABC", "BD Home 25 New", "Boarding Direct 25")
my_data = data.frame(address, name)
address name
1 882 4N Road River NY, NY 12345 ABC Center Building
2 882 - River Road NY, ZIP 12345 Cent. Bldg ABC
3 123 Fake Road Boston Drive Boston BD Home 25 New
4 123 Fake - Rd Boston 56789 Boarding Direct 25
看這個數據,很明顯前兩行是一樣的,后兩行也是一樣的。 但是,如果您嘗試直接刪除重復項,標准函數(例如“ distinct()
”)將 state 認為此數據集中沒有重復項,因為所有行都有一些獨特的元素。
我一直在嘗試研究 R 中能夠基於“模糊條件”刪除重復行的不同方法。
根據此處提供的答案( 查找近似重復記錄的技術),我遇到了這種稱為“記錄鏈接”的方法。 我在這里遇到了這個特定的教程 ( https://cran.r-project.org/web/packages/RecordLinkage/vi.nettes/WeightBased.pdf ),它可能能夠執行類似的任務,但我不確定如果這是針對我正在處理的問題。
有人可以幫我確認這個 Record Linkage 教程是否真的與我正在處理的問題相關 - 如果是這樣,有人可以告訴我如何使用它嗎?
例如,我想根據名稱和地址刪除重復項 - 並且只剩下兩行(即 row1/row2 中的一行和 row3/row4 中的一行 - 選擇哪一個並不重要)。
作為另一個例子 - 假設我想嘗試這個並且只根據“地址”列進行重復數據刪除:這也可能嗎?
有人可以告訴我這是如何工作的嗎?
謝謝!
注意:我聽說過一些關於使用 SQL JOINS 和 FUZZY JOINS 的選項(例如https://cran.r-project.org/web/packages/fuzzyjoin/readme/README.html )——但我不確定這個選項是否正確也適合。
對於這樣的任務,我喜歡使用分而治之的策略,因為您很快就會遇到 memory 問題,比較大量的字符串或更長的字符串。
library(tidyverse)
library(quanteda)
library(quanteda.textstats)
library(stringdist)
我添加了一個 ID 列並將名稱和地址組合成全文以進行比較。
my_data2 <- my_data|>
mutate(ID = factor(row_number()),
fulltext = paste(name, address))
在quanteda
中,相似性方法是在比較兩個字符串中哪些標記相同之前將字符串划分為單詞/標記。 與字符串距離相比,這是非常有效的:
duplicates <- my_data2 |>
# a bunch of wrangling to create the quanteda dfm object
corpus(docid_field = "ID",
text_field = "fulltext") |>
tokens() |>
dfm() |>
# calculate similarity using cosine (other methods are available)
textstat_simil(method = "cosine") |>
as_tibble() |>
# attaching the original documents back to the output
left_join(my_data2, by = c("document1" = "ID")) |>
left_join(my_data2, by = c("document2" = "ID"), suffix = c("", "_comparison"))
duplicates |>
select(cosine,
address, address_comparison,
name, name_comparison)
#> # A tibble: 5 × 5
#> cosine address address_comparison name name_…¹
#> <dbl> <chr> <chr> <chr> <chr>
#> 1 0.641 882 4N Road River NY, NY 12345 882 - River Road NY, Z… ABC … Cent. …
#> 2 0.0801 882 4N Road River NY, NY 12345 123 Fake Road Boston D… ABC … BD Hom…
#> 3 0.0833 882 - River Road NY, ZIP 12345 123 Fake Road Boston D… Cent… BD Hom…
#> 4 0.0962 882 - River Road NY, ZIP 12345 123 Fake - Rd Boston 5… Cent… Boardi…
#> 5 0.481 123 Fake Road Boston Drive Boston 123 Fake - Rd Boston 5… BD H… Boardi…
#> # … with abbreviated variable name ¹name_comparison
可以看到,第一條和第二條以及第三條和第四條的相似度分別為 0.641 和 0.481。 在大多數情況下,這種比較已經足以識別重復項。 但是,它完全忽略了詞序。 典型的例子是“狗咬人”和“人咬狗”的象征相似度為 100%,但意義完全不同。 查看您的數據集以確定標記的順序是否起作用。 如果您認為可以,請繼續閱讀。
在 stringdist 中實現的字符串相似度是距離的標准化版本。 查看距離,您比較的文本長度沒有任何作用。 但是,兩個字母不同的兩個 4 個字母的字符串非常不同,而兩個 100 個字母的字符串則不同。 您的示例看起來可能不是什么大問題,但總的來說,出於這個原因,我更喜歡相似性。
然而,字符串相似性和距離的問題在於它們的計算成本非常高。 即使是 100 條短文本也能很快占據你的整個 memory。所以你可以做的是過濾上面的結果,只計算看起來已經重復的候選字符串的相似度:
duplicates_stringsim <- duplicates |>
filter(cosine > 0.4) |>
mutate(stringsim = stringsim(fulltext, fulltext_comparison, method = "lv"))
duplicates_stringsim |>
select(cosine, stringsim,
address, address_comparison,
name, name_comparison)
#> # A tibble: 2 × 6
#> cosine stringsim address address_com…¹ name name_…²
#> <dbl> <dbl> <chr> <chr> <chr> <chr>
#> 1 0.641 0.48 882 4N Road River NY, NY 12345 882 - River … ABC … Cent. …
#> 2 0.481 0.354 123 Fake Road Boston Drive Boston 123 Fake - R… BD H… Boardi…
#> # … with abbreviated variable names ¹address_comparison, ²name_comparison
為了比較,我們已經排除的其他三個比較的stringsim分別是0.2、0.208和0.133。 盡管有點小,但字符串的相似性證實了第 1 階段的結果。
現在最后一步是從原始 data.frame 中刪除重復項。 為此,我使用另一個過濾器,從 duplicates_stringsim object 中提取 ID,然后從數據中刪除這些重復項。
dup_ids <- duplicates_stringsim |>
filter(stringsim > 0.3) |>
pull(document2)
my_data2 |>
filter(!ID %in% dup_ids)
#> address name ID
#> 1 882 4N Road River NY, NY 12345 ABC Center Building 1
#> 2 123 Fake Road Boston Drive Boston BD Home 25 New 3
#> fulltext
#> 1 ABC Center Building 882 4N Road River NY, NY 12345
#> 2 BD Home 25 New 123 Fake Road Boston Drive Boston
創建於 2022-11-16,使用reprex v2.0.2
請注意,我根據您對示例的要求選擇了截止值。 您將必須為您的數據集和可能的所有新項目微調這些。
stringdist::stringdist()
可用於查找近似重復項,至少在相對簡單的情況下是這樣。
使用您的示例數據,我們可以執行笛卡爾自連接以獲取所有行組合; 使用stringdist::stringdist()
計算address
和name
的所有行對的距離*; 並首先排列最相似的行對:
library(dplyr)
library(tidyr)
library(stringdist)
my_data_dists <- my_data %>%
mutate(row = row_number()) %>%
full_join(., ., by = character()) %>%
filter(row.x < row.y) %>%
mutate(
address.dist = stringdist(address.x, address.y),
name.dist = stringdist(name.x, name.y)
) %>%
arrange(scale(address.dist) + scale(name.dist)) %>%
relocate(
row.x, row.y,
address.dist, name.dist,
address.x, address.y,
name.x, name.y
)
row.x row.y address.dist name.dist address.x address.y name.x name.y
1 1 2 13 13 882 4N Road River NY, NY 12345 882 - River Road NY, ZIP 12345 ABC Center Building Cent. Bldg ABC
2 3 4 15 16 123 Fake Road Boston Drive Boston 123 Fake - Rd Boston 56789 BD Home 25 New Boarding Direct 25
3 2 3 25 13 882 - River Road NY, ZIP 12345 123 Fake Road Boston Drive Boston Cent. Bldg ABC BD Home 25 New
4 1 3 25 15 882 4N Road River NY, NY 12345 123 Fake Road Boston Drive Boston ABC Center Building BD Home 25 New
5 2 4 23 17 882 - River Road NY, ZIP 12345 123 Fake - Rd Boston 56789 Cent. Bldg ABC Boarding Direct 25
6 1 4 25 18 882 4N Road River NY, NY 12345 123 Fake - Rd Boston 56789 ABC Center Building Boarding Direct 25
從這里,您可以手動清除重復項,或觀察結果以選擇一個距離閾值來考慮行“重復項”。 如果我們采用后一種方法:看起來name.dist
可能不是一個可靠的指標(例如,最低值之一是誤報),但address.dist
分數低於 20 似乎是可靠的。 然后您可以應用它來過濾您的原始數據。
dupes <- my_data_dists$row.y[my_data_dists$address.dist < 20]
my_data[-dupes,]
address name
1 882 4N Road River NY, NY 12345 ABC Center Building
3 123 Fake Road Boston Drive Boston BD Home 25 New
對於更復雜的情況(例如,更多列、非常大的數據集),您最好使用 RecordLinkage 或評論中的其他一些建議。 但我發現 stringdist 非常靈活,對於只涉及幾列的情況很有幫助。
編輯:另一個接口由stringdist::stringdistmatrix()
或utils::adist()
提供,它返回dist
object 或一個或兩個向量的元素之間的距離matrix
:
stringdistmatrix(my_data$name)
# 1 2 3
# 2 13
# 3 15 13
# 4 18 17 16
adist(my_data$name)
# [,1] [,2] [,3] [,4]
# [1,] 0 13 15 18
# [2,] 13 0 13 17
# [3,] 15 13 0 16
# [4,] 18 17 16 0
編輯 2:我在gist中添加了一些更多信息來回應 OP 的問題。
* stringdist 函數默認使用最佳字符串 alignment ,但可以在method
參數中指定其他指標。
使用agrep
和max.distance = list(all = 0.6)
(60%) 的設置在同時使用地址和名稱以及僅使用地址時會產生良好的結果。 如果用於較大的數據集,可能會略有不同。
協議
近似字符串匹配(模糊匹配)
- max.distance:匹配允許的最大距離。
'all':所有轉換(插入、刪除和替換)的最大數量/分數
過濾唯一性,保留第一個條目(可以調整為使用最長的條目等)。
my_data[unique(sapply(paste(my_data$address, my_data$name), function(x)
agrep(x, paste(my_data$address, my_data$name),
max.distance = list(all = 0.6))[1])),]
address name
1 882 4N Road River NY, NY 12345 ABC Center Building
3 123 Fake Road Boston Drive Boston BD Home 25 New
僅使用地址
my_data[unique(sapply(my_data$address, function(x)
agrep(x, my_data$address, max.distance = list(all = 0.6))[1])),]
address name
1 882 4N Road River NY, NY 12345 ABC Center Building
3 123 Fake Road Boston Drive Boston BD Home 25 New
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.