在 R 中快速高效地提取和比较子字符串

Fast and efficient substring extraction and comparison in R

提问人:Nils R 提问时间:10/10/2023 最后编辑:Nils R 更新时间:10/10/2023 访问量:125

问:

我有一个问题,即在我的数据集中两个字符串的子字符串之间非常快速有效地进行比较,尽管有非常强大的机制,但它的运行速度不够快。 我有一个 2 列和大约 15 亿行,它具有以下结构:data.table

library(data.table)
library(stringr)
library(stringi)
library(stringdist)

dt <- data.frame(c("002134", "024345", "176234"), c("002003", "024234", "002004"))
colnames(dt) <- c("class1", "class2")
setDT(dt)

我想要的是一个函数,它 (1) 从两个向量的每个字符串中逐行提取前 3 位数字,(2) 比较两个向量之间的子字符串,以及 (3) 创建一个新的布尔变量来报告两个子字符串是否相等。

因此,期望的结果如下:

dt$sameclass <- c(TRUE, TRUE, FALSE)
print(dt)
   class1 class2 sameclass
1: 002134 002003      TRUE
2: 024345 024234      TRUE
3: 176234 002004     FALSE

我已经尝试了功能和功能之外的版本。为了比较我使用的子字符串,因为根据我的理解,可以并行化,这对我的服务器非常有益。但是,瓶颈似乎仍然是子字符串提取。stringrstringidata.tablestringdist


#stringi + stringdist without data.table: 
dt$redclass1 <- stri_sub(dt$class1, to = 3)
dt$redclass2 <- stri_sub(dt$class2, to = 3)
dt[, classdist := stringdist(a = redclass1, b = redclass2, method = "hamming")] 
dt[, sameclass := (classdist == 0)] 

#stringi + stringdist within data.table: 
dt[, classdist := stringdist(a = stri_sub(dt$class1, to = 3), b = stri_sub(dt$class2, to = 3), method = "hamming")] 
dt[, sameclass := (classdist == 0)] 

#stringr with separate function: 
sameclass <- function(subclass1, subclass2, classdepth){
  truthvalue <- (str_sub(subclass1, end = classdepth) == str_sub(subclass2, end = classdepth))
  return(truthvalue)
} 
dt[, sameclass := sameclass(subclass1 = class1, subclass2 = class2, classdepth = 3), by = seq_len(nrow(dt))]

所有版本要么遇到内存问题,要么需要几个小时到一天才能运行。由于我需要反复执行此操作,这对我不起作用,我想问一下您是否可以想出更快/更有效的方法。任何帮助将不胜感激!

编辑

我已经对这里建议的一些方法进行了基准测试,这些方法确实显示出显着的加速:

dt <- data.frame(rep(c("002134", "024345", "176234"), 1000), rep(c("002003", "024234", "002004"), 1000))
colnames(dt) <- c("class1", "class2")
setDT(dt)

times <- microbenchmark(

startswithtest = dt[, startsWith(class2, substring(class1, 1, 3))],
lapplytest = dt[, do.call(`==`, lapply(.SD, substring, 1, 3)), .SDcols = c("class1", "class2")],
numerictest = dt[, as.numeric(class1)%/%1000 == as.numeric(class2)%/%1000],
functiontest = dt[, sameclass(subclass1 = class1, subclass2 = class2, classdepth = 3), by = seq_len(nrow(dt))],
stringitest = dt[, stringdist(a = stri_sub(dt$class1, to = 3), b = stri_sub(dt$class2, to = 3), method = "hamming")], 
times = 50
)
times
           expr       min        lq       mean     median         uq        max neval
 startswithtest   312.501   356.901   593.4530   444.8515    737.301   1692.602    50
     lapplytest   383.602   439.201   736.3512   522.7010    966.901   2259.601    50
    numerictest  1763.100  1932.600  3229.6651  2399.7510   4153.201   8396.301    50
   functiontest 45677.700 61124.002 81567.9409 77844.5510 100084.901 133921.502    50
    stringitest   794.201  1028.200  1423.5289  1259.6005   1739.400   3640.701    50

我现在将使用 startsWith,因为它似乎提供了最高的速度(不幸的是,由于服务器的限制,我无法使用 C 函数)。感谢您的帮助!

R 子字符串 内存效率高 的 stringdist

评论


答:

3赞 ThomasIsCoding 10/10/2023 #1

也许你可以试试

> dt[, sameclass := do.call(`==`, lapply(.SD, substring, 1, 3))][]
   class1 class2 sameclass
1: 002134 002003      TRUE
2: 024345 024234      TRUE
3: 176234 002004     FALSE

或者可能更快

> dt[, sameclass := startsWith(class2, substring(class1, 1, 3))][]
   class1 class2 sameclass
1: 002134 002003      TRUE
2: 024345 024234      TRUE
3: 176234 002004     FALSE
4赞 Onyambu 10/10/2023 #2

如果您仍在寻找速度,请考虑使用 c++ for 循环

Rcpp::cppFunction("
std::vector<bool> compare(std::vector<std::string> x, std::vector<std::string> y){
   std::vector<bool> b;
   for (std::size_t i = 0; i < x.size();i++)
       b.push_back(x[i].substr(0,3) == y[i].substr(0,3));
 return b;
}")
dt[, sameclass:=compare(class1, class2)][]
   class1 class2 sameclass
1: 002134 002003      TRUE
2: 024345 024234      TRUE
3: 176234 002004     FALSE

编辑

由于字符串是数字,因此可以进行整数除法:

dt[, sameclass:=as.numeric(class1)%/%1000 == as.numeric(class2)%/%1000][]
   class1 class2 sameclass
1: 002134 002003      TRUE
2: 024345 024234      TRUE
3: 176234 002004     FALSE
5赞 Robert Hacken 10/10/2023 #3

既然我们谈论的是速度,那么基准测试可能会很方便。我比较了 @Nils R 的一种方法,包括 @ThomasIsCoding 和 @Onyambu 的方法,以及 @Onyambu 的 C++ 函数的略微优化版本(基本上重新实现了 Thomas 的方法):startsWith

Rcpp::cppFunction("
std::vector<bool> compare2(std::vector<std::string> x, std::vector<std::string> y){
    std::vector<bool> b;
    for (std::size_t i = 0; i < x.size(); ++i) {
        for (std::size_t j = 0; j < 3; ++j)
            if (x[i][j] != y[i][j]) {
                b.push_back(false);
                break;
            } else if (j == 2)
                b.push_back(true);
    }
    return b;
}")

数据和基准设置:

n <- 100000
dt <- data.frame(rep(c("002134", "024345", "176234"), n), 
                 rep(c("002003", "024234", "002004"), n))
colnames(dt) <- c("class1", "class2")
setDT(dt)
dt1 <- copy(dt)

microbenchmark::microbenchmark(
  nils.stringdist = {
    dt[, classdist := stringdist(a = stri_sub(class1, to = 3), 
                                 b = stri_sub(class2, to = 3), method = "hamming")] 
    dt[, sameclass := (classdist == 0)]},
  thomas.eq = {
    dt1[, sameclass := do.call(`==`, lapply(.SD, substring, 1, 3))]
    dt1[, sameclass := NULL]},
  thomas.startsWith = dt[, sameclass := startsWith(class2, substring(class1, 1, 3))],
  onyambu.cpp = dt[, sameclass := compare(class1, class2)],
  onyambu.numeric = dt[, sameclass := 
                         as.numeric(class1) %/% 1000 == as.numeric(class2) %/% 1000],
  cpp2 = dt[, sameclass := compare2(class1, class2)]
)

结果如下:

Unit: milliseconds
              expr      min        lq      mean    median        uq      max neval cld
   nils.stringdist  66.3890  70.08755  80.65836  75.80350  86.11460 133.0720   100  b 
         thomas.eq  35.2667  37.75490  43.33864  39.82885  44.83400  92.9219   100 a  
 thomas.startsWith  24.1990  26.61705  34.60704  29.93990  34.12790 124.3365   100 a  
       onyambu.cpp  30.5307  32.63700  36.83997  34.12350  37.55005 156.4901   100 a  
   onyambu.numeric 271.1953 297.88525 324.14725 310.84680 331.45795 806.8805   100   c
              cpp2  26.8051  28.00815  35.86227  29.96770  32.19430 428.3867   100 a  
 

自定义 C++ 函数似乎并没有明显提高效率,因为没有与 / 的使用相关的性能瓶颈。该方法在模拟中系统地更快,但由于字符串长度较短,因此速度并不明显。substringstartsWithstartsWith

评论

1赞 Nils R 10/10/2023
谢谢你,我只是在编辑我的帖子,所以我没有看到你的答案,但这更有帮助,因为它包括 C 版本!+1 来自我