【问题标题】:Largest substring matching across two different data frames跨两个不同数据帧的最大子串匹配
【发布时间】:2015-07-16 06:44:56
【问题描述】:

我有两个不同的data.frames“String”和“Keywords”,如下所述。 “字符串”有 50000 行,“关键字”有 10000 行。

String
#I love New York  
#Live in Los Angeles  
#He stays in Yorkshire  
#Condo in Lowell  
# ...

Keywords 
#Ohio  
#Montreal  
#Los Vego  
#York  
#New York   
#Lowell    
#... 

结果应存储在包含“String”和“Result”列的数据框中,如下所示

Result  

#              String        Result  
#       I love New York    New York     
#   Live in Los Angeles          NA  
# He stays in Yorkshire        York  
#       Condo in Lowell      Lowell  

字符串匹配应该是准确的,但可以不区分大小写。

【问题讨论】:

  • 当 2 个大小相等的大字符串与 String 匹配时会发生什么?例如:I love New York 将匹配 New Yorklove New
  • 它仍然没有执行数据帧关键字中术语的完全匹配。例如。关键字 York 不应该返回字符串他留在约克郡。 (如果关键字 Yorkshire 存在,它应该只返回 Yorkshire)。有没有办法解决这个问题?

标签: regex r


【解决方案1】:

我认为这不是最理想的解决方案,但确实有效:

stringFrame <- data.frame(String = c("I love New York","Live in Los Angeles","He stays in Yorkshire","Condo in Lowell"),
                      stringsAsFactors = FALSE) 
 wordFrame   <- data.frame(Keywords = c("Ohio","Montreal","Los Vego","York","New York","Lowell"),
                      stringsAsFactors = FALSE)

 result <- stringFrame
 for (i in 1:dim(result)[1]){
  string = result[i,"String"]
  temp = ""
  for (word in wordFrame$Keywords){
    if (grepl(word,string,ignore.case=TRUE)){
      if (nchar(word) > nchar(temp)){
        result[i,"Result"] <- word
        temp <- word
      }
    }
  }
}

我在标题中看到您正在寻找最长的单词,所以我更新了答案。现在你总会得到

 String               Result
 I love New York    New York

【讨论】:

  • 您可以在grepl 中使用ignore.case,而不是“降低”每个单词或字符串。
  • 非常感谢@Wannes 的解决方案。但是,它仍然没有做完全匹配。例如。即使关键字是 Yorkshire,也会返回 York。有没有解决的办法?如果结果是包含您建议的所有匹配关键字的向量,也会有所帮助。
【解决方案2】:

这应该比目前显示的嵌套循环快得多。在我的机器上,没有任何并行化,它在大约 12.5 分钟内完成了 100,000 个字符串和 50,000 个单词/子字符串。

样本数据:

library("data.table")
# Downloaded from https://raw.githubusercontent.com/datasets/airport-codes/master/data/airport-codes.csv
airports <- fread("airport-codes.csv")
first_bit <- paste(c("Lives", "Works", "Plays", "Condo", "Apartment", "I love"), "in")

places <- unique(c(airports$name, airports[!municipality == "", municipality]))

set.seed(123)
strings <- data.table(
  string = paste(sample(first_bit, 1e5, TRUE),
                 sample(places, 1e5, TRUE))
)
words <- sample(places, 5e4)

基于grepl的实际套路:

system.time({
  strings[, `:=`(lower = tolower(string), result = NA_character_)]
  words <- words[order(nchar(words), words, decreasing = TRUE)]
  i <- 0
  for (x in words) {
    i <- i + 1
    if (i %% 100 == 0) cat(i, "\n")
    found <- grepl(tolower(x), strings$lower, fixed = TRUE)
    strings[found & is.na(result), result := x]
  }
  strings[, lower := NULL]
})

注意在 Windows 上,fread 和它的同类在开箱即用的 https 链接上工作,但在 linux 上,您需要使用 download.file 和适当的 curlwget 选项。

编辑 OP 现在表明他只想要整个单词匹配。这可以使用非固定匹配和正则表达式中的\b 语法来实现。然而,这也是一个更快地完成整个事情的机会。

这是一个建议的算法,在我的机器上运行不到一分钟。它在空间边界将每个字符串拆分为单词(首先将多个连续空格压缩为一个之后)。然后它计算由整个单词组成的每个可能子字符串的长度。然后按长度拆分搜索的关键字,match 可用于查找子字符串和关键字之间的精确匹配。因为关键字从大到小排序,所以它总是使用最长的可用关键字。

library("data.table")
library("stringr")
# Downloaded from https://raw.githubusercontent.com/datasets/airport-codes/master/data/airport-codes.csv
airports <- fread("airport-codes.csv")
first_bit <- paste(c("Lives", "Works", "Plays", "Condo", "Apartment", "I love"), "in")

places <- unique(c(airports$name, airports[!municipality == "", municipality]))

set.seed(123)
strings <- data.table(
  string = paste(sample(first_bit, 1e5, TRUE),
                 sample(places, 1e5, TRUE))
)
words <- sample(places, 5e4)

system.time({
  strings[, `:=`(lower = tolower(str_replace_all(string, "\\s+", " ")), result = NA_character_, str_no = .I)]
  setkey(strings, str_no)
  words_dt <- data.table(word = words[order(nchar(words), words, decreasing = TRUE)])
  words_dt[, lower := tolower(str_replace_all(word, "\\s+", " "))]
  words_dt[, nc := nchar(lower)]

  gaps <- str_locate_all(strings$string, "\\S+")
  starts <- unlist(lapply(gaps, function(x) x[, 1]))
  starts_len <- unlist(lapply(lengths(gaps)/2, seq, 1))
  dists <- lapply(seq(gaps), function(i) dist(c(gaps[[i]][, 1], nchar(strings$string[i]) + 2)) - 1)
  bits_dt <-  data.table(dist = unlist(dists), str_no = rep(strings$str_no, lengths(dists)), start = rep(starts, starts_len), key = "str_no")

  setkey(strings, str_no)
  for (len in unique(nchar(words))) {
    cat(len, "\n")
    words_right_length <- words_dt[nc == len]
    bits_right_length <- bits_dt[.(strings[is.na(result), str_no])][dist == len]
    bits_right_length[, matches := match(substr(strings[str_no, lower], start, start + dist - 1), words_right_length$lower)]
    matched <- bits_right_length[, .(first_match = na.omit(matches)[1]), by = str_no][!is.na(first_match)]
    if (nrow(matched) > 0) {
      matched[, word := words_right_length[first_match, word]]
      setkey(matched, str_no)
      strings[matched, result := word]
    }
  }

  strings[, `:=`(lower = NULL, str_no = NULL)]
})

【讨论】:

  • 感谢@NickKennedy 的解决方案,抱歉回复晚了。但是我遇到了一个小问题。该算法不进行完全匹配。例如,如果 String 是 'He living in Yorkshire' 并且 Keyword 列表包含 York,它会返回 York 作为它不应该的 result .有没有办法解决这个问题?
  • @red 但在您的示例中,您将 York 作为他居住在约克郡的结果。您只想匹配整个单词吗?
  • 对不起...我没有注意到那个编辑。是的,我想要完全匹配的整个单词。
  • @red 试试上面的方法。它也快得多。
【解决方案3】:

您可以使用stringdist 包,它实现了最长的公共子字符串方法。 amatch 函数可用于将“单词”与您的字符串匹配:

strings <- data.frame(string=c("I love New York","Live in Los Angeles",
  "He stays in Yorkshire","Condo in Lowell"), stringsAsFactors = FALSE)
words   <-c("Ohio","Montreal","Los Vego","York",
  "New York","Lowell")

library(stringdist) 

strings$result = words[amatch(strings$string, words, method="lcs", maxDist=1E6)]

@NickK 评论此匹配 Lives in Los AngelesLos Vego。为了过滤掉这些部分匹配,你可以这样做

# filter out partial matches
match <- nchar(strings$string) - nchar(strings$result)  ==
  stringdist(strings$result, strings$string, method="lcs")
strings$result[!match] <- NA

此解决方案似乎比@NickK 的解决方案稍慢。使用他的示例数据集,上面的解决方案在我的系统上需要 486 秒,而他的需要 416 秒。

【讨论】:

  • 这不会返回 OP 想要的结果 - 它匹配 "Lives in Los Angeles""Los Vego"
  • @NickK 你是对的。我添加了两行代码来过滤掉这些部分匹配
猜你喜欢
  • 1970-01-01
  • 2018-03-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-08-12
相关资源
最近更新 更多