提问人:Nils R 提问时间:10/10/2023 最后编辑:Nils R 更新时间:10/10/2023 访问量:125
在 R 中快速高效地提取和比较子字符串
Fast and efficient substring extraction and comparison in R
问:
我有一个问题,即在我的数据集中两个字符串的子字符串之间非常快速有效地进行比较,尽管有非常强大的机制,但它的运行速度不够快。
我有一个 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
我已经尝试了功能和功能之外的版本。为了比较我使用的子字符串,因为根据我的理解,可以并行化,这对我的服务器非常有益。但是,瓶颈似乎仍然是子字符串提取。stringr
stringi
data.table
stringdist
#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 函数)。感谢您的帮助!
答:
也许你可以试试
> 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
如果您仍在寻找速度,请考虑使用 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
既然我们谈论的是速度,那么基准测试可能会很方便。我比较了 @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++ 函数似乎并没有明显提高效率,因为没有与 / 的使用相关的性能瓶颈。该方法在模拟中系统地更快,但由于字符串长度较短,因此速度并不明显。substring
startsWith
startsWith
评论