如何解决R将非常大的数据表列表合并为一个data.table
我有一个非常庞大的列表,包含13个data.tables(总计〜11.1 Gb)。我有16Gb的RAM。将列表加载到内存后,我还有5 GB的RAM。
我需要将它们合并为一个data.table
。由于它的data.table::rbindlist
参数,我更愿意使用fill = TRUE
(我的一些data.tables包含其他列没有的列-我需要用NA填充)。
问题是这需要5 GB以上的RAM才能完成,而我无法合并列表。好像我已经将数据加载到内存中了,合并的data.table
不会再大了。我只需要弄清楚是否有一种方法可以完成操作,而不必将整个列表复制到内存(占用22GB的RAM)中来执行rbindlist
。
为了首先列出该列表,我运行的lapply
如下所示:
df <- lapply(fs::dir_ls(dir),function(file) {
clean_data(file)
})
我正在获取.csv
文件的列表,并通过lapply
将它们转换为clean data.tables,这就是我最终得到的列表的原因。
purrr::map_dfr
似乎无效,也没有将lapply
包装在rbindlist
中。
解决方法
可能只有R方式可以做到这一点,但一种有效的方式是使用命令行(而非R)工具来实现。
设置此答案的目的:
mt1 <- mtcars[1:3,c(1,2,3)]
mt2 <- mtcars[3:4,4)]
mt3 <- mtcars[5:10,3,4)]
combined <- rbindlist(list(mt1,mt2,mt3),use.names = TRUE,fill = TRUE)
combined
# mpg cyl disp hp
# 1: 21.0 6 160.0 NA
# 2: 21.0 6 160.0 NA
# 3: 22.8 4 108.0 NA
# 4: 22.8 4 NA 93
# 5: 21.4 6 NA 110
# 6: 18.7 NA 360.0 175
# 7: 18.1 NA 225.0 105
# 8: 14.3 NA 360.0 245
# 9: 24.4 NA 146.7 62
# 10: 22.8 NA 140.8 95
# 11: 19.2 NA 167.6 123
write.table(mt1,"mt1.tsv",row.names = FALSE)
write.table(mt2,"mt2.tsv",row.names = FALSE)
write.table(mt3,"mt3.tsv",row.names = FALSE)
现在我们知道数据的样子了,让我们以编程方式获取文件名:
filenames <- list.files(".",pattern = "^mt.*\\.tsv",full.names = TRUE)
filenames
# [1] "./mt1.tsv" "./mt2.tsv" "./mt3.tsv"
从这里开始,让我们从每个文件中抓取第一行(快速/有效,因为每个文件只有1行),并rbindlist
,以便我们知道结果表的外观。当然,我们不需要保留任何实际值,只需保留列即可。
row1s <- rbindlist(lapply(filenames,function(fn) fread(fn,nrows = 1)),fill = TRUE)[0,]
row1s
# Empty data.table (0 rows and 4 cols): mpg,cyl,disp,hp
在此处进行演示时,请注意将0行表与原始表之一合并会呈现一致的架构。 (除非要验证一两个,否则不需要对真实数据进行此操作。)
row1s[mt1,on = intersect(names(row1s),names(mt1))]
# mpg cyl disp hp
# 1: 21.0 6 160 NA
# 2: 21.0 6 160 NA
# 3: 22.8 4 108 NA
row1s[mt2,names(mt2))]
# mpg cyl disp hp
# 1: 22.8 4 NA 93
# 2: 21.4 6 NA 110
目标是以编程方式对所有文件执行此操作:
# iterate through each file: read,left-join,write
for (fn in filenames) {
dat <- fread(fn)
dat <- row1s[dat,names(dat))]
fwrite(dat,file.path(dirname(fn),paste0("augm_",basename(fn))),sep = "\t")
}
newfilenames <- list.files(".",pattern = "^augm_mt.*\\.tsv$",full.names = TRUE)
newfilenames
# [1] "./augm_mt1.tsv" "./augm_mt2.tsv" "./augm_mt3.tsv"
要验证新文件看起来是否一致,请查找双\t
(表示空数据,即导入时为NA
)
# double-\t indicates an empty field
lapply(newfilenames,readLines,n = 2)
# [[1]]
# [1] "mpg\tcyl\tdisp\thp" "21\t6\t160\t"
# [[2]]
# [1] "mpg\tcyl\tdisp\thp" "22.8\t4\t\t93"
# [[3]]
# [1] "mpg\tcyl\tdisp\thp" "18.7\t\t360\t175"
现在我们有了这个,让我们转到命令提示符(在Windows,git-bash或仅在Windows的bash上,如果需要的话)。我们需要bash
和tail
或grep
中的一个。目的是希望从这些augm_mt
文件之一中获得列标题,而从其他文件中都没有。
如果我们天真地连接文件,我们会看到标题行在数据中间重复出现,并且使用R,这意味着每一列都是character
,可能不是您想要的:
$ cat augm_mt1.tsv augm_mt2.tsv
mpg cyl disp hp
21 6 160
21 6 160
22.8 4 108
mpg cyl disp hp
22.8 4 93
21.4 6 110
根据您拥有的工具以及对数据内容的信任程度,可以选择三个选项来避免这种情况。 (我建议您使用数字1,tail
,因为它含糊不清。)
-
如果您有
tail
,则可以为每个文件“从第2行开始”(跳过第1行):$ cat augm_mt2.tsv mpg cyl disp hp 22.8 4 93 21.4 6 110 $ tail -n +2 augm_mt2.tsv 22.8 4 93 21.4 6 110
如果您在多个文件上运行此命令,它倾向于在文件名前加上每组尾行(尝试一下),我们将通过添加
-q
选项来抑制使用连续行。 -
如果您知道一个或多个列名称在实际内容中从未出现过 ,则可以执行以下操作之一:
$ grep -v mpg augm_mt2.tsv 22.8 4 93 21.4 6 110 $ grep -v 'mpg.*cyl.*disp' augm_mt3.tsv 18.7 360 175 18.1 225 105 14.3 360 245 24.4 146.7 62 22.8 140.8 95 19.2 167.6 123
-
更复杂,但比数字2中的“手写正则表达式”更安全。
$ HDR=$(head -n 1 augm_mt2.tsv) $ grep -F "$HDR" -v augm_mt2.tsv 22.8 4 93 21.4 6 110
(
-F
的意思是“固定字符串”,因此不尝试进行正则表达式匹配。这是最安全的,因为诸如列名中的句点之类的内容可能会带来潜在的风险。远程但非零。)
无论选择哪种方式,这都是将这三个文件合并为一个大文件以读回R的方式:
$ { head -n 1 augm_mt1.tsv ; tail -q -n +2 augm_*.tsv ; } > alldata_mt.tsv
head -n 1
仅输出标题行,不输出数据,因此在下一条命令中更容易执行augm_*.tsv
。 (否则,我们需要找到一种方法来做所有事,但要先做。)
现在我们可以通过一个命令将其读回R:
fread("alldata_mt.tsv")
# mpg cyl disp hp
# 1: 21.0 6 160.0 NA
# 2: 21.0 6 160.0 NA
# 3: 22.8 4 108.0 NA
# 4: 22.8 4 NA 93
# 5: 21.4 6 NA 110
# 6: 18.7 NA 360.0 175
# 7: 18.1 NA 225.0 105
# 8: 14.3 NA 360.0 245
# 9: 24.4 NA 146.7 62
# 10: 22.8 NA 140.8 95
# 11: 19.2 NA 167.6 123
并通过此微数据进行验证:
all.equal(fread("alldata_mt.tsv"),combined)
# [1] TRUE
替代,它使中间文件没有列标题,因此我们不必在周围跳舞:
for (fn in filenames) {
dat <- fread(fn)
dat <- row1s[dat,sep = "\t",col.names = FALSE)
}
然后使用bash:
$ cat augm_*tsv > alldata2_mt.tsv
然后再次进入R,
fread("alldata2_mt.tsv",header = FALSE)
# V1 V2 V3 V4
# 1: 21.0 6 160.0 NA
# 2: 21.0 6 160.0 NA
# 3: 22.8 4 108.0 NA
# 4: 22.8 4 NA 93
# 5: 21.4 6 NA 110
# 6: 18.7 NA 360.0 175
# 7: 18.1 NA 225.0 105
# 8: 14.3 NA 360.0 245
# 9: 24.4 NA 146.7 62
# 10: 22.8 NA 140.8 95
# 11: 19.2 NA 167.6 123
...,您将必须知道名称才能重新分配它们。这种方法似乎可以减少工作量,但是确实确实有可能不经意地更改列名的顺序。上面第一个在所有文件中保留列名的方法可以避免潜在的错误操作。
,这是一种R data.table方法。在概念上与@ r2evans答案类似,但仅使用R,我们可以从将所有文件串联到单个csv中,并填充所有列来开始:
首先读取所有文件的列名,然后创建唯一名称的向量
library(data.table)
fnames = list.files(pattern = 'dt.+csv')
fcols = lapply(fnames,fread,nrows=0)
fcols = sapply(fcols,names)
fcols = unique(as.vector(fcols))
现在将每个文件的数据添加到单个csv中,并将缺少的值替换为NA。注意,可能不需要remove(fdt); gc()
行,因为fdt被重新分配并且R 应该为您处理此操作。我添加它们只是为了确保可以正常释放所有不再使用的内存。
for (f in fnames) {
fdt = fread(f)
fdt[,(setdiff(fcols,names(fdt))) := NA]
fwrite(fdt[,.SD,.SDcols = fcols],'all.csv',append = T)
remove(fdt)
gc()
}
然后我们只读取一个文件
fdt = fread('all.csv')
一些虚拟可复制数据文件
set.seed(1)
dt1 = data.table()[,(sample(letters[1:5],3)) := sample(10)]
dt2 = data.table()[,3)) := sample(10)]
dt3 = data.table()[,3)) := sample(10)]
dt4 = data.table()[,3)) := sample(10)]
fwrite(dt1,'dt1.csv')
fwrite(dt2,'dt2.csv')
fwrite(dt3,'dt3.csv')
fwrite(dt4,'dt4.csv')
remove(dt1,dt2,dt3,dt4)
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。