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

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


当前回答

这个问题之前在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是一种更好的方法。

其他回答

Often times I think it is just good practice to keep larger databases inside a database (e.g. Postgres). I don't use anything too much larger than (nrow * ncol) ncell = 10M, which is pretty small; but I often find I want R to create and hold memory intensive graphs only while I query from multiple databases. In the future of 32 GB laptops, some of these types of memory problems will disappear. But the allure of using a database to hold the data and then using R's memory for the resulting query results and graphs still may be useful. Some advantages are:

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

我正在阅读数据非常快地使用新的箭头包。它似乎还处于相当早期的阶段。

具体来说,我使用的是拼花柱状格式。这将转换回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行的模拟数据集上执行了一些独立的基准测试。基本上,我打乱了一堆东西,试图挑战压缩。我还添加了一个随机单词和两个模拟因素的简短文本字段。

Data

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。拼花地板(14.9 MB压缩更高) 拼花:test_data。拼花(40.7 MB) fst2: test_data2。fst (27.9 MB高压缩) 置:test_data。fst (768 MB) fread2: test_data.csv.gz (23.6MB) test_data.csv (987 mb) feather_arrow: test_data。羽毛(157.2 MB带箭头读取) 羽毛:test_data。羽毛(157.2 MB用羽毛阅读)

观察

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

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

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

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

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

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

这个问题之前在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是一种更好的方法。