【问题标题】:How can I match fuzzy match strings from two datasets?如何匹配来自两个数据集的模糊匹配字符串?
【发布时间】:2014-12-11 21:47:52
【问题描述】:

我一直在研究一种基于不完美字符串(例如公司名称)连接两个数据集的方法。过去我必须匹配两个非常脏的列表,一个列表包含姓名和财务信息,另一个列表包含姓名和地址。两者都没有可匹配的唯一 ID! 假设已经进行了清洁,可能有错别字和插入。

到目前为止,AGREP 是我发现的最接近的工具。我可以在 AGREP 包中使用 levenshtein 距离,它测量两个字符串之间的删除、插入和替换的数量。 AGREP 将返回距离最小(最相似)的字符串。

但是,我一直无法将此命令从单个值应用到整个数据框。我粗略地使用了一个 for 循环来重复 AGREP 函数,但一定有更简单的方法。

见以下代码:

a<-data.frame(name=c('Ace Co','Bayes', 'asd', 'Bcy', 'Baes', 'Bays'),price=c(10,13,2,1,15,1))
b<-data.frame(name=c('Ace Co.','Bayes Inc.','asdf'),qty=c(9,99,10))

for (i in 1:6){
    a$x[i] = agrep(a$name[i], b$name, value = TRUE, max = list(del = 0.2, ins = 0.3, sub = 0.4))
    a$Y[i] = agrep(a$name[i], b$name, value = FALSE, max = list(del = 0.2, ins = 0.3, sub = 0.4))
}

【问题讨论】:

  • 根据大家的反馈和我的一些意见,我创建了一个函数来解决我的确切问题。代码可以在这里找到:github.com/Adamishere/Fuzzymatching/blob/master/…
  • 感谢这个功能。这非常有用。但是我无法在 string1、string2 和 id2 中传递我的列。我的数据在 data.table 中,所以不确定调用函数时应该如何传递它们。你能建议一下吗?抱歉,如果我的问题非常基础,我已经开始学习 R,还有很长的路要走
  • 我只会使用data.frame(),然后一旦匹配完成,转换为data.table()
  • fuzzyjoin 包可能会有所帮助 - 请参阅下面的答案,使用fuzzyjoin::stringdist_left_join
  • 如果没有简单的 1 变量数据框,该函数如何工作?当我有 2 个数据框,每个数据框有多个列时,它在我的情况下不起作用。

标签: r string-matching fuzzy-search fuzzy-comparison


【解决方案1】:

这是使用fuzzyjoin 包的解决方案。它使用dplyr-like 语法和stringdist 作为可能的模糊匹配类型之一。

作为@C8H10N4O2 的suggestedstringdist method="jw" 为您的示例创建了最佳匹配。

作为fuzzyjoin 的开发者@dgrtwo 的suggested,我使用了一个大的max_dist,然后使用了dplyr::group_bydplyr::slice_min,以仅获得具有最小距离的最佳匹配。 (slice_min 替换了旧的 top_n,如果原始顺序很重要且不按字母顺序排列,请使用 mutate(rank = row_number(dist)) %&gt;% filter(rank == 1)


a <- data.frame(name = c('Ace Co', 'Bayes', 'asd', 'Bcy', 'Baes', 'Bays'),
                price = c(10, 13, 2, 1, 15, 1))
b <- data.frame(name = c('Ace Co.', 'Bayes Inc.', 'asdf'),
                qty = c(9, 99, 10))

library(fuzzyjoin); library(dplyr);

stringdist_join(a, b, 
                by = "name",
                mode = "left",
                ignore_case = FALSE, 
                method = "jw", 
                max_dist = 99, 
                distance_col = "dist") %>%
  group_by(name.x) %>%
  slice_min(order_by = dist, n = 1)

#> # A tibble: 6 x 5
#> # Groups:   name.x [6]
#>   name.x price     name.y   qty       dist
#>   <fctr> <dbl>     <fctr> <dbl>      <dbl>
#> 1 Ace Co    10    Ace Co.     9 0.04761905
#> 2  Bayes    13 Bayes Inc.    99 0.16666667
#> 3    asd     2       asdf    10 0.08333333
#> 4    Bcy     1 Bayes Inc.    99 0.37777778
#> 5   Baes    15 Bayes Inc.    99 0.20000000
#> 6   Bays     1 Bayes Inc.    99 0.20000000

【讨论】:

【解决方案2】:

解决方案取决于匹配 ab 的所需基数。如果是一对一,您将获得上面三个最接近的匹配项。如果是多对一,您将获得六个。

一对一案例(需要分配算法):

当我不得不这样做之前,我将其视为具有距离矩阵和分配启发式的分配问题(下面使用贪婪分配)。如果您想要“最佳”解决方案,最好使用optim

不熟悉 AGREP,但这里是使用 stringdist 作为距离矩阵的示例。

library(stringdist)
d <- expand.grid(a$name,b$name) # Distance matrix in long form
names(d) <- c("a_name","b_name")
d$dist <- stringdist(d$a_name,d$b_name, method="jw") # String edit distance (use your favorite function here)

# Greedy assignment heuristic (Your favorite heuristic here)
greedyAssign <- function(a,b,d){
  x <- numeric(length(a)) # assgn variable: 0 for unassigned but assignable, 
  # 1 for already assigned, -1 for unassigned and unassignable
  while(any(x==0)){
    min_d <- min(d[x==0]) # identify closest pair, arbitrarily selecting 1st if multiple pairs
    a_sel <- a[d==min_d & x==0][1] 
    b_sel <- b[d==min_d & a == a_sel & x==0][1] 
    x[a==a_sel & b == b_sel] <- 1
    x[x==0 & (a==a_sel|b==b_sel)] <- -1
  }
  cbind(a=a[x==1],b=b[x==1],d=d[x==1])
}
data.frame(greedyAssign(as.character(d$a_name),as.character(d$b_name),d$dist))

产生分配:

       a          b       d
1 Ace Co    Ace Co. 0.04762
2  Bayes Bayes Inc. 0.16667
3    asd       asdf 0.08333

我确信有一种更优雅的方式来执行贪婪赋值启发式,但上述方法对我有用。

多对一案例(不是分配问题):

do.call(rbind, unname(by(d, d$a_name, function(x) x[x$dist == min(x$dist),])))

产生结果:

   a_name     b_name    dist
1  Ace Co    Ace Co. 0.04762
11   Baes Bayes Inc. 0.20000
8   Bayes Bayes Inc. 0.16667
12   Bays Bayes Inc. 0.20000
10    Bcy Bayes Inc. 0.37778
15    asd       asdf 0.08333

编辑:使用method="jw" 来产生所需的结果。见help("stringdist-package")

【讨论】:

  • 谢谢!这很有帮助。虽然我很好奇,但在多对一的情况下,结果似乎并不正确,因为它们没有返回第一行之后的最佳匹配。
  • @Adam Lee 取决于您如何定义“最佳”匹配。有关默认距离度量的更多信息,请参阅 ?stringdist?adist。使用带有默认参数的这些函数中的任何一个,“Bayes”是一个更接近“asdf”的编辑,而不是“Bayes Inc.”。
  • 啊,我明白了!谢谢,所以这是造成这种情况的距离指标的问题。这再次非常有帮助!
  • @C8H10N4O2,我正在寻找一些关于如何从第二个数据集中获取匹配的某些特定等效列的数据的建议。我已将问题发布到 - stackoverflow.com/questions/42749447/… 如果您能提出建议,那将是很大的帮助
  • 这很有帮助 - 谢谢。我发现如果在调用 greedyAssign 函数之前过滤 d$dist ,这会进一步扩大,例如d &lt;- d[d$dist &lt; 0.2,] 。在为样本运行上面的代码(没有过滤器)之后,您通常可以选择一个粗略的截止点,超过该截止点建议的匹配不太可能有用。
【解决方案3】:

我不确定这是否对您有用,John Andrews,但它为您提供了另一种工具(来自 RecordLinkage 包)并且可能会有所帮助。

install.packages("ipred")
install.packages("evd")
install.packages("RSQLite")
install.packages("ff")
install.packages("ffbase")
install.packages("ada")
install.packages("~/RecordLinkage_0.4-1.tar.gz", repos = NULL, type = "source")

require(RecordLinkage) # it is not on CRAN so you must load source from Github, and there are 7 dependent packages, as per above

compareJW <- function(string, vec, cutoff) {
  require(RecordLinkage)
  jarowinkler(string, vec) > cutoff
}

a<-data.frame(name=c('Ace Co','Bayes', 'asd', 'Bcy', 'Baes', 'Bays'),price=c(10,13,2,1,15,1))
b<-data.frame(name=c('Ace Co.','Bayes Inc.','asdf'),qty=c(9,99,10))
a$name <- as.character(a$name)
b$name <- as.character(b$name)

test <- compareJW(string = a$name, vec = b$name, cutoff = 0.8)  # pick your level of cutoff, of course
data.frame(name = a$name, price = a$price, test = test)

> data.frame(name = a$name, price = a$price, test = test)
    name price  test
1 Ace Co    10  TRUE
2  Bayes    13  TRUE
3    asd     2  TRUE
4    Bcy     1 FALSE
5   Baes    15  TRUE
6   Bays     1 FALSE

【讨论】:

【解决方案4】:

同意上面的答案“不熟悉 AGREP,但这里是使用 stringdist 作为距离矩阵的示例。”但是从Merging Data Sets Based on Partially Matched Data Elements 附加签名函数如下会更准确,因为LV的计算是基于position/addition/deletion

##Here's where the algorithm starts...
##I'm going to generate a signature from country names to reduce some of the minor differences between strings
##In this case, convert all characters to lower case, sort the words alphabetically, and then concatenate them with no spaces.
##So for example, United Kingdom would become kingdomunited
##We might also remove stopwords such as 'the' and 'of'.
signature=function(x){
  sig=paste(sort(unlist(strsplit(tolower(x)," "))),collapse='')
  return(sig)
}

【讨论】:

    【解决方案5】:

    我在这些情况下使用lapply

    yournewvector: lapply(yourvector$yourvariable, agrep, yourothervector$yourothervariable, max.distance=0.01),
    

    然后把它写成 csv 就不是那么简单了:

    write.csv(matrix(yournewvector, ncol=1), file="yournewvector.csv", row.names=FALSE)
    

    【讨论】:

      【解决方案6】:

      模糊匹配

      Approximate String Matching 近似匹配一个字符串与另一个字符串。例如bananabananas.
      Fuzzy Matching 正在寻找字符串中的近似模式。例如bananabananas in pyjamas 内。

      Method R Implementation
      Basic Bitap≈Levenshtein b$name &lt;- lapply(b$name, agrep, a$name, value=TRUE); merge(a,b)
      Advanced ?stringdist::stringdist-metrics fuzzyjoin::stringdist_join(a, b, mode='full', by=c('name'), method='lv')
      Fuzzy Match TRE agrep2 &lt;- function(pattern, x) x[which.min(adist(pattern, x, partial=TRUE))]; b$name &lt;- lapply(b$name, agrep2, a$name); merge(a, b)

      自己跑

      # Data
      a <- data.frame(name=c('Ace Co.', 'Bayes Inc.', 'asdf'), qty=c(9,99,10))
      b <- data.frame(name=c('Ace Company', 'Bayes', 'asd', 'Bcy', 'Baes', 'Bays'), price=c(10,13,2,1,15,1))
      
      # Basic
      c <- b
      c$name.b <- c$name
      c$name <- lapply(c$name, agrep, a$name, value=TRUE)
      merge(a, c, all.x=TRUE)
      
      # Advanced
      fuzzyjoin::stringdist_join(a, b, mode='full')
      
      # Fuzzy Match
      c <- b
      c$name.b <- c$name
      c$name <- lapply(c$name, function(pattern, x) x[which.min(adist(pattern, x, partial=TRUE))], a$name)
      merge(a, c)
      

      【讨论】:

        【解决方案7】:

        这是我用来获取公司出现在列表中的次数,尽管公司名称不完全匹配,

        step.1安装语音包

        step.2 在“mylistofcompanynames”中创建一个名为“soundexcodes”的新列

        step.3使用soundex函数返回“soundexcodes”中公司名称的soundex代码

        step.4 将公司名称和相应的 soundex 代码复制到名为“companysoundexcodestrainingfile”的新文件(2 列称为“companynames”和“soundexcode”)中

        step.5 删除“companysoundexcodestrainingfile”中 soundexcodes 的重复项

        step.6浏览剩余的公司名称列表并更改您希望它出现在原始公司中的名称

        示例: Amazon Inc A625 可以是 Amazon A625 埃森哲有限公司A455可以是埃森哲A455

        step.6 通过“soundexcodes”在 companysoundexcodestrainingfile$soundexcodes 和 mylistofcompanynames$soundexcodes 之间执行 left_join 或(简单的 vlookup)

        step.7 结果应该是原始列表,其中包含一个名为“co.y”的新列,其中包含您在培训文件中留下的公司名称。

        step.8 对“co.y”进行排序并检查大多数公司名称是否正确匹配,如果正确,则将旧公司名称替换​​为通过 soundex 代码的 vlookup 给出的新名称。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2012-02-14
          • 2014-11-02
          • 2014-12-30
          • 2019-09-12
          • 1970-01-01
          • 2020-05-14
          • 1970-01-01
          相关资源
          最近更新 更多