提问人:stats_noob 提问时间:11/9/2022 最后编辑:stats_noob 更新时间:11/17/2022 访问量:948
删除 R 中的模糊重复项
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()
我一直在尝试研究 R 中的不同方法,这些方法能够根据“模糊条件”对行进行重复数据删除。
根据这里提供的答案(查找近似重复记录的技术),我遇到了这种称为“记录链接”的方法。我在这里(https://cran.r-project.org/web/packages/RecordLinkage/vignettes/WeightBased.pdf)遇到了这个特定的教程,它可能能够执行类似的任务,但我不确定这是否适用于我正在处理的问题。
有人可以帮我确认这个记录链接教程是否实际上与我正在处理的问题相关 - 如果是这样,有人可以告诉我如何使用它吗?
例如,我想根据名称和地址删除重复项 - 并且只剩下两行(即 row1/row2 中的一行和 row3/row4 中的一行 - 无论选择哪一个都无关紧要)。
再举一个例子 - 假设我想尝试这个,并且只根据“地址”列进行重复数据删除:这也可行吗?
有人可以告诉我这是如何工作的吗?
谢谢!
注意:我听说过一些关于将 SQL JOINS 与 FUZZY JOINS 一起使用的选项(例如 https://cran.r-project.org/web/packages/fuzzyjoin/readme/README.html) - 但我不确定此选项是否也适用。
答:
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
从这里,您可以手动清除重复项,或查看结果以选择距离阈值以将行视为“重复项”。如果我们采用后一种方法:它看起来可能不是一个可靠的指标(例如,最低值之一是误报),但低于 20 的分数似乎是可靠的。然后,您可以应用此功能来筛选原始数据。name.dist
address.dist
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
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:我添加了更多信息,以回答 OP 的问题。
* StringDist 函数默认使用最佳字符串对齐方式,但可以在方法
参数中指定其他指标。
评论
address.dist
mutate()
my_data_dists$name.dist
my_data_dists$address.dist
dupes
base::agrep()
utils::adist()
对于这样的任务,我喜欢使用分而治之的策略,因为在比较更多字符串或更长的字符串时,您很快就会遇到内存问题。
包
library(tidyverse)
library(quanteda)
library(quanteda.textstats)
library(stringdist)
第 1 阶段:代币相似性
我添加了一个 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%的象征相似性,但含义却完全不同。查看您的数据集,以确定令牌的顺序是否起作用。如果您认为是这样,请继续阅读。
第 2 阶段:字符串相似性
在 stringdist 中实现的字符串相似性是距离的规范化版本。看距离,你比较的文本的长度没有作用。但是,两个字母不同的两个 4 个字母的字符串非常不同,而两个 100 个字母的字符串则不然。您的示例看起来可能不是一个大问题,但总的来说,出于这个原因,我更喜欢相似性。
然而,字符串相似性和距离的问题在于,它们的计算成本非常高。即使是几篇 100 篇短文本也会很快占用您的整个记忆。因此,您可以做的是过滤上面的结果,并仅计算已经看起来像是重复的候选者的字符串相似性:
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对象中提取 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
请注意,我根据您对示例的要求选择了截止值。您必须针对您的数据集以及可能的所有新项目进行微调。
评论
fulltext = paste(name, address)
当同时使用 address 和 name 以及 only address 时,使用 and 设置 (60%) 会得到很好的结果。如果用于较大的数据集,可能会略有不同。agrep
max.distance = list(all = 0.6)
agrep
近似字符串匹配(模糊匹配)
- 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
上一个:SQL 捆绑包产品操作
下一个:R/SQL 中每个组的累积差异
评论
refineR
postmastr