R 将非常大的数据表列表合并到一个 data.table 中

标签 r memory data.table out-of-memory

我有一个非常大的列表,包含 13 个 data.tables(总共约 11.1 GB)。我有 16Gb 内存。将列表加载到内存后,我还剩下 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 将它们转换为干净的 data.tables,这就是我最终得到列表的方式。

purrr::map_dfr 似乎不起作用,仅将 lapply 包装在 rbindlist 中也不起作用。

最佳答案

可能有一种仅适用于 R 的方法可以实现此目的,但一种有效的方法是使用命令行(而非 R)工具来实现此目的。

为了这个答案而设置:

mt1 <- mtcars[1:3,c(1,2,3)]
mt2 <- mtcars[3:4,c(1,2,4)]
mt3 <- mtcars[5:10,c(1,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 行(快速/高效,因为每个文件只有 1 行)并rbindlist 它们,以便我们知道结果表应该是什么样子。当然,我们不需要保留任何实际值,只需保留列即可。

row1s <- rbindlist(lapply(filenames, function(fn) fread(fn, nrows = 1)),
                   use.names = TRUE, 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, on = intersect(names(row1s), 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, on = intersect(names(row1s), 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,如果需要的话)。我们需要 bashtailgrep 之一。目标是希望从这些 augm_mt 文件之一获取列标题,而不是其他文件。

如果我们天真地连接文件,我们将看到标题行在数据中间重复......对于 R,这意味着每一列都将是字符,可能不是什么你想要:

$ 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,如果你有的话,因为它是最不模糊的。)

  1. 如果您有 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 来抑制这种情况,转而使用连续行选项。

  2. 如果您知道一个或多个列名称​​从未在真实内容中出现,那么您可以执行以下操作之一:

    $ 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
    
  3. 更复杂,但应该比数字 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, on = intersect(names(row1s), names(dat))]
  fwrite(dat, file.path(dirname(fn), paste0("augm_", basename(fn))), 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 中,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/63876506/

相关文章:

Windows 中的 copymemory() 问题

regex - data.table集名与正则表达式结合使用

r - 如何拆分但忽略R中引用字符串中的分隔符?

r - R 中的百分位数结果与 MS Excel 不匹配

r - 将名称列表转换为 R 中的整数标签

R ggplot 不会显示图例颜色

R 使用 data.table 中的条件查找波高于给定值的频率和持续时间

r - 正确使用应用函数从列表元素组合生成列表

mysql - 使用 24GB 服务器的 MySQL 内存不足

python - 读取大 CSV 后跟 `.iloc` 切片列时出现 Pandas MemoryError