快速读取非常大的表作为数据框


502

我有非常大的表(3000万行),我想将其作为R中的数据帧加载。 read.table()它具有许多便利的功能,但是似乎实现中有很多逻辑会使事情变慢。就我而言,我假设我提前知道了列的类型,该表不包含任何列标题或行名,并且没有任何我要担心的病理字符。

我知道使用列表读取表scan()可能很快,例如:

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

但是我将其转换为数据帧的一些尝试似乎使上述性能降低了6倍:

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

有更好的方法吗?还是完全不同的解决方法?

Answers:


425

几年后的更新

这个答案很旧,R继续前进。调整read.table运行速度会一点点好处。您的选择是:

  1. 使用vroomtidyverse包vroom将数据从csv / tab分隔的文件直接导入R tibble。

  2. 使用freaddata.table导入CSV从/制表符分隔的文件中的数据直接导入R.见MNEL的答案

  3. 使用read_tablereadr(从2015年4月在CRAN)。这与fread上面非常类似。链接中的自述文件解释了这两种功能之间的区别(readr目前声称比“慢1.5-2倍” data.table::fread)。

  4. read.csv.rawfrom iotools提供了用于快速读取CSV文件的第三个选项。

  5. 尝试在数据库而不是平面文件中存储尽可能多的数据。(以及更好的永久存储介质,数据以二进制格式在R中来回传递,这是更快的。)如JD Long的答案所述,read.csv.sqlsqldf包将数据导入到临时SQLite数据库中,然后读取它另请参见:程序包,以及程序包页面的反向依赖部分。为您提供了一种伪装成数据帧的数据类型,但实际上是其下面的MonetDB,从而提高了性能。导入数据及其功能。 使您可以直接处理存储在几种类型的数据库中的数据。RODBCDBIMonetDB.Rmonetdb.read.csvdplyr

  6. 以二进制格式存储数据对于提高性能也很有用。使用saveRDS/ readRDS(见下文),则h5rhdf5为HDF5格式,或包write_fst/ read_fstfst包。


原始答案

无论您使用read.table还是scan,都有一些简单的方法可以尝试。

  1. 设置nrows= 数据中的记录数nmaxin scan)。

  2. 确保comment.char=""关闭注释的解释。

  3. 使用colClassesin 明确定义每列的类read.table

  4. 设置multi.line=FALSE还可以提高扫描性能。

如果这些都不起作用,请使用性能分析软件包之一来确定哪些行使速度变慢。也许您可以read.table根据结果编写一个简化版本。

另一种选择是在将数据读入R之前对其进行过滤。

或者,如果问题在于您必须定期读取数据,则可以使用这些方法一次读取数据,然后使用以下方法将数据帧另存为二进制blob: save saveRDS,那么下次您可以使用 load readRDS


4
感谢您的提示Richie。我做了一些测试,似乎为read.table使用nrow和colClasses选项带来的性能提升是相当适度的。例如,读取〜7M行表需要78s(不带选项)和67s(带选项)。(注意:该表有1个字符列,4个整数列,我使用comment.char =''和stringsAsFactors = FALSE读取)。尽可能使用save()和load()是一个很好的技巧-一旦与save()存储在一起,同一张表只需12秒即可加载。
eytan

2
“羽毛”包具有一种新的二进制格式,可与Python的pandas数据帧配合使用
rsoren'Mar

4
我认为您可能需要就该软件包再次更新您的帖子feather。读取数据feather要比快得多fread。例如,在我刚加载的4GB数据集上read_feather,速度约为的4.5倍fread。用于保存数据fwrite仍然更快。blog.dominodatalab.com/the-r-data-io-shootout
Z玻色子

2
但是,与RDS相比,羽毛文件的大小要大得多。我认为它不支持压缩。RDS文件为216 MB,羽状文件为4GB。因此feather读取速度更快,但它使用了更多的存储空间。
Z玻色子

@Zboson如果需要将数据帧存储在可以从R和Python访问的文件中,那么这feather是一个不错的选择。如果您只关心能够在R中读取数据,则更rds可取。
Richie Cotton

279

这是一个freaddata.table1.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]

标准读表

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

优化的读取表

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

恐惧

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

43
很好的答案,基准测试在其他情况下也适用。只需用不到一分钟的时间读入4GB文件fread。曾尝试使用基本的R函数读取它,但大约花费了15个小时。
阿里·弗里德曼

1
我的基准测试表明data.table中的read.csv甚至具有更大的速度优势。注意data.table不是标准的R,但是(不幸地)由它的创建者在CRAN上很好地共享。它甚至不被认为足以构成通用R包列表的标准,更没有资格代替数据帧。它具有很多优点,但也有一些违反直觉的方面。您可能希望将as.data.frame(fread.csv(“ test.csv”))与该包一起使用,以返回标准R数据帧世界。
ivo Welch 2015年

3
@mnel您能否重新运行基准测试并加入readr
jangorecki 2015年

2
第二个@jangorecki。此外,鉴于fread现在有一些真正的竞争对手,可能会为优化fread使用情况添加基准-指定colClasses
。– MichaelChirico

1
@jangorecji @ MichaelChirico给出的代码是完全可复制的,因此可以直接模拟阅读器...重新运行代码,在我的机器上,即使我通过网络运行它,但大多数结果的运行时间是大多数结果的两倍甚至更多。良好的更新版本,因为现在已经有一段时间了。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。此处显示的最快解决方案的fread是我这边的2s进行比较(由于某种原因,第一次运行在8.69s上)
R. Prost

249

我最初没有看到这个问题,几天后又问了类似的问题。我打算将上一个问题记下来,但是我想在这里添加一个答案,以解释我以前是如何sqldf()做到的。

关于将2GB或更多文本数据导入R数据帧的最佳方法,鲜有讨论。昨天,我写了一篇博客文章,内容涉及sqldf()用于将数据导入到SQLite作为暂存区域,然后将其从SQLite导入到R中。这对我来说确实很好。我能够在不到5分钟的时间内提取2GB(3列,40mm行)的数据。相比之下,该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'))

1
你好 您如何将其用作设计为可同时与所有数据一起使用的其他包(例如zoo)的输入?
skan,

@skan最终对象是一个数据帧。因此,您必须将其转换为Zoo对象才能与Zoo一起使用。查看Zoo docs中的示例以获取插图。
JD 2013年

@JD龙 嗨,问题在于,当您将其转换为Zoo对象时,它将尝试使其适合内存。如果太大,则会产生错误。而且,如果zoo对象的结果(例如两个系列的集合)也同样是sql或ff对象,那么它也必须是sql或ff对象。
skan 2013年

我不知道sqldf有什么问题。我已经在磁盘上创建了一个简单的1GB文件(带有2个数字列),并使用DTSQL <-read.csv.sql(“ f2.txt”,dbname = tempfile()),它尝试将整个数据加载到内存中。明天我将尝试使用ff和revoscaler代替。
skan 2013年

1
m是千,所以mm是千,即百万。我可能应该把它大写为MM。但是我发现,如果您有足够多的听众,那么大约一百万个缩写都会使人感到困惑。为了使我过于冗长,我感到更加困惑! accountingcoach.com/blog/what-does-m-and-mm-stand-for
JD

73

奇怪的是,即使这是很重要的问题,多年来也没有人回答问题的最底层部分data.frame-s只是具有正确属性的列表,因此,如果您有大量数据,则不想使用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::setattrdata.table::setattr不会。
mnel 2012年

6
也许您使用了错误的命令?如果使用,则没有副本df=scan(...); names(df)=...; attr...; class...-请参阅tracemem()(在R 2.15.2中进行测试)
Simon Urbanek

3
您能否详细说明如何按列将大数据转储到二进制文件中?
dabsingh

32

以前曾在R-Help询问过此内容,因此值得回顾。

一个建议有使用readChar(),然后做与结果字符串操作strsplit()substr()。您可以看到readChar涉及的逻辑远少于read.table。

我不知道这里是否存在内存问题,但是您可能还想看看HadoopStreaming软件包。这使用HadoopHadoop是一个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)

这里的基本思想是将数据导入分成多个块。您甚至可以使用一个并行框架(例如snow),并通过对文件进行分段来并行运行数据导入,但是对于大数据集却无济于事,因为您会遇到内存限制,这就是为什么map-reduce是更好的方法。


我只是做了一个快速测试,由于某些莫名其妙的原因,readChar的确比readLines快得多。但是,与简单的C测试相比,它作为sin仍然很慢。在读取100兆的简单任务中,R比C慢5到10倍
Jonathan Chang

1
不明白你的意思。Hadoop的重点是处理非常大的数据,这就是问题所在。
Shane 2010年

1
尽管名称如此,hsTableReader本身与Hadoop无关,它是用于分段处理大数据的。它一次从con读取一行大块,然后将每个大块作为data.frame传递给FUN进行处理。使用ignoreKey = FALSE时,它会通过键(第一列中的条目)进行一些额外的分组,这与Map / Reduce方法有关。
DavidR

你好 您将如何使用此Hadoop数据作为其他软件包(例如Zoo)的输入,这些软件包旨在与所有数据同时使用?
skan,

10

另一种方法是使用该vroom程序包。现在在CRAN上。 vroom不会加载整个文件,它会索引每个记录的位置,并在以后使用时读取。

只支付您使用的费用。

介绍弗鲁姆开始使用弗鲁姆弗鲁姆基准

基本概述是,初次读取大文件会更快,而对数据的后续修改可能会稍慢。因此,根据您的用途,它可能是最佳选择。

请从下面的vroom基准中查看一个简化的示例,要看的关键部分是超快速的读取时间,但是诸如聚合等的操作会稍微减少。

package                 read    print   sample   filter  aggregate   total
read.delim              1m      21.5s   1ms      315ms   764ms       1m 22.6s
readr                   33.1s   90ms    2ms      202ms   825ms       34.2s
data.table              15.7s   13ms    1ms      129ms   394ms       16.3s
vroom (altrep) dplyr    1.7s    89ms    1.7s     1.3s    1.9s        6.7s

5

小小的附加点值得一提。如果文件很大,则可以使用(在bedGraph工作目录中文件的名称在哪里)动态计算行数(如果没有标题):

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

然后,您可以在中使用它read.csvread.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大得多的东西,它很小。但是我经常发现我只想在我从多个数据库查询时创建并保存内存密集型图形。在将来的32 GB笔记本电脑中,其中一些类型的内存问题将消失。但是使用数据库保存数据,然后使用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)

3

我正在使用新产品快速读取数据 arrow程序包。它似乎还处于早期阶段。

具体来说,我使用的是实木复合地板柱状格式。这会转换回data.frame R中的a,但如果不这样做,则可以得到更深的加速。这种格式很方便,因为它也可以在Python中使用。

我主要的用例是在相当受限制的RShiny服务器上。由于这些原因,我更喜欢将数据附加到应用程序(即,不使用SQL),因此需要较小的文件大小和速度。

该链接的文章提供了基准测试和良好的概述。我在下面引用了一些有趣的观点。

https://ursalabs.org/blog/2019-10-columnar-perf/

文件大小

也就是说,Parquet文件的大小甚至是压缩后的CSV的一半。Parquet文件如此之小的原因之一是由于字典编码(也称为“字典压缩”)。与使用通用字节压缩程序(如LZ4或ZSTD(以FST格式使用))相比,字典压缩可以产生更好的压缩效果。Parquet旨在产生非常小的文件,可以快速读取。

读取速度

通过输出类型进行控制时(例如,将所有R data.frame输出彼此进行比较),我们可以看到Parquet,Feather和FST的性能在相对较小的范围内。pandas.DataFrame输出也是如此。data.table :: fread在1.5 GB的文件大小上具有惊人的竞争力,但在2.5 GB的CSV上却落后于其他文件。


独立测试

我对1,000,000行的模拟数据集执行了一些独立的基准测试。基本上,我整理了一堆东西来尝试挑战压缩。我还添加了一个简短的文本字段,其中包含随机单词和两个模拟因子。

数据

library(dplyr)
library(tibble)
library(OpenRepGrid)

n <- 1000000

set.seed(1234)
some_levels1 <- sapply(1:10, function(x) paste(LETTERS[sample(1:26, size = sample(3:8, 1), replace = TRUE)], collapse = ""))
some_levels2 <- sapply(1:65, function(x) paste(LETTERS[sample(1:26, size = sample(5:16, 1), replace = TRUE)], collapse = ""))


test_data <- mtcars %>%
  rownames_to_column() %>%
  sample_n(n, replace = TRUE) %>%
  mutate_all(~ sample(., length(.))) %>%
  mutate(factor1 = sample(some_levels1, n, replace = TRUE),
         factor2 = sample(some_levels2, n, replace = TRUE),
         text = randomSentences(n, sample(3:8, n, replace = TRUE))
         )

读和写

写入数据很容易。

library(arrow)

write_parquet(test_data , "test_data.parquet")

# you can also mess with the compression
write_parquet(test_data, "test_data2.parquet", compress = "gzip", compression_level = 9)

读取数据也很容易。

read_parquet("test_data.parquet")

# this option will result in lightning fast reads, but in a different format.
read_parquet("test_data2.parquet", as_data_frame = FALSE)

我测试了在一些竞争选择中读取此数据的方法,并且得到的结果与上述文章的预期稍有不同。

基准测试

该文件远不及基准测试文章那么大,所以也许是不同的。

测验

  • rds: test_data.rds(20.3 MB)
  • parquet2_native :(具有更高压缩比的14.9 MB和as_data_frame = FALSE
  • parquet2: test_data2.parquet(压缩后为14.9 MB)
  • 实木复合地板: test_data.parquet(40.7 MB)
  • fst2: test_data2.fst(压缩后为27.9 MB,更高)
  • fst test_data.fst(76.8 MB)
  • fread2: test_data.csv.gz(23.6MB)
  • 读取 test_data.csv(98.7MB)
  • feather_arrow: test_data.feather(用读取的157.2 MB arrow
  • 羽毛: test_data.feather(阅读的157.2 MB feather

观察结果

对于这个特定文件,fread实际上是非常快的。我喜欢高度压缩parquet2测试中的小文件大小。我可能会花一些时间来处理本机数据格式,而不是data.frame如果我真的需要加快速度的话。

fst也是一个不错的选择。我将使用高度压缩的fst格式还是高度压缩的格式,parquet具体取决于我是否需要权衡速度或文件大小。


0

我觉得fread是一种更快的功能,而不是传统的read.table。指定其他属性,例如仅选择所需的列,将colclasses和string指定为因素将减少导入文件的时间。

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"))

0

我已经尝试了以上所有内容,[reader] [1]做得最好。我只有8GB RAM

循环20个文件,每个5gb,7列:

read_fwf(arquivos[i],col_types = "ccccccc",fwf_cols(cnpj = c(4,17), nome = c(19,168), cpf = c(169,183), fantasia = c(169,223), sit.cadastral = c(224,225), dt.sitcadastral = c(226,233), cnae = c(376,382)))
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.