【问题标题】:Extract column from text in memory从内存中的文本中提取列
【发布时间】:2021-12-01 18:57:04
【问题描述】:

我正在寻找一种从制表符分隔的文本中读取单列的快速方法,该文本作为字符向量存在于内存中。

我正在使用特定于我的领域的文件格式,它大致类似于压缩的 tsv 文件。从此类文件中读取行的子集既快速又容易,但由于内存限制(而且我需要的行不知道先验)。

所以,我最终在内存中得到了一个字符向量,每一行都有一个元素,但制表符分隔符仍在其中。我对如何从该文本中快速提取特定列感到有些困惑。在下面的示例中,提取第三列的最快方法是什么?文本中没有任何“惊喜”,例如 cmets 或引用名称,但在我的实际情况中,列没有固定宽度。目前我发现最快的方法是使用readr::read_tsv() 函数。

library(readr)

set.seed(0)
# About 88Mb of memory
n_examples <- 1e6
text <- paste(
  as.character(as.hexmode(sample(n_examples))),
  as.character(as.hexmode(sample(n_examples))),
  as.character(as.hexmode(sample(n_examples))),
  as.character(as.hexmode(sample(n_examples))),
  sep = "\t"
)

fun_read.table <- function(x, i) {
  read.table(
    text = x, sep = "\t", 
    colClasses = c("character", "character", "character", "character")
  )[[i]]
}

fun_read_tsv <- function(x, i) {
  read_tsv(file = I(x), col_select = all_of(i), 
           col_types = "cccc", col_names = LETTERS[1:4])[[1]]
}

bm <- bench::mark(
  fun_read.table(text, 3),
  fun_read_tsv(text, 3), 
  min_iterations = 5
)
#> Warning: Some expressions had a GC in every iteration; so filtering is disabled.

print(bm)
#> # A tibble: 2 x 13
#>   expression                   min   median `itr/sec` mem_alloc `gc/sec` n_itr
#>   <bch:expr>              <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl> <int>
#> 1 fun_read.table(text, 3)    1.34s    1.57s     0.619    93.2MB    1.11      5
#> 2 fun_read_tsv(text, 3)   879.36ms 903.72ms     1.10     35.1MB    0.219     5
#> # ... with 6 more variables: n_gc <dbl>, total_time <bch:tm>, result <list>,
#> #   memory <list>, time <list>, gc <list>

以下是我尝试过的一些替代方案,但速度并不比read_tsv() 快。 data.table::fread() 方法非常慢,因为它首先将输入文本写入临时文件。我还没有想出一个基于正则表达式的方法来捕获第三列,所以我不知道这是否会更快。

library(data.table)
#> Warning: package 'data.table' was built under R version 4.1.1

fun_tstrsplit <- function(x, i) {
  tstrsplit(x, "\t", keep = i)[[1]]
}

fun_fread <- function(x, i) {
  fread(
    text = x, sep = "\t",
    colClasses = c("character", "character", "character", "character"),
    select = i
  )[[1]]
}

fun_scan <- function(x, i) {
  ncols <- lengths(regmatches(x[[1]], gregexpr("\t", x[[1]]))) + 1
  scan(
    text = x, sep = "\t", what = "", quiet = TRUE
  )[seq_along(x) %% ncols == i]
}

reprex package (v2.0.1) 于 2021 年 10 月 13 日创建

【问题讨论】:

  • 您能详细说明您尝试了哪个data.table 版本吗?我试图重现,但即使使用 setDTthreads(1L) fread 对我来说也比 read_tsv 快。
  • 我尝试了 v1.14.2,在我的机器上,无论使用 6 个线程还是 1 个线程,它的中位数都需要约 6.9 秒。在我的 linux 机器上需要 5.1 秒。您是否碰巧有一个 SSD,写入文件的速度比我在 HDD 上的速度快?
  • 没错,我有一个 SSD。您可以尝试在启动 R 之前设置TMPDIR=/dev/shm 以将文件转储到内存磁盘而不是硬盘上。

标签: r readr


【解决方案1】:

使用 Rcpp 编写的定制函数对我来说在 Teunbrand 中运行得最快(比 read_tsv 快两倍多),并且使用了 read_tsv 大约四分之一的内存分配,尽管它涉及一些复制和可能会被优化。

我也包含了一个使用 sub 的版本,但这比 read_tsv 慢,尽管它不需要太多内存。

Rcpp::cppFunction("

std::vector<std::string> fun_rcpp(CharacterVector a, int col) {
  if(col < 1) Rcpp::stop(\"col must be a positive integer\");
  std::vector<std::string> b = Rcpp::as<std::vector<std::string>>(a);
  std::vector<std::string> result(a.size());
  for(uint32_t i = 0; i < a.size() ; i++)
  {
    int n_tabs = 0;
    std::string entry = \"\";
    for(uint16_t j = 0; j < b[i].size(); j++)              
    {
      if(n_tabs == (col - 1) & b[i][j] != '\\t') entry.push_back(b[i][j]);
      if((b[i][j]) == '\\t') n_tabs++;
      if(n_tabs == col) break;
    }
    result[i] = entry;
  }
  return result;
}

")

fun_sub <- function(x, i)
{
  s <- paste0("^", paste0(rep(".*?\t", i - 1), collapse = ""), "(.*?)\t.*$")
  sub(s, "\\1", x)
}

这些函数都给出了预期的输出:

identical(fun_read_tsv(text, 3), fun_rcpp(text, 3))
#> [1] TRUE

identical(fun_read_tsv(text, 3), fun_sub(text, 3))
#> [1] TRUE

这里显示了基准以进行比较:

bench::mark(
  fun_read.table(text, 3),
  fun_read_tsv(text, 3),
  fun_sub(text, 3),
  fun_rcpp(text, 3),
  min_iterations = 5
)

#> # A tibble: 4 x 13
#>   expression                   min   median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc
#>   <bch:expr>              <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl> <int> <dbl>
#> 1 fun_read.table(text, 3)    1.35s    1.35s     0.738   93.23MB    5.17      1     7
#> 2 fun_read_tsv(text, 3)   788.86ms 792.35ms     1.26    36.04MB    0.314     4     1
#> 3 fun_sub(text, 3)           1.27s    1.29s     0.777    7.63MB    0.194     4     1
#> 4 fun_rcpp(text, 3)       379.02ms 381.17ms     2.62     7.63MB    0.655     4     1
#> # ... with 5 more variables: total_time <bch:tm>, result <list>, memory <list>,
#   time <list>, gc <list>

请注意,Rcpp 函数的行为与预期的差不多,如果您提供小于 1 的列号或使用错误的变量类型来选择列,则会发出适当的错误。但是,如果您选择的列号大于当前的列数,它将返回一个空字符串向量,而不是引发错误。如果您想要在这里有不同的行为,例如错误或NA 的向量,您可以轻松地为 C++ 函数编写 R 包装器

【讨论】:

  • 感谢艾伦,这很棒而且速度更快!也很高兴看到如此清晰的正则表达式选项!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-02-05
  • 1970-01-01
  • 2014-03-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多