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

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

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

264993 次浏览

这是几年后的一次更新

这个答案是旧的,而R已经继续了。调整read.table以运行得更快一点几乎没有什么好处。你的选择是:

  1. 使用vroom从tidyverse包vroom将数据从csv/制表符分隔的文件直接导入到R tibble。看到# EYZ3。

  2. data.table中使用fread将数据从csv/制表符分隔的文件直接导入r。参见mnel的回答

  3. readr中使用read_table(从2015年4月开始在CRAN上)。这很像上面的fread。链接中的自述解释了两个功能之间的区别(readr目前声称“慢1.5-2倍”;比# EYZ4)。

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

  5. 试着在数据库中存储尽可能多的数据,而不是平面文件。(作为一种更好的永久存储介质,数据以二进制格式传递给R,这更快)sqldf包中的read.csv.sql,如JD Long的回答所述,将数据导入临时SQLite数据库,然后将其读入R。另见:sqldf0包,以及sqldf1页面的反向依赖部分。sqldf2提供了一个数据类型,它假装是一个数据帧,但实际上是一个MonetDB,从而提高了性能。使用sqldf3函数导入数据。sqldf4允许您直接使用存储在几种类型的数据库中的数据。

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


原来的答案

无论你是否使用read,都有一些简单的事情可以尝试。表或扫描。

  1. 设置nrows=数据中的记录数 (nmaxscan中)。

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

  3. read.table中使用colClasses显式定义每个列的类。

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

如果这些都不起作用,那么使用分析包来确定哪些行减慢了速度。也许您可以根据结果编写一个简化版本的read.table

另一种方法是在将数据读入R之前过滤数据。

或者,如果问题是您必须定期读取它,那么使用这些方法读取一次数据,然后将数据帧保存为二进制blob,使用save saveRDS,然后下次使用load readRDS可以更快地检索它。

这之前是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)

这里的基本思想是将数据导入分解成块。您甚至可以使用一个并行框架(例如snow),并通过分割文件来并行运行数据导入,但对于大型数据集来说,这是没有帮助的,因为您将遇到内存限制,这就是为什么map-reduce是一种更好的方法。

一开始我没有看到这个问题,几天后我问了一个类似的问题。我将记下我之前的问题,但我想在这里添加一个答案,以解释我是如何使用sqldf()来做到这一点的。

稍微讨论一下关于导入2GB或更多文本数据到R数据帧的最佳方法。昨天我写了一篇博客,关于使用sqldf()将数据导入SQLite作为暂存区,然后将它从SQLite吸到r中。这对我来说非常有用。我能够在<中拉入2GB(3列,40mm行)的数据;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'))

奇怪的是,多年来没有人回答这个问题的底部部分,尽管这是一个很重要的问题——# eyz0只是一个具有正确属性的列表,所以如果你有大量的数据,你不想使用as.data.frame或类似的列表。简单地将列表就地“转换”为数据帧要快得多:

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

这不会复制数据,所以它是即时的(不像所有其他方法)。它假设您已经相应地在列表中设置了names()

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

下面是一个使用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

从文件中读

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

一个小的附加点值得一提。如果你有一个非常大的文件,你可以在运行中计算行数(如果没有头)使用(其中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

很多时候,我认为在一个数据库中保存较大的数据库(例如Postgres)是一个很好的实践。我没有使用比(nrow * ncol) ncell = 10M更大的值,这个值非常小;但我经常发现,当我从多个数据库查询时,我希望R只创建和保存内存密集型的图形。在未来的32gb笔记本电脑中,其中一些类型的内存问题将会消失。但是使用数据库来保存数据,然后使用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)

而不是传统的读取。我觉得fread是一个更快的函数。 指定额外的属性,如只选择所需的列,指定colclasses和字符串作为因素,将减少导入文件的时间

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"))
另一种方法是使用vroom包。现在在CRAN。 vroom不会加载整个文件,它会索引每条记录所在的位置,并在稍后使用它时读取

只按使用付费。

参见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

我非常快地读取数据使用新的arrow包。它似乎还处于相当早期的阶段。

具体来说,我使用的是拼花柱状格式。这将转换回R中的data.frame,但如果不这样做,您可以获得更大的加速。这种格式很方便,因为它也可以从Python中使用。

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

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

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

文件大小

也就是说,Parquet文件甚至是gzip的CSV的一半大。Parquet文件如此小的原因之一是字典编码(也称为“字典压缩”)。字典压缩可以产生比使用LZ4或ZSTD(以FST格式使用)等通用字节压缩器更好的压缩效果。Parquet的设计目的是生成非常小的文件,以便快速读取。

读取速度

当通过输出类型控制时(例如,比较所有R data.frame输出),我们看到Parquet、Feather和FST的性能之间的差距相对较小。熊猫也是如此。DataFrame输出。数据。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

这个文件远没有基准测试文章那么大,所以这可能就是区别所在。

测试

  • # EYZ0 test_data。rds (20.3 MB)
  • parquet2_native: (14.9 MB高压缩和as_data_frame = FALSE)
  • # EYZ0 test_data2。拼花地板(14.9 MB压缩更高)
  • # EYZ0 test_data。拼花(40.7 MB)
  • # EYZ0 test_data2。fst (27.9 MB高压缩)
  • # EYZ0 test_data。fst (768 MB)
  • fread2: test_data.csv.gz (23.6MB)
  • 从文件中读: test_data.csv (987 mb)
  • # EYZ1 test_data。羽毛(157.2 MB读取arrow)
  • # EYZ1 test_data。羽毛(157.2 MB读取feather)

观察

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

这里fst也是一个很好的选择。我将使用高度压缩的fst格式或高度压缩的parquet格式,这取决于我是否需要速度或文件大小的权衡。

上面这些我都试过了,[1]做得最好。我只有8gb的内存

循环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)))

我想以最简单的形式贡献基于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,这只是感觉不对。在部署模型时,该对象可能难以使用并产生问题,等等。