r 大 data.table 为什么使用正则表达式提取单词比 stringr::word 更快?
r large data.table why is extracting a word with regex faster than stringr::word?
我有一个很大的 data.table,有超过 700 万行和 38 列。其中一列是字符向量,其中包含一个长的描述性句子。我知道每句话的第一个词是一个类别,第二个词是一个名称,这两个我都需要放在两个新的列中以供以后分析。
这可能无法很好地说明时差,因为它太小了(实际上 system.time()
在这个例子中给出了 0),但这里有一个玩具字符串来说明我正在尝试的内容做:
# Load libraries:
library(data.table)
library(stringr)
# Create example character string:
x <- c("spicy apple cream", "mild peach melba", "juicy strawberry tart")
id <- c(1,2,3)
# Create dt:
mydt <- data.table(id = id, desert = x)
假设在我的真实数据中,我想从每个字符串中提取第一个单词,并将其放入一个名为 category 的新变量中,然后从每个字符串中提取第二个单词并将其放入一个名为 fruit_name.
词法上最简单的方法似乎是使用 stringr::word()
,这很有吸引力,因为它避免了计算复杂的正则表达式的需要:
# Add a new category column:
mydt[, category := stringr::word(desert, 1)]
# Add a new fruit name column:
mydt[, fruit_name := stringr::word(desert, 2)]
虽然这在小数据集上工作正常,但在我的真实数据集上它会永远持续下去(我怀疑它挂了,尽管我杀死了它并在 10 分钟后重新启动了 R)。就上下文而言,此数据集中的其他字符向量类型操作需要大约 20 秒才能完成 运行,因此似乎此函数的劳动强度和计算量特别大。
相比之下,如果我将正则表达式与 sub()
一起使用,它不会挂起,而且似乎以与其他字符向量运算大致相同的速度工作:
# Create category column with regex:
mydt[, category := sub("(^\w+).*", "\1", desert)]
# Create fruit name column with regex:
mydt[, fruit_name := sub("^\w+\s+(\w+).*", "\1", desert)]
谁能阐明这两种方法之间的速度差异?有趣的是,即使有这个玩具示例,运行ning system.time()
with stringr::word()
在给出结果之前会挂起几秒钟,但这可能只是因为我的真实(大)数据集是加载到我的环境中。
stringr::word()
是否以某种方式打破了 data.table 按引用替换的惯例(创建新列而不复制整个 table)?不知何故,我认为 sub()
会更糟,因为它可能会复制整个字符串,然后替换为与正则表达式模式匹配的位,但实际上它要快得多。
非常感谢任何见解!
> sessionInfo()
R version 4.1.2 (2021-11-01)
Platform: x86_64-w64-mingw32/x64 (64-bit)
Running under: Windows 10 x64 (build 19043)
Matrix products: default
locale:
[1] LC_COLLATE=English_United Kingdom.1252 LC_CTYPE=English_United Kingdom.1252
[3] LC_MONETARY=English_United Kingdom.1252 LC_NUMERIC=C
[5] LC_TIME=English_United Kingdom.1252
attached base packages:
[1] stats graphics grDevices utils datasets methods base
other attached packages:
[1] officer_0.4.1 flextable_0.6.9 data.table_1.14.2 lubridate_1.8.0
[5] forcats_0.5.1 stringr_1.4.0 dplyr_1.0.7 purrr_0.3.4
[9] readr_2.1.0 tidyr_1.1.4 tibble_3.1.6 ggplot2_3.3.5
[13] tidyverse_1.3.1
loaded via a namespace (and not attached):
[1] xfun_0.28 tidyselect_1.1.1 haven_2.4.3 colorspace_2.0-2
[5] vctrs_0.3.8 generics_0.1.1 htmltools_0.5.2 base64enc_0.1-3
[9] utf8_1.2.2 rlang_0.4.12 pillar_1.6.4 glue_1.5.0
[13] withr_2.4.2 DBI_1.1.1 gdtools_0.2.3 dbplyr_2.1.1
[17] uuid_1.0-3 modelr_0.1.8 readxl_1.3.1 lifecycle_1.0.1
[21] munsell_0.5.0 gtable_0.3.0 cellranger_1.1.0 zip_2.2.0
[25] rvest_1.0.2 evaluate_0.14 knitr_1.36 fastmap_1.1.0
[29] tzdb_0.2.0 fansi_0.5.0 broom_0.7.10 Rcpp_1.0.7
[33] scales_1.1.1 backports_1.3.0 jsonlite_1.7.2 fs_1.5.0
[37] systemfonts_1.0.3 digest_0.6.28 hms_1.1.1 stringi_1.7.5
[41] grid_4.1.2 cli_3.1.0 tools_4.1.2 magrittr_2.0.1
[45] crayon_1.4.2 pkgconfig_2.0.3 ellipsis_0.3.2 xml2_1.3.2
[49] reprex_2.0.1 rmarkdown_2.11 assertthat_0.2.1 httr_1.4.2
[53] rstudioapi_0.13 R6_2.5.1 compiler_4.1.2
这没有链接到 data.table
。
sub
依赖内部C代码调用:
function (pattern, replacement, x, ignore.case = FALSE, perl = FALSE,
fixed = FALSE, useBytes = FALSE)
{
if (is.factor(x) && length(levels(x)) < length(x)) {
sub(pattern, replacement, levels(x), ignore.case, perl,
fixed, useBytes)[x]
}
else {
if (!is.character(x))
x <- as.character(x)
.Internal(sub(as.character(pattern), as.character(replacement),
x, ignore.case, perl, fixed, useBytes))
}
}
而 stringr::word
依赖于多个 lapply
/vapply
/mapply
调用:
function (string, start = 1L, end = start, sep = fixed(" "))
{
n <- max(length(string), length(start), length(end))
string <- rep(string, length.out = n)
start <- rep(start, length.out = n)
end <- rep(end, length.out = n)
breaks <- str_locate_all(string, sep)
words <- lapply(breaks, invert_match)
len <- vapply(words, nrow, integer(1))
neg_start <- !is.na(start) & start < 0L
start[neg_start] <- start[neg_start] + len[neg_start] +
1L
neg_end <- !is.na(end) & end < 0L
end[neg_end] <- end[neg_end] + len[neg_end] + 1L
start[start > len] <- NA
end[end > len] <- NA
starts <- mapply(function(word, loc) word[loc, "start"],
words, start)
ends <- mapply(function(word, loc) word[loc, "end"], words,
end)
str_sub(string, starts, ends)
}
对于单个字符串,没有太大区别:
desert <-"spicy apple cream"
microbenchmark::microbenchmark(
stringr::word(desert, 1),
sub("(^\w+).*", "\1", desert))
Unit: microseconds
expr min lq mean median uq max neval
stringr::word(desert, 1) 50.3 58.35 95.816 71.80 115.35 323.8 100
sub("(^\\w+).*", "\\1", desert) 46.3 51.05 68.810 53.85 63.20 265.1 100
但是如果你复制 10^6 次,sub
会快 20 倍:
desert <- rep("spicy apple cream",10^6)
microbenchmark::microbenchmark(
stringr::word(desert, 1),
sub("(^\w+).*", "\1", desert),times=5)
Unit: milliseconds
expr min lq mean median uq max
stringr::word(desert, 1) 11605.1720 13724.731 14484.9069 14043.3454 16066.1067 16985.1798
sub("(^\\w+).*", "\\1", desert) 696.2793 752.516 771.5857 797.5788 803.7969 807.7577
对于 data.table,'stringi' 库比 'stringr' 更快,后者是一组使用 stringi 库的 tidyverse 例程。 data.table grepl 的包装器是 %ilike% 这可能更方便,但仍然比 stringi 库慢。
在下面的函数中,我搜索了大约 70 万行,每行 64 个字段,每个 VAERS 数据。我在每行的两个字段中搜索相同的术语,并将搜索结果传递给管道。整个搜索在 de-duplicate 和 dcast.
之前的 data.table 操作的 'i' (过滤器)短语中进行。
stringi 函数的速度大约是其三倍。请参阅下面的 'system.time' 个结果。结果表(未显示)return 相同的数据。没有任何复杂的分析,看起来 'stringi' 使用内存比 %ilike% grepl 包装器更有效,后者在操作中看起来更 'CPU bound' 。更多信息:
[https://rdatatable.gitlab.io/data.table/reference/like.html][1]
[https://stringr.tidyverse.org/][2]
library(data.table)
all.heart.ilike <- function()
{
all.covid.data[
(All_symptoms %ilike% "carditis"| SYMPTOM_TEXT %ilike% "carditis")|
(All_symptoms %ilike% "cardiac"| SYMPTOM_TEXT %ilike% "cardiac")|
(All_symptoms %ilike% "myocardial"| SYMPTOM_TEXT %ilike% "myocardial")|
(All_symptoms %ilike% "coronary"| SYMPTOM_TEXT %ilike% "coronary")|
(All_symptoms %ilike% "infarct"| SYMPTOM_TEXT %ilike% "infarct")|
(All_symptoms %ilike% "stroke"| SYMPTOM_TEXT %ilike% "stroke"),][
!duplicated(VAERS_ID_enhanced),.(count=.N),.(SEX,DECADE=AGE_YRS%/%10)][DECADE >= 1,
dcast(.SD,DECADE ~ SEX,value.var="count",fun.aggregate=fsum)]
}
library(stringi)
all.heart.stri <- function()
{
all.covid.data[
(stri_detect_regex(All_symptoms,"carditis",case_insensitive=TRUE) |stri_detect_regex(SYMPTOM_TEXT,"carditis",case_insensitive=TRUE))|
(stri_detect_regex(All_symptoms,"cardiac",case_insensitive=TRUE) |stri_detect_regex(SYMPTOM_TEXT,"cardiac",case_insensitive=TRUE))|
(stri_detect_regex(All_symptoms,"myocardial",case_insensitive=TRUE) |stri_detect_regex(SYMPTOM_TEXT,"myocardial",case_insensitive=TRUE))|
(stri_detect_regex(All_symptoms,"coronary",case_insensitive=TRUE) |stri_detect_regex(SYMPTOM_TEXT,"coronary",case_insensitive=TRUE))|
(stri_detect_regex(All_symptoms,"infarct",case_insensitive=TRUE) |stri_detect_regex(SYMPTOM_TEXT,"infarct",case_insensitive=TRUE))|
(stri_detect_regex(All_symptoms,"stroke",case_insensitive=TRUE) |stri_detect_regex(SYMPTOM_TEXT,"stroke",case_insensitive=TRUE)),][
!duplicated(VAERS_ID_enhanced),.(count=.N),.(SEX,DECADE=AGE_YRS%/%10)][DECADE >= 1,
dcast(.SD,DECADE ~ SEX,value.var="count",fun.aggregate=fsum)]
}
system.time(all.heart.ilike())
user system elapsed
47.67 0.05 47.75
system.time(all.heart.stri())
user system elapsed
17.12 2.56 19.73
我有一个很大的 data.table,有超过 700 万行和 38 列。其中一列是字符向量,其中包含一个长的描述性句子。我知道每句话的第一个词是一个类别,第二个词是一个名称,这两个我都需要放在两个新的列中以供以后分析。
这可能无法很好地说明时差,因为它太小了(实际上 system.time()
在这个例子中给出了 0),但这里有一个玩具字符串来说明我正在尝试的内容做:
# Load libraries:
library(data.table)
library(stringr)
# Create example character string:
x <- c("spicy apple cream", "mild peach melba", "juicy strawberry tart")
id <- c(1,2,3)
# Create dt:
mydt <- data.table(id = id, desert = x)
假设在我的真实数据中,我想从每个字符串中提取第一个单词,并将其放入一个名为 category 的新变量中,然后从每个字符串中提取第二个单词并将其放入一个名为 fruit_name.
词法上最简单的方法似乎是使用 stringr::word()
,这很有吸引力,因为它避免了计算复杂的正则表达式的需要:
# Add a new category column:
mydt[, category := stringr::word(desert, 1)]
# Add a new fruit name column:
mydt[, fruit_name := stringr::word(desert, 2)]
虽然这在小数据集上工作正常,但在我的真实数据集上它会永远持续下去(我怀疑它挂了,尽管我杀死了它并在 10 分钟后重新启动了 R)。就上下文而言,此数据集中的其他字符向量类型操作需要大约 20 秒才能完成 运行,因此似乎此函数的劳动强度和计算量特别大。
相比之下,如果我将正则表达式与 sub()
一起使用,它不会挂起,而且似乎以与其他字符向量运算大致相同的速度工作:
# Create category column with regex:
mydt[, category := sub("(^\w+).*", "\1", desert)]
# Create fruit name column with regex:
mydt[, fruit_name := sub("^\w+\s+(\w+).*", "\1", desert)]
谁能阐明这两种方法之间的速度差异?有趣的是,即使有这个玩具示例,运行ning system.time()
with stringr::word()
在给出结果之前会挂起几秒钟,但这可能只是因为我的真实(大)数据集是加载到我的环境中。
stringr::word()
是否以某种方式打破了 data.table 按引用替换的惯例(创建新列而不复制整个 table)?不知何故,我认为 sub()
会更糟,因为它可能会复制整个字符串,然后替换为与正则表达式模式匹配的位,但实际上它要快得多。
非常感谢任何见解!
> sessionInfo()
R version 4.1.2 (2021-11-01)
Platform: x86_64-w64-mingw32/x64 (64-bit)
Running under: Windows 10 x64 (build 19043)
Matrix products: default
locale:
[1] LC_COLLATE=English_United Kingdom.1252 LC_CTYPE=English_United Kingdom.1252
[3] LC_MONETARY=English_United Kingdom.1252 LC_NUMERIC=C
[5] LC_TIME=English_United Kingdom.1252
attached base packages:
[1] stats graphics grDevices utils datasets methods base
other attached packages:
[1] officer_0.4.1 flextable_0.6.9 data.table_1.14.2 lubridate_1.8.0
[5] forcats_0.5.1 stringr_1.4.0 dplyr_1.0.7 purrr_0.3.4
[9] readr_2.1.0 tidyr_1.1.4 tibble_3.1.6 ggplot2_3.3.5
[13] tidyverse_1.3.1
loaded via a namespace (and not attached):
[1] xfun_0.28 tidyselect_1.1.1 haven_2.4.3 colorspace_2.0-2
[5] vctrs_0.3.8 generics_0.1.1 htmltools_0.5.2 base64enc_0.1-3
[9] utf8_1.2.2 rlang_0.4.12 pillar_1.6.4 glue_1.5.0
[13] withr_2.4.2 DBI_1.1.1 gdtools_0.2.3 dbplyr_2.1.1
[17] uuid_1.0-3 modelr_0.1.8 readxl_1.3.1 lifecycle_1.0.1
[21] munsell_0.5.0 gtable_0.3.0 cellranger_1.1.0 zip_2.2.0
[25] rvest_1.0.2 evaluate_0.14 knitr_1.36 fastmap_1.1.0
[29] tzdb_0.2.0 fansi_0.5.0 broom_0.7.10 Rcpp_1.0.7
[33] scales_1.1.1 backports_1.3.0 jsonlite_1.7.2 fs_1.5.0
[37] systemfonts_1.0.3 digest_0.6.28 hms_1.1.1 stringi_1.7.5
[41] grid_4.1.2 cli_3.1.0 tools_4.1.2 magrittr_2.0.1
[45] crayon_1.4.2 pkgconfig_2.0.3 ellipsis_0.3.2 xml2_1.3.2
[49] reprex_2.0.1 rmarkdown_2.11 assertthat_0.2.1 httr_1.4.2
[53] rstudioapi_0.13 R6_2.5.1 compiler_4.1.2
这没有链接到 data.table
。
sub
依赖内部C代码调用:
function (pattern, replacement, x, ignore.case = FALSE, perl = FALSE,
fixed = FALSE, useBytes = FALSE)
{
if (is.factor(x) && length(levels(x)) < length(x)) {
sub(pattern, replacement, levels(x), ignore.case, perl,
fixed, useBytes)[x]
}
else {
if (!is.character(x))
x <- as.character(x)
.Internal(sub(as.character(pattern), as.character(replacement),
x, ignore.case, perl, fixed, useBytes))
}
}
而 stringr::word
依赖于多个 lapply
/vapply
/mapply
调用:
function (string, start = 1L, end = start, sep = fixed(" "))
{
n <- max(length(string), length(start), length(end))
string <- rep(string, length.out = n)
start <- rep(start, length.out = n)
end <- rep(end, length.out = n)
breaks <- str_locate_all(string, sep)
words <- lapply(breaks, invert_match)
len <- vapply(words, nrow, integer(1))
neg_start <- !is.na(start) & start < 0L
start[neg_start] <- start[neg_start] + len[neg_start] +
1L
neg_end <- !is.na(end) & end < 0L
end[neg_end] <- end[neg_end] + len[neg_end] + 1L
start[start > len] <- NA
end[end > len] <- NA
starts <- mapply(function(word, loc) word[loc, "start"],
words, start)
ends <- mapply(function(word, loc) word[loc, "end"], words,
end)
str_sub(string, starts, ends)
}
对于单个字符串,没有太大区别:
desert <-"spicy apple cream"
microbenchmark::microbenchmark(
stringr::word(desert, 1),
sub("(^\w+).*", "\1", desert))
Unit: microseconds
expr min lq mean median uq max neval
stringr::word(desert, 1) 50.3 58.35 95.816 71.80 115.35 323.8 100
sub("(^\\w+).*", "\\1", desert) 46.3 51.05 68.810 53.85 63.20 265.1 100
但是如果你复制 10^6 次,sub
会快 20 倍:
desert <- rep("spicy apple cream",10^6)
microbenchmark::microbenchmark(
stringr::word(desert, 1),
sub("(^\w+).*", "\1", desert),times=5)
Unit: milliseconds
expr min lq mean median uq max
stringr::word(desert, 1) 11605.1720 13724.731 14484.9069 14043.3454 16066.1067 16985.1798
sub("(^\\w+).*", "\\1", desert) 696.2793 752.516 771.5857 797.5788 803.7969 807.7577
对于 data.table,'stringi' 库比 'stringr' 更快,后者是一组使用 stringi 库的 tidyverse 例程。 data.table grepl 的包装器是 %ilike% 这可能更方便,但仍然比 stringi 库慢。
在下面的函数中,我搜索了大约 70 万行,每行 64 个字段,每个 VAERS 数据。我在每行的两个字段中搜索相同的术语,并将搜索结果传递给管道。整个搜索在 de-duplicate 和 dcast.
之前的 data.table 操作的 'i' (过滤器)短语中进行。stringi 函数的速度大约是其三倍。请参阅下面的 'system.time' 个结果。结果表(未显示)return 相同的数据。没有任何复杂的分析,看起来 'stringi' 使用内存比 %ilike% grepl 包装器更有效,后者在操作中看起来更 'CPU bound' 。更多信息:
[https://rdatatable.gitlab.io/data.table/reference/like.html][1] [https://stringr.tidyverse.org/][2]
library(data.table)
all.heart.ilike <- function()
{
all.covid.data[
(All_symptoms %ilike% "carditis"| SYMPTOM_TEXT %ilike% "carditis")|
(All_symptoms %ilike% "cardiac"| SYMPTOM_TEXT %ilike% "cardiac")|
(All_symptoms %ilike% "myocardial"| SYMPTOM_TEXT %ilike% "myocardial")|
(All_symptoms %ilike% "coronary"| SYMPTOM_TEXT %ilike% "coronary")|
(All_symptoms %ilike% "infarct"| SYMPTOM_TEXT %ilike% "infarct")|
(All_symptoms %ilike% "stroke"| SYMPTOM_TEXT %ilike% "stroke"),][
!duplicated(VAERS_ID_enhanced),.(count=.N),.(SEX,DECADE=AGE_YRS%/%10)][DECADE >= 1,
dcast(.SD,DECADE ~ SEX,value.var="count",fun.aggregate=fsum)]
}
library(stringi)
all.heart.stri <- function()
{
all.covid.data[
(stri_detect_regex(All_symptoms,"carditis",case_insensitive=TRUE) |stri_detect_regex(SYMPTOM_TEXT,"carditis",case_insensitive=TRUE))|
(stri_detect_regex(All_symptoms,"cardiac",case_insensitive=TRUE) |stri_detect_regex(SYMPTOM_TEXT,"cardiac",case_insensitive=TRUE))|
(stri_detect_regex(All_symptoms,"myocardial",case_insensitive=TRUE) |stri_detect_regex(SYMPTOM_TEXT,"myocardial",case_insensitive=TRUE))|
(stri_detect_regex(All_symptoms,"coronary",case_insensitive=TRUE) |stri_detect_regex(SYMPTOM_TEXT,"coronary",case_insensitive=TRUE))|
(stri_detect_regex(All_symptoms,"infarct",case_insensitive=TRUE) |stri_detect_regex(SYMPTOM_TEXT,"infarct",case_insensitive=TRUE))|
(stri_detect_regex(All_symptoms,"stroke",case_insensitive=TRUE) |stri_detect_regex(SYMPTOM_TEXT,"stroke",case_insensitive=TRUE)),][
!duplicated(VAERS_ID_enhanced),.(count=.N),.(SEX,DECADE=AGE_YRS%/%10)][DECADE >= 1,
dcast(.SD,DECADE ~ SEX,value.var="count",fun.aggregate=fsum)]
}
system.time(all.heart.ilike())
user system elapsed
47.67 0.05 47.75
system.time(all.heart.stri())
user system elapsed
17.12 2.56 19.73