Быстрое чтение очень больших таблиц как данных

422

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

Я знаю, что чтение в таблице в виде списка с помощью scan() может быть довольно быстрым, например:

datalist <- scan('myfile',sep='\t',list(url='',popularity=0,mintime=0,maxtime=0)))

Но некоторые из моих попыток преобразовать это в dataframe, похоже, уменьшают производительность выше в 6 раз:

df <- as.data.frame(scan('myfile',sep='\t',list(url='',popularity=0,mintime=0,maxtime=0))))

Есть ли лучший способ сделать это? Или, возможно, совсем другой подход к проблеме?

Теги:
dataframe
import
r-faq

8 ответов

364
Лучший ответ

Обновление, спустя несколько лет

Этот ответ старый, и R двинулся дальше. Тонкая настройка read.table для работы немного быстрее принесет read.table пользу. Ваши варианты:

  1. Использование fread в data.table для импорта данных из файлов csv/tab-delimited непосредственно в R. См. Ответ mnel.

  2. Использование read_table в readr (на CRAN с апреля 2015 года). Это работает так же, как fread выше. Ссылка readme в ссылке объясняет разницу между двумя функциями (readr настоящее время утверждает, что она "на 1,5-2 раза медленнее", чем data.table::fread).

  3. read.csv.raw от iotools предоставляет третий вариант для быстрого чтения файлов CSV.

  4. Попытка хранить как можно больше данных в базах данных, а не в плоских файлах. (А также как лучший постоянный носитель данных, данные передаются в и из R в двоичном формате, что происходит быстрее.) read.csv.sql в пакете sqldf, как описано в JD Long answer, импортирует данные во временный SQLite, а затем читает его в R. См. Также: пакет RODBC, а также раздел с обратным RODBC на RODBC пакета DBI. MonetDB.R дает вам тип данных, который претендует на роль фрейма данных, но на самом деле является MonetDB снизу, что повышает производительность. Импортируйте данные с monetdb.read.csv функции monetdb.read.csv. dplyr позволяет работать непосредственно с данными, хранящимися в нескольких типах баз данных.

  5. Сохранение данных в двоичных форматах также может быть полезно для повышения производительности. Используйте saveRDS/readRDS (см ниже), h5 или rhdf5 пакеты формата HDF5 или write_fst/read_fst от fst пакета.


Оригинальный ответ

Есть несколько простых вещей, чтобы попробовать, используете ли вы read.table или сканирование.

  1. Set nrows= количество записей в ваших данных (nmax в scan).

  2. Убедитесь, что comment.char="" отключить интерпретацию комментариев.

  3. Явно определите классы каждого столбца, используя colClasses в read.table.

  4. Установка multi.line=FALSE также может повысить производительность сканирования.

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

Другая альтернатива - это фильтрация ваших данных, прежде чем вы прочитаете ее в R.

Или, если проблема в том, что вы должны регулярно ее читать, используйте эти методы для однократного чтения данных, а затем сохраните фрейм данных как двоичный код с помощью save saveRDS, то в следующий раз вы сможете быстрее получить его с помощью load readRDS.

  • 4
    Спасибо за советы Ричи. Я провел небольшое тестирование, и кажется, что прирост производительности при использовании параметров nrow и colClasses для read.table довольно скромный. Например, чтение таблицы строк ~ 7M занимает 78 секунд без параметров и 67 секунд с параметрами. (примечание: таблица имеет 1 символьный столбец, 4 целочисленных столбца, и я читаю, используя comment.char = '' и stringsAsFactors = FALSE). Использование save () и load (), когда это возможно, отличный совет - после сохранения с помощью save () эта же таблица загружается всего за 12 секунд.
  • 1
    Пакет «пера» имеет новый двоичный формат, который хорошо сочетается с фреймами данных Python Python
Показать ещё 5 комментариев
267

Вот пример, который использует fread из data.table 1.8.7

Примеры приведены на странице справки fread, с таймингами на моем Windows XP Core 2 duo E8400.

library(data.table)
# Demo speedup
n=1e6
DT = data.table( a=sample(1:1000,n,replace=TRUE),
                 b=sample(1:1000,n,replace=TRUE),
                 c=rnorm(n),
                 d=sample(c("foo","bar","baz","qux","quux"),n,replace=TRUE),
                 e=rnorm(n),
                 f=sample(1:1000,n,replace=TRUE) )
DT[2,b:=NA_integer_]
DT[4,c:=NA_real_]
DT[3,d:=NA_character_]
DT[5,d:=""]
DT[2,e:=+Inf]
DT[3,e:=-Inf]

стандарт read.table

write.table(DT,"test.csv",sep=",",row.names=FALSE,quote=FALSE)
cat("File size (MB):",round(file.info("test.csv")$size/1024^2),"\n")    
## File size (MB): 51 

system.time(DF1 <- read.csv("test.csv",stringsAsFactors=FALSE))        
##    user  system elapsed 
##   24.71    0.15   25.42
# second run will be faster
system.time(DF1 <- read.csv("test.csv",stringsAsFactors=FALSE))        
##    user  system elapsed 
##   17.85    0.07   17.98

оптимизированный read.table

system.time(DF2 <- read.table("test.csv",header=TRUE,sep=",",quote="",  
                          stringsAsFactors=FALSE,comment.char="",nrows=n,                   
                          colClasses=c("integer","integer","numeric",                        
                                       "character","numeric","integer")))


##    user  system elapsed 
##   10.20    0.03   10.32

Fread

require(data.table)
system.time(DT <- fread("test.csv"))                                  
 ##    user  system elapsed 
##    3.12    0.01    3.22

sqldf

require(sqldf)

system.time(SQLDF <- read.csv.sql("test.csv",dbname=NULL))             

##    user  system elapsed 
##   12.49    0.09   12.69

# sqldf as on SO

f <- file("test.csv")
system.time(SQLf <- sqldf("select * from f", dbname = tempfile(), file.format = list(header = T, row.names = F)))

##    user  system elapsed 
##   10.21    0.47   10.73

ff/ffdf

 require(ff)

 system.time(FFDF <- read.csv.ffdf(file="test.csv",nrows=n))   
 ##    user  system elapsed 
 ##   10.85    0.10   10.99

Вкратце:

##    user  system elapsed  Method
##   24.71    0.15   25.42  read.csv (first time)
##   17.85    0.07   17.98  read.csv (second time)
##   10.20    0.03   10.32  Optimized read.table
##    3.12    0.01    3.22  fread
##   12.49    0.09   12.69  sqldf
##   10.21    0.47   10.73  sqldf on SO
##   10.85    0.10   10.99  ffdf
  • 36
    Отличный ответ, и бенчмаркинг имеет место в других контекстах. Просто прочитайте файл размером 4 Гб за минуту с fread . Попытался прочитать его с помощью базовых функций R, и это заняло около 15 часов.
  • 1
    Мой тест показывает еще большие преимущества для read.csv в data.table. обратите внимание, что data.table не является стандартным R, но (к сожалению) «просто» приятно делится его создателями в CRAN. он даже не считается достаточно стандартным для составления общего списка пакетов R, а тем более не может рассматриваться как замена для фреймов данных. у этого есть много преимуществ, но также и некоторые очень противоречивые аспекты. вы можете использовать as.data.frame (fread.csv ("test.csv")) с пакетом, чтобы вернуться в стандартный мир фреймов данных R.
Показать ещё 4 комментария
255

Я не рассматривал этот вопрос на начальном этапе и задал аналогичный вопрос несколько дней спустя. Я собираюсь снять свой предыдущий вопрос, но я подумал, что добавлю здесь ответ, чтобы объяснить, как я использовал sqldf() для этого.

Был немного обсуждения относительно наилучшего способа импортировать 2 ГБ или более текстовых данных в фрейм данных R. Вчера я написал сообщение об использовании sqldf() для импорта данных в SQLite в качестве промежуточной области, а затем сосать его из SQLite в R. Это очень хорошо работает для меня. Я смог вытащить 2 ГБ (3 столбца, 40 мм строк) данных в < 5 минут. Напротив, команда read.csv выполнялась всю ночь и никогда не завершалась.

Здесь мой тестовый код:

Настройте тестовые данные:

bigdf <- data.frame(dim=sample(letters, replace=T, 4e7), fact1=rnorm(4e7), fact2=rnorm(4e7, 20, 50))
write.csv(bigdf, 'bigdf.csv', quote = F)

Я перезапустил R перед запуском следующей процедуры импорта:

library(sqldf)
f <- file("bigdf.csv")
system.time(bigdf <- sqldf("select * from f", dbname = tempfile(), file.format = list(header = T, row.names = F)))

Я пропускаю следующую строку всю ночь, но она никогда не завершалась:

system.time(big.df <- read.csv('bigdf.csv'))
  • 22
    JD, вы заслуживаете медаль за этот ответ. Слишком много людей слишком долго стучали головой об этой проблеме.
  • 11
    Смотрите также read.csv.sql
Показать ещё 9 комментариев
70

Странно, что никто не ответил на нижнюю часть вопроса в течение многих лет, даже если это важно - data.frame - это просто списки с правильными атрибутами, поэтому, если у вас есть большие данные, которые вы не хотите использовать as.data.frame или аналогичный для списка. Это намного быстрее, чтобы просто "превратить" список в кадр данных на месте:

attr(df, "row.names") <- .set_row_names(length(df[[1]]))
class(df) <- "data.frame"

Это не делает копию данных так, чтобы она была немедленной (в отличие от всех других методов). Предполагается, что вы уже установили names() в списке.

[Что касается загрузки больших данных в R - лично, я сбрасываю их по столбцу в двоичные файлы и использую readBin() - это самый быстрый метод (отличный от mmapping) и ограничен только скоростью диска. Разбор ASCII файлов по своей сути медленный (даже в C) по сравнению с двоичными данными.]

  • 6
    Использование tracmem предполагает, что attr<- и class<- делают копии внутри. bit::setattr или data.table::setattr не будут.
  • 6
    Может быть, вы использовали неправильный порядок? Копии не будет, если вы используете df=scan(...); names(df)=...; attr...; class... - см. tracemem() (протестировано в R 2.15.2)
Показать ещё 2 комментария
29

Ранее это было в R-Help, поэтому стоит рассмотреть.

Было предложено использовать readChar(), а затем выполнить строковые манипуляции с результатом strsplit() и substr(). Вы можете видеть, что логика, используемая в readChar, намного меньше, чем read.table.

Я не знаю, есть ли здесь проблема с памятью, но вы также можете хотеть взглянуть на пакет HadoopStreaming. Этот использует Hadoop, который является картой MapReduce, предназначенной для работы с большими наборами данных. Для этого вы должны использовать функцию hsTableReader. Это пример (но у него есть кривая обучения, чтобы узнать Hadoop):

str <- "key1\t3.9\nkey1\t8.9\nkey1\t1.2\nkey1\t3.9\nkey1\t8.9\nkey1\t1.2\nkey2\t9.9\nkey2\"
cat(str)
cols = list(key='',val=0)
con <- textConnection(str, open = "r")
hsTableReader(con,cols,chunkSize=6,FUN=print,ignoreKey=TRUE)
close(con)

Основная идея здесь - разбить импорт данных на куски. Вы даже можете зайти так далеко, чтобы использовать одну из параллельных фреймворков (например, снег) и запустить импорт данных параллельно, сегментируя файл, но, скорее всего, для больших наборов данных, которые не помогут, поскольку вы столкнетесь с ограничениями памяти, поэтому подход map-reduce является лучшим подходом.

  • 0
    Я только что сделал быстрый тест, и readChar, кажется, намного быстрее, чем даже readLines, по какой-то необъяснимой причине. Тем не менее, он все еще медленный как грех по сравнению с простым тестом C. При простой задаче чтения 100 мегабайт R примерно в 5-10 раз медленнее, чем C
  • 4
    Привлечь сюда Hadoop - все равно что принести пушку в бой с ножом.
Показать ещё 3 комментария
5

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

>numRow=as.integer(system(paste("wc -l", bedGraph, "| sed 's/[^0-9.]*\\([0-9.]*\\).*/\\1/'"), intern=T))

Затем вы можете использовать это либо в read.csv, read.table...

>system.time((BG=read.table(bedGraph, nrows=numRow, col.names=c('chr', 'start', 'end', 'score'),colClasses=c('character', rep('integer',3)))))
   user  system elapsed 
 25.877   0.887  26.752 
>object.size(BG)
203949432 bytes
4

Часто я считаю, что просто хорошая практика - хранить большие базы данных в базе данных (например, Postgres). Я не использую ничего слишком большого, чем (nrow * ncol) ncell = 10M, что довольно мало; но я часто нахожу, что я хочу, чтобы R создавал и удерживал графики с интенсивной памятью только тогда, когда я запрашивал из нескольких баз данных. В будущем из 32 ГБ ноутбуков некоторые из этих типов проблем памяти исчезнут. Но привлекательность использования базы данных для хранения данных, а затем использования R-памяти для результирующих результатов запроса и графиков по-прежнему может быть полезна. Некоторые преимущества:

(1) Данные остаются загруженными в вашу базу данных. Вы просто подключаетесь в pgadmin к базам данных, которые вы хотите, когда снова включаете свой ноутбук.

(2) Действительно, R может выполнять множество более важных статистических и графических операций, чем SQL. Но я думаю, что SQL лучше спроектирован для запроса больших объемов данных, чем R.

# Looking at Voter/Registrant Age by Decade

library(RPostgreSQL);library(lattice)

con <- dbConnect(PostgreSQL(), user= "postgres", password="password",
                 port="2345", host="localhost", dbname="WC2014_08_01_2014")

Decade_BD_1980_42 <- dbGetQuery(con,"Select PrecinctID,Count(PrecinctID),extract(DECADE from Birthdate) from voterdb where extract(DECADE from Birthdate)::numeric > 198 and PrecinctID in (Select * from LD42) Group By PrecinctID,date_part Order by Count DESC;")

Decade_RD_1980_42 <- dbGetQuery(con,"Select PrecinctID,Count(PrecinctID),extract(DECADE from RegistrationDate) from voterdb where extract(DECADE from RegistrationDate)::numeric > 198 and PrecinctID in (Select * from LD42) Group By PrecinctID,date_part Order by Count DESC;")

with(Decade_BD_1980_42,(barchart(~count | as.factor(precinctid))));
mtext("42LD Birthdays later than 1980 by Precinct",side=1,line=0)

with(Decade_RD_1980_42,(barchart(~count | as.factor(precinctid))));
mtext("42LD Registration Dates later than 1980 by Precinct",side=1,line=0)
0

Вместо обычного read.table я чувствую, что fread - это более быстрая функция. Указание дополнительных атрибутов, таких как выбор только необходимых столбцов, определение колласс и строки в качестве факторов, сократит время, необходимое для импорта файла.

data_frame <- fread("filename.csv",sep=",",header=FALSE,stringsAsFactors=FALSE,select=c(1,4,5,6,7),colClasses=c("as.numeric","as.character","as.numeric","as.Date","as.Factor"))

Ещё вопросы

Сообщество Overcoder
Наверх
Меню