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. Спасибо, Аллан, это здорово и намного быстрее! Также приятно видеть, что опция регулярных выражений изложена так четко!