быстро загрузить подмножество строк из data.frame, сохраненного с помощью `saveRDS()`

С большим файлом (1 ГБ), созданным путем сохранения большого data.frame (или data.table), можно ли очень быстро загрузить небольшое подмножество строк из этого файла?

(Дополнительно для ясности: я имею в виду что-то такое же быстрое, как mmap, т. е. время выполнения должно быть приблизительно пропорционально объему извлеченной памяти, но постоянным по размеру общего набора данных. «Пропуск данных» должен иметь практически нулевая стоимость. Это может быть очень просто или невозможно, или что-то среднее, в зависимости от формата сериализации.)

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

Прав ли я, предполагая, что это было бы невозможно со сжатым файлом просто потому, что gzip требует распаковать все с самого начала?

 saveRDS(object, file = "", ascii = FALSE, version = NULL,
         compress = TRUE, refhook = NULL)

Но я надеюсь, что двоичный (ascii=F) несжатый (compress=F) может позволить что-то подобное. Использовать mmap в файле, а затем быстро переходить к интересующим строкам и столбцам?

Я надеюсь, что это уже сделано, или есть другой формат (достаточно компактный), который позволяет это и хорошо поддерживается в R.

Я использовал такие вещи, как gdbm (из Python) и даже реализовал пользовательскую систему в Rcpp для определенной структуры данных, но меня это не устраивает.

После публикации я немного поработал с пакетом ff (CRAN) и я очень впечатлен этим (хотя не так много поддержки character векторов).


person Aaron McDaid    schedule 15.07.2016    source источник


Ответы (1)


Прав ли я, предполагая, что это было бы невозможно со сжатым файлом просто потому, что gzip требует распаковать все с самого начала?

Действительно, для краткого объяснения возьмем за отправную точку какой-нибудь фиктивный метод:

AAAAVVBABBBC gzip сделает что-то вроде: 4A2VBA3BC

Очевидно, что вы не можете извлечь все A из файла, не прочитав его целиком, так как вы не можете угадать, есть ли в конце A или нет.

На другой вопрос «Загрузка части сохраненного файла» я не вижу решения в своей голове. Вы, вероятно, можете с write.csv и read.csv (или fwrite и fread из пакета data.table) с параметрами skipи nrows могли бы быть альтернативой.

Во что бы то ни стало, использование любой функции для уже прочитанного файла будет означать загрузку всего файла в память перед фильтрацией, что занимает не больше времени, чем чтение файла и последующее подмножество из памяти.

Вы можете создать что-то в Rcpp, используя потоки для чтения данных без загрузки их в память, но чтение и анализ каждой записи перед принятием решения о том, следует ли ее сохранить или нет, не даст вам действительно лучшей пропускной способности.

saveDRS сохранит сериализованную версию данных, например:

> myvector <- c("1","2","3").
> serialize(myvector,NULL)
 [1] 58 0a 00 00 00 02 00 03 02 03 00 02 03 00 00 00 00 10 00 00 00 03 00 04 00 09 00 00 00 01 31 00 04 00 09 00 00 00 01 32 00 04 00 09 00 00
[47] 00 01 33

Это, конечно, разборчиво, но означает чтение байт за байтом в соответствии с форматом.

С другой стороны, вы можете написать как csv (или write.table для более сложных данных) и использовать внешний инструмент перед чтением, что-то вроде:

z <- tempfile()
write.table(df, z, row.names = FALSE)
shortdf <- read.table(text= system( command = paste0( "awk 'NR > 5 && NR < 10 { print }'" ,z) ) )

Вам понадобится система Linux с awk, который способный анализировать миллионы строк за несколько миллисекунд или использовать скомпилированную версию awk очевидно.

Основное преимущество заключается в том, что awk может фильтровать регулярное выражение или некоторые другие условия для каждой строки данных.

Дополнение для случая data.frame, data.frame - это более или менее список векторов (простой случай), этот список будет сохранен последовательно, поэтому, если у нас есть кадр данных, например:

> str(ex)
'data.frame':   3 obs. of  2 variables:
 $ a: chr  "one" "five" "Whatever"
 $ b: num  1 2 3

Это сериализация:

> serialize(ex,NULL)
  [1] 58 0a 00 00 00 02 00 03 02 03 00 02 03 00 00 00 03 13 00 00 00 02 00 00 00 10 00 00 00 03 00 04 00 09 00 00 00 03 6f 6e 65 00 04 00 09 00
 [47] 00 00 04 66 69 76 65 00 04 00 09 00 00 00 08 57 68 61 74 65 76 65 72 00 00 00 0e 00 00 00 03 3f f0 00 00 00 00 00 00 40 00 00 00 00 00 00
 [93] 00 40 08 00 00 00 00 00 00 00 00 04 02 00 00 00 01 00 04 00 09 00 00 00 05 6e 61 6d 65 73 00 00 00 10 00 00 00 02 00 04 00 09 00 00 00 01
[139] 61 00 04 00 09 00 00 00 01 62 00 00 04 02 00 00 00 01 00 04 00 09 00 00 00 09 72 6f 77 2e 6e 61 6d 65 73 00 00 00 0d 00 00 00 02 80 00 00
[185] 00 ff ff ff fd 00 00 04 02 00 00 00 01 00 04 00 09 00 00 00 05 63 6c 61 73 73 00 00 00 10 00 00 00 01 00 04 00 09 00 00 00 0a 64 61 74 61
[231] 2e 66 72 61 6d 65 00 00 00 fe

Перевел на ascii для идеи:

X
    one five    Whatever?ð@@    names   a   b       row.names
ÿÿÿý    class   
data.frameþ

У нас есть заголовок файла, заголовок списка, затем каждый вектор, составляющий список, поскольку мы понятия не имеем, какой размер займет вектор символов, мы не можем переходить к произвольным данным, мы должны анализировать каждый заголовок (байты непосредственно перед текстовыми данными дают его длину). Что еще хуже, теперь, чтобы получить соответствующие целые числа, мы должны перейти к заголовку целочисленного вектора, который не может быть определен без анализа заголовка каждого символа и их суммирования.

Так что, на мой взгляд, создание чего-то возможно, но, вероятно, это будет не намного быстрее, чем чтение всего объекта, и будет хрупким для формата сохранения (поскольку в R уже есть 3 формата для сохранения объектов).

Некоторые ссылки здесь

Тот же вид, что и вывод сериализации в формате ascii (более читаемый, чтобы понять, как он организован):

> write(rawToChar(serialize(ex,NULL,ascii=TRUE)),"")
A
2
197123
131840
787
2
16
3
262153
3
one
262153
4
five
262153
8
Whatever
14
3
1
2
3
1026
1
262153
5
names
16
2
262153
1
a
262153
1
b
1026
1
262153
9
row.names
13
2
NA
-3
1026
1
262153
5
class
16
1
262153
10
data.frame
254
person Tensibai    schedule 15.07.2016
comment
но чтение и синтаксический анализ каждой записи перед принятием решения о том, следует ли ее сохранить или нет, не улучшит пропускную способность. Нет необходимости читать все подряд. fseek может пропускать произвольно большие фрагменты данных за постоянное время. Настоящий вопрос заключается в том, позволяет ли нам формат узнать точный размер (на диске) подструктуры данных, которую мы хотим игнорировать. - person Aaron McDaid; 16.07.2016
comment
@AaronMcDaid Не совсем, формат последовательный. Чтение data.frame - это более или менее чтение списка, код здесь, если вы хотите понять, что я имею в виду. Вкратце, я имею в виду, что вы не можете «пропустить» N строк, потому что вам придется выполнять несколько fseeks для каждой строки. Я добавлю некоторые подробности об этом в ответ. - person Tensibai; 18.07.2016
comment
Благодарю за разъяснение. Я прочитаю ваш ответ еще раз. На самом деле, я только что закончил писать свой собственный код для решения этой проблемы, сохраняя столбцы data.frame в серии bigmemory объектов (bigmemory в CRAN). Это позволяет произвольно искать любую строку. Мне пришлось позаботиться о хранении character векторов особым образом, но теперь это работает. - person Aaron McDaid; 18.07.2016
comment
@AaronMcDaid Вы должны попробовать сравнить его с microbenchmark, чтобы увидеть, действительно ли это приносит улучшения. Лучшими вариантами с точки зрения скорости, которые я знаю, являются fread и fwrite из пакета data.table, который выполняет какую-то параллельную обработку. Я не понимаю, как использование bigmemory может улучшить скорость загрузки, так как весь объект будет загружаться в память с диска, вообще не сохраняя никаких операций ввода-вывода. - person Tensibai; 18.07.2016
comment
filebacked.big.matrix хранится на диске и поэтому загружается с mmap практически мгновенно. Я могу получить доступ к произвольным строкам в течение нескольких секунд после нового запуска R в файле размером 10 ГБ. Весь объект не загружается как таковой, а просто отображается в виртуальную память. - person Aaron McDaid; 18.07.2016
comment
Интересный момент, я посмотрю на это, не могли бы вы поделиться упрощенной версией вашего подхода, чтобы сравнить его с другими? - person Tensibai; 19.07.2016
comment
@ Тенсибай, да. Я написал короткую запись в блоге об этом со ссылкой на мой код. - person Aaron McDaid; 20.07.2016