Как добавить несколько больших объектов data.table в одну data.table и быстро экспортировать в csv без нехватки памяти?

Простой ответ на этот вопрос — «купить больше оперативной памяти», но я надеюсь получить более конструктивный ответ и узнать что-то в процессе.

Я использую Windows 7 64-бит с 8 ГБ оперативной памяти.

У меня есть несколько очень больших файлов .csv.gz (~ 450 МБ без сжатия) с точно такой же информацией заголовка, которую я читаю в R и выполняю некоторую обработку. Затем мне нужно объединить обработанные объекты R в один главный объект и записать обратно в .csv на диск.

Я делаю эту же операцию с несколькими наборами файлов. Например, у меня есть 5 папок, в каждой из которых по 6 файлов csv.gz. Мне нужно получить 5 мастер-файлов, по одному для каждой папки.

Мой код выглядит примерно так:

for( loop through folders ){
    master.file = data.table()

    for ( loop through files ) {
        filename = list.files( ... )
        file = as.data.table ( read.csv( gzfile( filename ), stringsAsFactors = F ))
        gc()

        ...do some processing to file...

        # append file to the running master.file
        if ( nrow(master.file) == 0 ) {
            master.file = file
        } else {
            master.file = rbindlist( list( master.file, file) )
        }
        rm( file, filename )
        gc()
    }

    write.csv( master.file, unique master filename, row.names = FALSE )

    rm( master.file )
    gc()

}

Этот код не работает. Я получаю ошибку cannot allocate memory до того, как он записывает окончательный CSV. Я наблюдал за монитором ресурсов во время выполнения этого кода и не понимаю, почему он использует 8 ГБ ОЗУ для выполнения этой обработки. Общий размер всех файлов составляет примерно 2,7 ГБ, поэтому я ожидал, что максимальный объем памяти, который R будет использовать, составит 2,7 ГБ. Но операция write.csv, по-видимому, использует тот же объем памяти, что и объект данных, который вы записываете, поэтому, если у вас есть объект размером 2,7 ГБ в памяти и вы пытаетесь записать его, вы будете использовать 5,6 ГБ памяти.

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

Я подозреваю, что мог бы использовать пакет sqldf, как уже упоминалось of-memory">здесь и здесь но когда я установил оператор sqldf равным переменной R, я получил те же ошибки нехватки памяти.


person Brian D    schedule 20.12.2013    source источник
comment
Общее правило заключается в том, что размер вашего самого большого объекта должен быть в 3 раза больше. (Так что вы уже нарушили это правило.) Кроме того, у вас может быть 8 МБ ОЗУ, но вам нужно вычесть ОЗУ, которое вы используете для ОС, других приложений и фоновых утилит.   -  person IRTFM    schedule 21.12.2013
comment
Почему бы вам не использовать write.table(yourFirstDataTable, sep = ",", file = YourFile.csv) для первого прочитанного и обработанного файла, а затем write.table(yourOtherDataTables, sep = ",", file = YourFile.csv, append = TRUE, col.names = FALSE)?   -  person A5C1D2H2I1M1N2O1R2T1    schedule 21.12.2013
comment
@AnandaMahto, это отличное предложение! Я забыл, что для write.table была опция добавления. write.csv отключает эту опцию, но с write.table мне не нужно добавлять данные в R, я могу просто добавлять каждый новый объект в файл на диске.   -  person Brian D    schedule 24.12.2013


Ответы (1)


Обновление от 23.12.2013. Следующее решение работает полностью в R без нехватки памяти (спасибо, @AnandaMahto).
Основное предостережение в отношении этого метода заключается в том, что вы должны быть абсолютно уверены, что файлы, которые вы читаете и записываете, каждый раз иметь одни и те же столбцы заголовков в точно таком же порядке, или ваш код обработки R должен гарантировать это, поскольку write.table не проверяет это за вас.

for( loop through folders ){

    for ( loop through files ) {

        filename = list.files( ... )
        file = as.data.table ( read.csv( gzfile( filename ), stringsAsFactors = F ))
        gc()

        ...do some processing to file...

        # append file to the running master.file
        if ( first time through inner loop) {
            write.table(file, 
                        "masterfile.csv", 
                        sep = ",", 
                        dec = ".", 
                        qmethod = "double", 
                        row.names = "FALSE")
        } else {
            write.table(file,
                        "masterfile.csv",
                        sep = ",",
                        dec = ".",
                        qmethod = "double",
                        row.names = "FALSE",
                        append = "TRUE",
                        col.names = "FALSE")
        }
        rm( file, filename )
        gc()
    }
    gc()
}

Мое первоначальное решение:

for( loop through folders ){

    for ( loop through files ) {
        filename = list.files( ... )
        file = as.data.table ( read.csv( gzfile( filename ), stringsAsFactors = F ))
        gc()

        ...do some processing to file...

        #write out the file
        write.csv( file, ... )
        rm( file, filename )
        gc()
    }        
    gc()
}

Затем я загрузил и установил пакет sed для GnuWin32 и использовал инструменты командной строки Windows, чтобы добавить файлы как следует:

copy /b *common_pattern*.csv master_file.csv

Это добавляет вместе все отдельные файлы .csv, имена которых содержат текстовый шаблон «common_pattern», заголовки и все такое.

Затем я использую sed.exe для удаления всех строк заголовка, кроме первой, следующим образом:

"c:\Program Files (x86)\GnuWin32\bin\sed.exe" -i 2,${/header_pattern/d;} master_file.csv

-i указывает sed просто перезаписать указанный файл (на месте).

2,$ указывает sed искать диапазон от 2-й строки до последней строки ($)

{/header_pattern/d;} говорит sed искать все строки в диапазоне с текстом "header_pattern" и d удалить эти строки

Чтобы убедиться, что это делает то, что я хотел, я сначала напечатал строки, которые планировал удалить.

"c:\Program Files (x86)\GnuWin32\bin\sed.exe" -n 2,${/header_pattern/p;} master_file.csv

Работает как шарм, я просто хотел бы сделать все это в R.

person Brian D    schedule 20.12.2013
comment
Я пока только просмотрел вопрос и ответ... как насчет fread? - person Matt Dowle; 21.12.2013
comment
Я посмотрел fread, и там было примечание, что оно еще не предназначено для использования в производстве, поэтому я отказался от него. - person Brian D; 21.12.2013
comment
fread() по моему опыту отлично работает. Убедитесь, что у вас включена виртуальная память с помощью чего-то вроде memory.limit(size = 6 * 8192). Это медленно, но работает. (И добавьте «купить SSD» в список «купить оперативную память» ;)). - person Peter; 22.12.2013
comment
К вашему сведению, явный вызов gc() ничего не дает - person hadley; 22.12.2013
comment
@Peter Сколько у вас оперативной памяти и какой размер файла вы загружаете? Я подозреваю, что это сильно отличается от цифр в вопросе, но ваш комментарий довольно общий о fread? - person Matt Dowle; 23.12.2013
comment
@BrianD В этом случае я бы попробовал fread. Нечего терять. Примечание о непроизводственном использовании связано с созданием зависимостей кода в вашем коде (аргументы могут измениться в будущем). Он не нестабилен в нерабочем смысле. - person Matt Dowle; 23.12.2013
comment
@hadley «Ничего не достигает» кажется слишком сильным. Никогда, никогда? Как вы оценили то, что сделал gc()? Вы упоминаете в этой статье «несмотря на то, что вы читали в другом месте», но не вдаетесь в подробности — они менее умны, чем вы, или просто все ошибаются? Я полагаю, что существуют разные уровни сборки мусора, более глубокий уровень запускается каждые 20 (iirc) — вы знали об этом? Вы абсолютно уверены в том, что пишете? - person Matt Dowle; 23.12.2013
comment
@MattDowle Мой комментарий по поводу fread() заключался в том, что у меня он всегда работал нормально и не связан с объемом оперативной памяти. Моя проблема была аналогична той, что была в вопросе, только немного больше (25 файлов, разделенных табуляцией, 220-450 МБ без сжатия, всего ~ 9 ГБ, также на 64-разрядной версии Win 7, на Intel Core 2 Quad Q6600 с 8 ГБ ОЗУ). В его случае, вероятно, 6 * RAM - это слишком много (я просто копировал и вставлял, и мне нужно больше RAM, потому что я переделывал и делал другие вещи после импорта). Я сделал все в R, так как в моем случае файлы имели разный порядок столбцов. - person Peter; 23.12.2013
comment
@hadley Я не знаю, как именно работает gc() (буду читать то, на что вы ссылаетесь), но у меня неофициальное впечатление, что он никогда не запускался автоматически. Код, в котором я использовал его вручную после каждой операции с интенсивным использованием оперативной памяти, работал быстрее. Может быть, из-за виртуальной памяти, которую мне пришлось использовать? Жаль, что я перешел на машину с 32 ГБ ОЗУ и не могу предоставить точные тесты для поддержки этого... - person Peter; 23.12.2013
comment
@Peter gc() всегда запускается автоматически, когда это необходимо. В противном случае R очень быстро исчерпал бы память. Если вы видите какие-либо различия, это, вероятно, эффект плацебо. - person hadley; 24.12.2013
comment
@MattDowle в этом случае я думаю, что бремя доказательства лежит на обвинителе: вам нужно предоставить доказательства того, что сборщик мусора R не работает и его нужно запускать вручную. - person hadley; 24.12.2013
comment
@hadley В руководстве ?gc говорится, что оно запускает сбор, а также что может быть полезно вызывать его вручную. Вы хотите сказать, что инструкция неверна? Если да, то как вы предлагаете его улучшить? - person Matt Dowle; 24.12.2013
comment
@Peter Если вы загружали 9 ГБ в 8 ГБ, то, конечно, вы меняли местами, и именно поэтому вы рекомендовали SSD. Это не имеет ничего общего с fread. R работает в оперативной памяти. Если вы выберете больше, чем ОЗУ, вы будете меняться местами, а это медленно. Ваш первоначальный комментарий, похоже, касается R (и любого программного обеспечения в оперативной памяти), не имеющего ничего общего с fread. fread предназначен для загрузки данных в оперативную память. Это не помогает для данных больше, чем оперативная память. оперативная память очень дешевая; например некоторые data.table пользователи имеют 512 ГБ оперативной памяти (0,5 ТБ). Рад, что вы увеличили объем памяти в четыре раза до 32 ГБ. - person Matt Dowle; 24.12.2013
comment
@MattDowle Да, конечно, вы абсолютно правы, и я знаю, что вы пишете. «Медленная» часть относится к использованию виртуальной памяти, а не к fread. Возможно, мне следовало опубликовать часть виртуальной памяти под вопросом, а не под решением. К сожалению, я не могу редактировать, просто удалите его сейчас (и я сделаю это, если хотите). data.table сэкономил мне много времени, большое вам спасибо! Рекомендация SSD была промежуточным решением, мой старый ПК уже имел максимально поддерживаемые 8 ГБ ОЗУ, и было дешевле получить SSD, чем совершенно новый ПК (что я в конечном итоге мог себе позволить / оправдать). - person Peter; 24.12.2013
comment
@MattDowle Он всегда говорит, что это также будет происходить автоматически без вмешательства пользователя, и основная цель вызова gc — получение отчета об использовании памяти, и его вызов может только побудить R вернуть память операционной системе (что является довольно слабым требовать). Так что я думаю, что помощь сбалансирована. - person hadley; 24.12.2013
comment
@hadley Это работает заметно быстрее с ручным gc(). Только что проверил на виртуальной машине (Win XP 32 бит, 512 МБ ОЗУ + 3 ГБ файл подкачки). Количество строк, которые fread читает, важно для генерации разного времени. Я не знаю, из-за R- или из-за особенностей ОС. Может я что-то не так делаю (код далеко не красивый и неэффективный), но на плацебо не похоже. Может быть, неоптимальный порог для запуска gc() в сценарии с низким объемом оперативной памяти и большим количеством файлов подкачки? - person Peter; 24.12.2013
comment
@Hadley Если вы утверждаете, что руководство для ?gc делает слабое утверждение, то вам нужно это показать. Насколько я вижу, вы только что согласились с тем, что вызов gc() иногда может что-то сделать. Это все, что нужно, чтобы опровергнуть ваше (сильное) заявление о том, что явный вызов gc() ничего не дает. Поскольку вы ученый, я оспариваю ваши научные рассуждения в данном случае. Вы не ответили на мой вопрос о том, знали ли вы о различных уровнях сборки мусора и о том, что более глубокая сборка происходит каждые 20 (iirc). Есть ли у вас какая-либо поддержка вашего заявления от кого-либо еще? - person Matt Dowle; 24.12.2013
comment
@Питер Интересно. Во внутренних компонентах R большая векторная куча отличается от маленьких векторов (iirc примерно 4 КБ, но не цитируйте меня). Я очень мало знаю об этой области, но это может объяснить, почему вы заметили, что она зависит от количества строк в файле; т. е. чтобы векторы (столбцы) были достаточно большими, чтобы их можно было разместить в большой векторной куче. Дополнительные настройки для точной настройки триггерной точки сборки мусора находятся в ?Memory. - person Matt Dowle; 24.12.2013
comment
@BrianD Чтобы избежать копирования и вставки кода (часть write.table() почти идентична), вы можете определить переменную if(first time through inner loop) append.to.file<-TRUE else append.to.file<-FALSE и вызвать write.table(append=append.to.file,...). Легче читать, а также если вы решите изменить формат вывода в будущем. - person Peter; 26.12.2013
comment
@hadley для некоторого моего кода R я использую явный вызов gc, чтобы убедиться, что R не перегружает ресурсы (чтобы другие процессы работали гладко параллельно с R). Это вопрос, отличный от того, вызывает ли вызов gc дополнительную ценность при запуске R и только R, но все же это важное использование gc для моих нужд. - person eddi; 02.01.2014
comment
@eddi это веская причина, но я считаю, что это будет работать только в некоторых операционных системах. - person hadley; 02.01.2014
comment
@hadley, у меня это работает как на Linux, так и на Windows (быстрый тест - проверить использование памяти R после создания и удаления большого объекта и последующего запуска gc) - person eddi; 02.01.2014
comment
Рассмотрите возможность использования нового fwrite, см. 31. в новостях для 1.9.7 и инструкции по установке для Последняя версия - person MichaelChirico; 08.04.2016