r Large data.table 为什么使用正则表达式提取单词比 stringr::word 更快?

Amy*_*y M 0 performance r stringr data.table

我有一个大型 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)

Run Code Online (Sandbox Code Playgroud)

假设在我的真实数据中,我想从每个字符串中提取第一个单词,并将其放入一个名为“category”的新变量中,然后从每个字符串中提取第二个单词并将其放入一个名为“fruit_name”的新变量中。

词法上最简单的方法似乎是使用stringr::word()which 很有吸引力,因为它避免了计算复杂的正则表达式的需要:

# Add a new category column:
mydt[, category := stringr::word(desert, 1)]

# Add a new fruit name column:
mydt[, fruit_name := stringr::word(desert, 2)]

Run Code Online (Sandbox Code Playgroud)

虽然这在小数据集上工作得很好,但在我的真实数据集上却花了很长时间(我怀疑它挂起了,尽管我杀死了它并在 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)]

Run Code Online (Sandbox Code Playgroud)

谁能阐明这两种方法之间的速度差异?有趣的是,即使使用这个玩具示例,在给出结果之前运行system.time()也会stringr::word()挂起几秒钟,但这可能只是因为我的真实(大)数据集已加载到我的环境中。

是否stringr::word()以某种方式破坏了 data.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   
Run Code Online (Sandbox Code Playgroud)

Wal*_*ldi 5

这没有链接到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))
  }
} 
Run Code Online (Sandbox Code Playgroud)

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)
}
Run Code Online (Sandbox Code Playgroud)

对于单个字符串,没有太大区别:

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
Run Code Online (Sandbox Code Playgroud)

但如果你复制 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

Run Code Online (Sandbox Code Playgroud)