快速将大型表格作为数据帧读取

566
我有非常大的表格(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))))

有更好的方法吗?或者可能完全不同的解决问题的方法?

12个回答

497

几年后的更新

这个答案已经过时了,R语言已经有了新的发展。对read.table进行微调以提高速度几乎没有任何好处。你的选择有:

  1. 使用tidyverse包中的vroom,可以直接将csv/制表符分隔文件导入R tibble中。详见Hector's answer

  2. 使用data.table中的fread,可以直接将csv/制表符分隔文件导入R中。详见mnel's answer

  3. 使用readr中的read_table(自2015年4月起在CRAN上可用)。其与上述fread类似。链接中的readme解释了两个函数之间的区别(目前readr声称比data.table::fread“慢1.5-2倍”)。

  4. iotools中的read.csv.raw提供了第三种快速读取CSV文件的选择。

  5. 尽可能将数据存储在数据库中而不是平面文件中。(除了作为更好的永久存储介质外,数据以二进制格式传递到和从R中,速度更快。)JD Long's answer中描述了sqldf包中的read.csv.sql将数据导入临时SQLite数据库,然后将其读入R中。另请参见:RODBC包,以及DBI package页面的反向依赖部分。MonetDB.R提供了一个假装是数据框但实际上是MonetDB的数据类型,可以增加性能。使用其monetdb.read.csv函数导入数据。dplyr允许您直接使用存储在多种类型数据库中的数据。

  6. 将数据存储在二进制格式中也可以提高性能。使用saveRDS/readRDS(详见下文),h5rhdf5包进行HDF5格式,或使用fst包中的write_fst/read_fst


原始答案

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

  1. 设置nrows=您数据中的记录数(在scan中为nmax)。

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

  3. 使用read.table中的colClasses明确定义每列的类别。

  4. 在scan中设置multi.line=FALSE也可能提高性能。

如果这些方法都不起作用,则使用其中一个分析包来确定哪些行正在减慢速度。也许您可以根据结果编写一个简化版的read.table

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

或者,如果问题在于您需要定期读取它,则可以使用以下方法将数据仅读取一次,然后使用save saveRDS将数据框保存为二进制文件,下次可以使用load readRDS更快地检索它。


4
谢谢你的提示Richie。我进行了一些测试,似乎使用read.table函数的nrow和colClasses选项所获得的性能提升并不是很显著。例如,读取一个大约有700万行的表格,如果不使用这些选项,需要78秒,而使用这些选项则需要67秒。(请注意:该表格有1个字符列和4个整数列,并且我使用了comment.char=''和stringsAsFactors=FALSE参数进行读取)。尽可能使用save()和load()函数是一个很好的提示 - 一旦使用save()函数存储后,同样的表格只需12秒即可加载。 - eytan
2
“feather”包有一个新的二进制格式,可以很好地与Python的pandas数据框架配合使用。 - rsoren
5
我认为也许你需要再次更新有关“feather”软件包的帖子。对于读取数据来说,“feather”的速度比“fread”要快得多。例如,在一个4GB数据集上,我刚刚加载的“read_feather”比“fread”快了约4.5倍。但是在保存数据方面,“fwrite”仍然更快。https://blog.dominodatalab.com/the-r-data-i-o-shootout/ - Z boson
4
Feather 文件的大小比 RDS 大得多,且我认为它不支持压缩。RDS 文件为 216 MB,而 Feather 文件为 4GB。因此,feather 的读取速度更快,但它使用了更多的存储空间。 - Z boson
2
如果您需要将数据框存储在可以从R和Python访问的文件中,则“feather”是一个不错的选择。如果您只关心能够在R中读取数据,则最好使用“rds”。 - Richie Cotton
显示剩余5条评论

304
这是一个使用 data.table 1.8.7 中的 fread 的示例。
这些示例来自于 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

53
很棒的回答,这个基准测试在其他情境下也适用。使用fread在不到一分钟的时间内读取了一个4GB文件。之前尝试使用R基本函数读入,需要大约15个小时。 - Ari B. Friedman
1
我的基准测试表明,在data.table中使用read.csv可以获得更大的速度优势。请注意,data.table不是标准的R语言,而只是由其创建者在CRAN上“很好地”共享。它甚至没有被认为足够标准,以至于不能列入常见的R包列表,更不用说作为数据框的替代品。它有很多优点,但也有一些非常反直觉的方面。您可能希望使用as.data.frame(fread.csv("test.csv"))与该软件包一起返回到标准的R数据框世界。 - ivo Welch
3
@mnel,您能否重新运行基准测试并包含readr - jangorecki
2
第二个 @jangorecki。另外,考虑到 fread 现在有一些真正的竞争对手,为优化的 fread 使用添加基准测试可能会很有用 -- 指定 colClasses 等。 - MichaelChirico
1
@jangorecji @MichaelChirico 给出的代码是完全可复制的,因此模拟readr很容易...重新运行代码,在我的机器上,大多数结果的经过时间是原来的两倍甚至更快(尽管我正在通过网络运行它,并且更新版本已经有一段时间了)...使用readr时,我需要7秒,但第二次运行时也不到一秒钟(0.66秒),我怀疑在网络中存在某些缓存或瓶颈。对于这里展示的最快解决方案fread,在我的电脑上需要2秒进行比较(第一次运行需要8.69秒,因为某种原因较慢)。 - R. Prost

261

一开始我没有看到这个问题,几天后问了一个类似的问题。我将删除之前的问题,但我想在这里添加一个答案来解释我如何使用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
这种方法似乎不能正确处理带引号的数据(例如,如果所有字段都带引号且字段中的引号是双引号)。 sqldf文档涉及了这个问题,但似乎没有提出解决方案。 - hgcrpd
@JDLong 我可能在学校里睡过了这一节课,但是"40mm 行"是什么意思?毫米? - user1322720
1
@M代表千,所以MM代表千千或百万。我可能应该将其大写为MM。但是,如果您的受众多样化,几乎任何百万缩写都可能会让某些人感到困惑。在我过度冗长的尝试中,很抱歉我让它变得更加混乱! http://www.accountingcoach.com/blog/what-does-m-and-mm-stand-for - JD Long
很遗憾,read.cvs.sql 无法正确处理文件中的 NULL 或空字符串... - Chris
显示剩余3条评论

81

奇怪的是,多年来没有人回答问题的底部,尽管这是一个重要的问题——data.frame只是具有正确属性的列表,因此如果您有大量数据,不要使用as.data.frame或类似函数将其转换为数据框。直接在原地将列表“转换”为数据框速度更快:

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

这种方法不会复制数据,因此它是立即执行的(与所有其他方法不同)。它假定您已经根据列表设置了names()

[至于将大型数据加载到R中-个人而言,我通过按列将它们转储到二进制文件中,并使用readBin() - 这是迄今为止最快的方法(除了内存映射),仅受到磁盘速度的限制。与二进制数据相比,解析ASCII文件在本质上很慢(即使在C中也是如此)。


6
使用tracmem提示attr<-class<-在内部进行复制。 bit::setattrdata.table::setattr则不会这样做。 - mnel
6
可能您的顺序不正确?如果您使用了 df=scan(...); names(df)=...; attr...; class...,则不会复制 - 请参阅 tracemem()(在 R 2.15.2 中测试过)。 - Simon Urbanek
6
请问您能详细说明如何按列将大型数据导入二进制文件吗? - dabsingh
请问您能否提供一个例子吗? - Angelo

33

这个问题之前R-Help 中提出过, 所以值得回顾一下。

那里的一个建议是使用readChar(), 然后使用strsplit()substr()对结果进行字符串操作。你可以看到,与read.table相比,readChar涉及的逻辑要少得多。

我不知道内存是否是一个问题,但你可能也想看看 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)

基本思路是将数据导入分成块。您甚至可以使用其中一个并行框架(例如snow)并通过分段运行数据导入来并行运行,但对于大型数据集,这很可能不会有所帮助,因为您将遇到内存限制,这就是为什么map-reduce是更好的方法的原因。

我刚做了一个快速测试,readChar 似乎比 readLines 快得多,原因不明。然而,与简单的 C 测试相比,它仍然慢得像罪恶一样。在读取 100 兆字节的简单任务中,R 比 C 慢了约 5-10 倍。 - Jonathan Chang
1
不理解你的观点。Hadoop 的重点是处理非常大的数据,这也是问题所在。 - Shane
1
尽管名字中带有“Hadoop”,但hsTableReader与Hadoop本身没有任何关系,它是用于分块处理大数据的。它从con中一次读取一块行,并将每个块作为data.frame传递给FUN进行处理。如果ignoreKey=FALSE,则会按键(第一列中的条目)进行一些额外的分组,这与Map/Reduce方法相关。 - DavidR
你好。您如何将这个Hadoop数据用作其他包的输入,例如zoo,旨在同时处理所有数据? - skan

15

另一种选择是使用vroom包。现在已经在CRAN上了。 vroom不会加载整个文件,而是索引每个记录的位置,并在使用时稍后读取。

只为您使用的部分付费。

请参见介绍vroom开始使用vroomvroom基准测试

基本概述是,对于巨大文件的初始读取,速度更快,对数据的后续修改可能略慢。因此,根据您的使用情况,它可能是最佳选择。

请参见下面从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

10

我正在使用新的arrow包快速读取数据。它似乎处于相当早期的阶段。

具体来说,我正在使用parquet列格式。这将在R中转换回data.frame,但如果不这样做,您甚至可以获得更深层次的加速。这种格式很方便,因为它也可以从Python中使用。

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

这篇文章提供了基准测试和良好的概述。以下是一些有趣的引用。

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

文件大小

也就是说,Parquet 文件的大小甚至只有被 gzip 压缩后 CSV 文件的一半。使 Parquet 文件如此小的原因之一是采用了字典编码(也称“字典压缩”)。与使用通用字节压缩器(如 LZ4 或 ZSTD)相比,字典压缩可以产生更好的压缩效果(FST 格式中使用这些压缩器)。Parquet 的设计目标是生成非常小且读取速度快的文件。

读取速度

当按输出类型控制时(例如将所有 R 数据框输出进行比较),可以看到 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)

我测试了一下读取这些数据与几个竞争选项的结果,并且得到了与上面文章略有不同的结果,这是可以预料的。

benchmarking

这个文件远远不如基准文章那么大,也许这就是区别所在。

测试

  • rds: test_data.rds (20.3 MB)
  • parquet2_native:(14.9 MB,使用更高的压缩率和as_data_frame = FALSE
  • parquet2: test_data2.parquet(14.9 MB,使用更高的压缩率)
  • parquet: 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)
  • fread: test_data.csv(98.7MB)
  • feather_arrow: test_data.feather(使用arrow读取157.2 MB)
  • feather: test_data.feather(使用feather读取157.2 MB)

观察结果

对于这个特定的文件,fread非常快。我喜欢高度压缩的parquet2测试所带来的小文件大小。如果我真的需要加速,我可能会投入时间来使用本地数据格式而不是data.frame
在这里,fst也是一个很好的选择。根据我是否需要速度或文件大小权衡,我要么使用高度压缩的fst格式,要么使用高度压缩的parquet

7

值得一提的是,如果你有一个非常大的文件,你可以实时计算行数(没有标题)使用以下命令(其中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

6
通常情况下,将较大的数据库放在数据库中(例如Postgres)是一个好的实践方法。我没有使用超过(nrow * ncol) ncell = 10M的数据量,这很小;但我经常发现我希望R在查询多个数据库时创建和保持内存密集型图形。在未来的32 GB笔记本电脑中,一些这些类型的内存问题将消失。但是仍然有使用数据库来保存数据并使用R的内存来处理查询结果和图形的吸引力。其中一些优点是:
(1) 数据保持加载在您的数据库中。当您打开笔记本电脑时,只需在pgadmin中重新连接到您想要的数据库即可。
(2) R可以进行许多巧妙的统计和绘图操作,但是我认为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)

DuckDB 是一个相对较新的开源分析数据库,现在可以在 CRAN 上使用。它是一个非常小的软件包,同时支持类似 PostGres 的 SQL 命令。它还支持使用 SQL 命令查询 parquet 格式文件。 - San

1

我希望以最简单的形式提供基于Spark的解决方案:

# Test Data ---------------------------------------------------------------

set.seed(123)
bigdf <-
    data.frame(
        dim = sample(letters, replace = T, 4e7),
        fact1 = rnorm(4e7),
        fact2 = rnorm(4e7, 20, 50)
    )
tmp_csv <- fs::file_temp(pattern = "big_df", ext = ".csv")
readr::write_csv(x = bigdf, file = tmp_csv)

# Spark -------------------------------------------------------------------

# Installing if needed
# sparklyr::spark_available_versions()
# sparklyr::spark_install()

library("sparklyr")
sc <- spark_connect(master = "local")

# Uploading CSV
system.time(tbl_big_df <- spark_read_csv(sc = sc, path = tmp_csv))

Spark 产生了相当不错的结果:

>> system.time(tbl_big_df <- spark_read_csv(sc = sc, path = tmp_csv))
   user  system elapsed 
  0.278   0.034  11.747 

这是在配备32GB内存的MacBook Pro上进行的测试。

备注

Spark通常不应该能够在速度优化的软件包中“获胜”。尽管如此,我想用Spark提供一个答案:

  • 对于某些评论和回答,在使用Spark无法工作时可能是一个可行的替代方案
  • 从长远来看,将尽可能多的数据压入data.frame中可能会在稍后尝试在该对象上执行其他操作并击中架构性能极限时出现问题

我认为对于像这样的问题,其中任务是处理1e7或更多行,应该考虑使用Spark。即使可能可以将该数据“压入”单个data.frame中,但感觉并不正确。很可能那个对象将难以处理,并在部署模型等方面造成问题。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接