使用data.table包中的fread一次读取多个数据块。

32

我试图使用data.table包中的fread函数输入一个大型制表符分隔文件(约2GB)。但是,由于它太大了,无法完全放入内存。我尝试通过使用skipnrow参数分块输入:

chunk.size = 1e6
done = FALSE
chunk = 1
while(!done)
{
    temp = fread("myfile.txt",skip=(chunk-1)*chunk.size,nrow=chunk.size-1)
    #do something to temp
    chunk = chunk + 1
    if(nrow(temp)<2) done = TRUE
}
在上述情况下,我每次读入100万行进行计算,然后获取下一个100万行等等。这段代码的问题在于,在检索到每个块之后,fread需要从文件开头开始扫描,因为在每次循环迭代后,skip会增加100万行。结果是,在每个块之后,fread需要花费越来越长的时间才能实际到达下一个块,这使得效率非常低下。
有没有一种方法可以告诉fread每读取100万行就暂停一次,然后继续从那个点继续读取,而不必重新从开头开始?有解决方案吗,还是应该提出一个新的功能请求?

1
这里有一个类似的FR 链接。我也会链接到这篇文章。 - Arun
感谢指出并提供链接!看起来像是一个优先级很高的功能请求。 - FBC
今天遇到了同样的问题。 - user3375672
1
@Arun,新的Github页面上有提交需求吗?我找不到。 - Zach
现在是否已经可以实现这个功能了? - EDC
显示剩余3条评论
4个回答

22
您应该使用LaF软件包。这会在数据上引入一种指针,从而避免读取整个文件时出现的问题,尤其是对于非常大的数据。据我所知,在 data.table 软件包中使用 fread()需要知道总行数,这对于GB级别的数据需要花费时间。 使用LaF中的指针,您可以转到想要的每行;并且可以读取您可以在其上应用函数的数据块,并继续移动到下一个数据块。在我的小型PC上,我以10e6行的步长运行了25 GB的csv文件,并提取了需要的完全~5e6个观测值 - 每个10e6个块花费30秒。

更新:

library('LaF')
huge_file <- 'C:/datasets/protein.links.v9.1.txt'

#First detect a data model for your file:
model <- detect_dm_csv(huge_file, sep=" ", header=TRUE)

然后使用模型创建与您的文件的连接:

df.laf <- laf_open(model)

完成后,您可以执行各种操作,而无需像在data.table pckgs中那样知道文件的大小。例如,将指针放置在第100e6行并从此处读取1e6行数据:

goto(df.laf, 100e6)
data <- next_block(df.laf,nrows=1e6)

现在 data 包含了你的CSV文件中100e6行之后的1e6行数据。

你可以按照你内存能够承受的大小,将数据分块读取并只保留你需要的部分。例如,我的例子中的huge_file指向一个含有所有已知蛋白序列的文件,其大小超过27 GB - 在我的电脑上太大了。为了获取仅包含人类序列的数据,我使用了生物体ID进行了筛选,其中9606代表人类,这应该出现在变量protein1的开头。一种粗略的方法是将其放入一个简单的for循环中,并逐个读取数据块:

library('dplyr')
library('stringr')

res <- df.laf[1,][0,]
for(i in 1:10){
  raw <-
    next_block(df.laf,nrows=100e6) %>% 
    filter(str_detect(protein1,"^9606\\."))
  res <- rbind(res, raw)

    }

现在res 包含了经过筛选的人类数据。但更好的方式,特别是对于更复杂的操作(例如实时计算数据),函数process_blocks()以一个函数作为参数。因此,在该函数中,您可以对每个数据块执行任何操作。请阅读文档。


是的,请查看更新。 - user3375672
2
谢谢这个。我有一份872493862行61GB的文件,它的处理速度相当快。我试着使用"fread()"和"nrows"以及"skip"来实现同样的循环方法,但随着每次循环需要跳过更多的行,速度变得越来越慢。 - Adam Waring

10

你可以使用readr的read_*_chunked函数读取数据,并按块过滤数据。请参见示例此处此处

# Cars with 3 gears
f <- function(x, pos) subset(x, gear == 3)
read_csv_chunked(readr_example("mtcars.csv"), DataFrameCallback$new(f), chunk_size = 5)

我尝试了这种方法,但对于我的61GB文件来说速度太慢了。 - Adam Waring

8

相关选项是chunked包。以下是一个使用3.5 GB文本文件的示例:

library(chunked)
library(tidyverse)

# I want to look at the daily page views of Wikipedia articles
# before 2015... I can get zipped log files
# from here: hhttps://dumps.wikimedia.org/other/pagecounts-ez/merged/2012/2012-12/
# I get bz file, unzip to get this: 

my_file <- 'pagecounts-2012-12-14/pagecounts-2012-12-14'

# How big is my file?
print(paste(round(file.info(my_file)$size  / 2^30,3), 'gigabytes'))
# [1] "3.493 gigabytes" too big to open in Notepad++ !
# But can read with 010 Editor

# look at the top of the file 
readLines(my_file, n = 100)

# to find where the content starts, vary the skip value, 
read.table(my_file, nrows = 10, skip = 25)

我们将从文件块开始工作,可以像平常一样使用大多数dplyr动词:

# Let the chunked pkg work its magic! We only want the lines containing 
# "Gun_control". The main challenge here was identifying the column
# header
df <- 
read_chunkwise(my_file, 
               chunk_size=5000,
               skip = 30,
               format = "table",
               header = TRUE) %>% 
  filter(stringr::str_detect(De.mw.De.5.J3M1O1, "Gun_control"))

# this line does the evaluation, 
# and takes a few moments...
system.time(out <- collect(df))

在这里,我们可以像往常一样处理输出,因为它比输入文件要小得多:

# clean up the output to separate into cols, 
# and get the number of page views as a numeric
out_df <- 
out %>% 
  separate(De.mw.De.5.J3M1O1, 
           into = str_glue("V{1:4}"),
           sep = " ") %>% 
  mutate(V3 = as.numeric(V3))

 head(out_df)
    V1                                                        V2   V3
1 en.z                                               Gun_control 7961
2 en.z Category:Gun_control_advocacy_groups_in_the_United_States 1396
3 en.z          Gun_control_policy_of_the_Clinton_Administration  223
4 en.z                            Category:Gun_control_advocates   80
5 en.z                         Gun_control_in_the_United_Kingdom   68
6 en.z                                    Gun_control_in_america   59
                                                                                 V4
1 A34B55C32D38E32F32G32H20I22J9K12L10M9N15O34P38Q37R83S197T1207U1643V1523W1528X1319
2                                     B1C5D2E1F3H3J1O1P3Q9R9S23T197U327V245W271X295
3                                     A3B2C4D2E3F3G1J3K1L1O3P2Q2R4S2T24U39V41W43X40
4                                                            D2H1M1S4T8U22V10W18X14
5                                                             B1C1S1T11U12V13W16X13
6                                                         B1H1M1N2P1S1T6U5V17W12X12

#--------------------

似乎chunkedLaF的一个包装器。 - Artem Klevtsov
没错,我认为更加用户友好。 - Ben

7

fread()可以帮助您按块读取数据。

您在代码中犯的错误是,在循环期间更改函数中skip参数的大小时应保持nrow不变。

以下是我为我的数据编写的代码示例:

data=NULL

for (i in 0:20){
    data[[i+1]]=fread("my_data.csv",nrow=10000,select=c(1,2:100),skip =10000*i)   
}

您可以将以下代码插入到循环中:

start_time <- Sys.time()
#####something!!!!

end_time <- Sys.time()

end_time - start_time

检查每个循环平均花费的时间是否相似。可以使用另一个循环将数据按行组合,并在R中使用默认的rbind函数。示例代码可能如下所示:
new_data = data[[1]]

for (i in 1:20){
    new_data=rbind(new_data,data[[i+1]],use.names=FALSE)
}

合并成一个大数据集。

希望我的回答可以帮助你解决问题。

我使用这种方法,在大约8分钟内加载了一个包含2k+列、200k行的18Gb数据。


你是我的英雄,我使用了参数rnows但它实际上是nrow。 - Sergio

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