Извлечь столбец из текста в памяти

r #readr

#r #readr

Вопрос:

Я ищу быстрый способ чтения в одном столбце из текста, разделенного табуляцией, который хранится в виде символьного вектора в памяти.

Я использую формат файла, специфичный для моего поля, который примерно напоминает сжатый файл tsv. Это быстро и легко читать в подмножестве строк из такого файла, но невозможно читать данные напрямую read.table() data.table::fread() или readr::read_tsv() из-за ограничений памяти (а нужные мне строки неизвестны априори).

Итак, в итоге я получаю символьный вектор в памяти с элементом для каждой строки, но разделители табуляции все еще там. Я немного озадачен тем, как быстро извлечь определенный столбец из этого текста. В приведенном ниже примере, какой самый быстрый способ извлечь третий столбец? В тексте нет никаких «сюрпризов», таких как комментарии или имена в кавычках, но в моем реальном случае столбцы не имеют фиксированной ширины. Самый быстрый метод, который я нашел до сих пор, — это использовать 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]
}
 

Создано 2021-10-13 пакетом reprex (v2.0.1)

Комментарии:

1.Не могли бы вы изменить, какую data.table версию вы пробовали? Я пытался воспроизвести, но даже с setDTthreads(1L) fread этим было быстрее, чем read_tsv у меня.

2. Я попробовал версию v1.14.2, и на моей машине медиана занимает ~ 6,9 секунды, независимо от использования 6 или 1 потока. На моей машине с Linux это занимает 5,1 секунды. У вас случайно нет SSD, который записывает в файл быстрее, чем то, что я получаю на своем жестком диске?

3. Это правда, у меня есть SSD. Вы можете попробовать установить TMPDIR=/dev/shm перед запуском R сброс файла на диск памяти вместо HDD.

Ответ №1:

Специальная функция, написанная с помощью Rcpp, работала для меня здесь быстрее всего (чуть более чем в два раза быстрее 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) amp; 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 resu<
}

")

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 или используете неправильный тип переменной для выбора столбца. Однако, если вы выберете номер столбца, превышающий количество имеющихся столбцов, он вернет вектор пустых строк, а не выдаст ошибку. Вы могли бы легко написать оболочку R для функции C , если вам нужно другое поведение здесь, например, ошибка или вектор NA

Комментарии:

1. Спасибо, Аллан, это здорово и намного быстрее! Также приятно видеть, что опция регулярных выражений изложена так четко!