[英]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.