Append多个大的data.table;使用colClasses和fread进行自定义数据强制类型转换;命名管道

8

这篇文章涉及多个bug报告/功能请求,但它们在分离后并不一定有意义。事先抱歉这篇巨大的文章。按照help(data.table)的建议在此发布。另外,我是R的新手,如果我在下面的代码中没有遵循最佳实践,请谅解。我正在努力学习。

1. rbindlist 在6*8GB文件上崩溃(我的内存为128GB)

首先,我想报告一下使用rbindlist将大型数据表附加到数据表会导致R崩溃(ubuntu 13.10,封装的R版本3.0.1-3ubuntu1,数据表从CRAN中的R中安装)。机器有128 GiB的RAM,因此,考虑到数据的大小,我不应该耗尽内存。

我的代码:

append.tables <- function(files) {
    moves.by.year <- lapply(files, fread)
    move <- rbindlist(moves.by.year)
    rm(moves.by.year)
    move[,week_end := as.Date(as.character(week_end), format="%Y%m%d")]
    return(move)
}

崩溃信息:

 append.tables crashes with this:
> system.time(move <- append.tables(files))
 *** caught segfault ***
address 0x7f8e88dc1d10, cause 'memory not mapped'

Traceback:
 1: rbindlist(moves.by.year)
 2: append.tables(files)
 3: system.time(move <- append.tables(files))

这里有6个文件,每个文件大约有8 GiB或1亿行,包含8个变量,使用制表符分隔。

2. fread 能否接受多个文件名?

无论如何,在这种情况下,我认为更好的方法是允许 fread 接受作为文件名向量的文件:

files <- c("my", "files", "to be", "appended")
dt <- fread(files)

在内部实现时,您可以比使用R语言时需要同时保留所有这些对象更节省内存。

3. colClasses会报错

我的第二个问题是,我需要为其中一种数据类型指定自定义转换处理程序,但该操作失败:

dt <- fread(tfile, colClasses=list(date="myDate"))
Error in fread(tfile, colClasses = list(date = "myDate")) : 
  Column name 'myDate' in colClasses not found in data

是的,在日期方面,简单地写:
    dt[,date := as.Date(as.character(date), format="%Y%m%d")]

工作正常。

然而,我有一个不同的用例,那就是在将字符转换为整数之前从其中一个数据列中删除小数点。这里的精度非常重要(因此我们需要使用整数类型),从 double 类型强制转换为整数会导致数据丢失。

现在,我可以通过一些 system() 调用来迂回处理此问题,将文件追加并通过一些 sed 魔法来处理它们(在这里被简化)(其中 tfile 是另一个临时文件):

if (has_header) {
    tfile2 <- tempfile()
    system(paste("echo fakeline >>", tfile2))
    system(paste("head -q -n1", files[[1]], ">>", tfile2))
    system(paste("tail -q -n+2", tfile2, paste(files, collapse=" "),
                 " | sed 's/\\.//' >>", tfile), wait=wait)
    unlink(tfile2)
} else {
    system(paste("cat", paste(files, collapse=" "), ">>", tfile), wait=wait)
}

但这需要额外的读/写循环。我有4 TiB的数据要处理,这需要大量的额外读写(不是全部放到一个data.table中,而是分成约1000个)。

4. fread认为命名管道是空文件

通常我会设置wait=TRUE。但我想尝试通过使用命名管道system('mkfifo', tfile)、设置wait=FALSE,并运行fread(tfile)来避免额外的读写循环。然而,fread抱怨管道是一个空文件:

system(paste("tail -q -n+2", tfile2, paste(files, collapse=" "),
             " | sed 's/\\.//' >>", tfile), wait=FALSE)
move <- fread(tfile)
Error in fread(tfile) : File is empty: /tmp/RtmpbxNI1L/file78a678dc1999

无论如何,这都有点像是一个黑客攻击。

如果我有愿望清单,代码将更简化

理想情况下,我希望能够像这样做:

setClass("Int_Price")
setAs("character", "Int_Price",
    function (from) {
        return(as.integer(gsub("\\.", "", from)))
    }
)

dt <- fread(files, colClasses=list(price="Int_Price"))

然后我会得到一个很好的、经过适当强制转换的数据表 data.table


太好了!感谢您抽出时间记录这些要点。如果您能将它们提交到data.table项目页面上,那就更有帮助了。向下滚动以获取bugsfeature requests的链接。在bugs中,除非我们有可重现的示例,否则很难对其进行任何处理。这么多问题很可能得不到答案(甚至被关闭),因为这违反了SO政策。 - Arun
您应该将这些作为单独的特性请求(FRs)/错误报告进行提交,即使它们在您看来是集体的。 - Arun
1个回答

7

更新:rbindlist错误已经在提交1100v1.8.11中修复。来自NEWS:

o 修复了一个罕见的segmentation fault问题,在超过250m行时发生(内存分配期间的整数溢出);关闭#5305。感谢Guenter J. Hitsch的报告。


如评论中所述,您应该单独提出不同的问题。但由于它们是这么好的观点,而且链接到最后的愿望中,好吧,我会一起回答。

1. rbindlist在6 * 8GB文件上崩溃(我有128GB内存)

请再次运行并更改以下行:

moves.by.year <- lapply(files, fread)

to

moves.by.year <- lapply(files, fread, verbose=TRUE)

并将结果发送给我。我认为问题不在于文件大小,而是与文件类型和内容有关。你说得对,freadrbindlist应该没有问题,在128GB的计算机上加载48GB的数据。如你所说,lapply应该返回48GB,然后rbindlist应该创建一个新的48GB的单表。由于96GB小于128GB,这应该可以在你的128GB机器上运行。1亿行×6是6亿行,远远低于20亿行的限制,因此应该没问题(data.table还没跟上R3中长向量支持的步伐,否则>2^31行也没问题)。

2. fread能接受多个文件名吗?

好主意。就像你说的那样,fread可以扫描所有6个文件,首先检测它们的类型并计算总行数。然后直接为600万行分配内存空间。这将避免不必要地占用48GB的RAM。在读取第一个文件之前,它还可能检测到第5个或第6个文件中的任何异常情况,因此在出现问题时会更快地返回。

我将把这个功能请求作为工单提交,并在此处发布链接。

3. colClasses会引发错误消息

当类型为list时,类型似乎出现在=的左边,列名称或位置的向量出现在右边。其目的是比read.csv中的colClasses更容易,后者只接受向量;以免反复重复输入"character"等。我本来以为在?fread中有更好的文档记录,但事实证明不是这样。我会检查一下。

因此,不要写:

fread(tfile, colClasses=list(date="myDate"))
Error in fread(tfile, colClasses = list(date = "myDate")) : 
    Column name 'myDate' in colClasses not found in data

正确的语法是:

fread(tfile, colClasses=list(myDate="date"))

鉴于您在问题中所说的,我理解您实际上想要的是:
fread(tfile, colClasses=list(character="date"))  # just fread accepts list

或者

fread(tfile, colClasses=c("date"="character"))   # both read.csv and fread

任何一种方法都能将名为“date”的列加载为字符,以便在强制转换之前可以操作它。 如果它确实只是日期,那么我还需要自动实现该强制转换。 您提到了 numeric 的精度,因此仅提醒您 integer64 也可以通过 fread 直接读取。
4. fread认为命名管道是空文件
希望这个问题现在已经解决了? fread 通过内存映射其输入来工作。 它可以接受非文件,例如http地址和连接(待定),而首先出于方便起见它会将完整的输入写入ramdisk以便从那里映射输入。 fread 之所以快速,与首先查看整个输入密切相关。

感谢您的帮助。我应该指出,read.table/csv实际上接受colClasses的列表。例如,传递colClasses=list(integer_var="character")就可以正常工作。我不得不说,fread偏离这种行为是令人惊讶的。 - James
fread在colClasses中无法正确处理自定义类 - James
@James 谢谢,我会看一下。colClasses=list(...)?read.csv中没有记录。我的理解是colClasses应该是一个字符向量。如果你传入了一个list并且它能工作,那么这很幸运,但不能保证将来仍然有效(我认为它目前正在将列表转换为字符向量)。你看到fread(,colClasses=list(character=150:200))提供的data.table的优势了吗?还有其他方法可以做到这一点吗? - Matt Dowle
我确实看到了广泛数据只需要指定每种类型一次的优势。话虽如此,由于新列排序可能引入错误,我尽量避免引用列号,除非是固定宽度的数据。话虽如此,我对差异感到惊讶,并认为这是一个错误。但你是对的。现在我仔细查看read.table,发现它并没有完全记录我所看到的行为。话虽如此,我更关心的是无法为列指定自定义处理程序(请参见我的先前评论)。这真的限制了我读取大型数据集的能力。 - James
@James 目前,所有自定义的colClasses都被读取为character,对吗?之后只需要使用for (i in thosecolumns) DT[,(i):=as.myclass(get(i))]或类似的set()方法就可以了。这样你就不必再费心了,直到我能够自动完成这个操作为止。如果可以的话,这样做可以吗? - Matt Dowle

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