【发布时间】:2018-06-22 07:58:04
【问题描述】:
给定data.table 中的任意列名列表,我想将这些列的内容连接成单个字符串,存储在新列中。我需要连接的列并不总是相同的,因此我需要动态生成表达式。
我偷偷怀疑我使用 eval(parse(...)) 调用的方式可以用更优雅的方式替换,但下面的方法是迄今为止我能得到的最快的方法。
对于 1000 万行,此示例数据大约需要 21.7 秒(base R paste0 需要稍长一些 - 23.6 秒)。我的实际数据有 18-20 列被连接起来,多达 1 亿行,所以减速变得有点不切实际。
有什么想法可以加快速度吗?
当前方法
library(data.table)
library(stringi)
RowCount <- 1e7
DT <- data.table(x = "foo",
y = "bar",
a = sample.int(9, RowCount, TRUE),
b = sample.int(9, RowCount, TRUE),
c = sample.int(9, RowCount, TRUE),
d = sample.int(9, RowCount, TRUE),
e = sample.int(9, RowCount, TRUE),
f = sample.int(9, RowCount, TRUE))
## Generate an expression to paste an arbitrary list of columns together
ConcatCols <- c("x","a","b","c","d","e","f","y")
PasteStatement <- stri_c('stri_c(',stri_c(ConcatCols,collapse = ","),')')
print(PasteStatement)
给予
[1] "stri_c(x,a,b,c,d,e,f,y)"
然后使用以下表达式连接列:
DT[,State := eval(parse(text = PasteStatement))]
输出样本:
x y a b c d e f State
1: foo bar 4 8 3 6 9 2 foo483692bar
2: foo bar 8 4 8 7 8 4 foo848784bar
3: foo bar 2 6 2 4 3 5 foo262435bar
4: foo bar 2 4 2 4 9 9 foo242499bar
5: foo bar 5 9 8 7 2 7 foo598727bar
分析结果
更新 1:fread、fwrite 和 sed
按照@Gregor 的建议,尝试使用sed 在磁盘上进行连接。感谢 data.table 超快的fread 和fwrite 函数,我能够将列写入磁盘,使用 sed 消除逗号分隔符,然后在大约 18.3 秒内读回后处理的输出 -- 切换的速度不够快,但还是一个有趣的切线!
ConcatCols <- c("x","a","b","c","d","e","f","y")
fwrite(DT[,..ConcatCols],"/home/xxx/DT.csv")
system("sed 's/,//g' /home/xxx/DT.csv > /home/xxx/DT_Post.csv ")
Post <- fread("/home/xxx/DT_Post.csv")
DT[,State := Post[[1]]]
18.3 秒总时间的细分(无法使用 profvis,因为 sed 对 R 分析器不可见)
-
data.table::fwrite()- 0.5 秒 -
sed- 14.8 秒 -
data.table::fread()- 3.0 秒 -
:=- 0.0 秒
如果不出意外,这证明了 data.table 作者在磁盘 IO 性能优化方面所做的大量工作。 (我用的是1.10.5开发版,给fread增加了多线程,fwrite多线程已经有一段时间了。
一个警告:如果有一种解决方法可以使用fwrite 和@Gregor 在下面的另一条评论中建议的空白分隔符来写入文件,那么这种方法可以合理地缩减为 ~ 3.5秒!
对此切线的更新:分叉 data.table 并注释掉需要大于长度 0 的分隔符的行,却神秘地得到了一些空格?在导致一些段错误试图弄乱C 内部之后,我暂时把它放在了冰上。理想的解决方案不需要写入磁盘并将所有内容都保存在内存中。
更新 2:sprintf 用于整数特定情况
这里的第二个更新:虽然我在原始使用示例中包含了字符串,但我的实际用例专门连接整数值(根据上游清理步骤,始终可以假定为非 null)。
由于用例非常具体并且与原始问题不同,我不会直接将时间与之前发布的时间进行比较。然而,一个要点是,虽然stringi 很好地处理了许多字符编码格式、混合向量类型而无需指定它们,并且开箱即用地进行了一堆错误处理,但这确实增加了一些时间(这可能是在大多数情况下都值得)。
通过使用基本 R 的 sprintf 函数并让它预先知道所有输入都是整数,我们可以为 500 万行计算 18 个整数列减少大约 30% 的运行时间。 (20.3 秒而不是 28.9)
library(data.table)
library(stringi)
RowCount <- 5e6
DT <- data.table(x = "foo",
y = "bar",
a = sample.int(9, RowCount, TRUE),
b = sample.int(9, RowCount, TRUE),
c = sample.int(9, RowCount, TRUE),
d = sample.int(9, RowCount, TRUE),
e = sample.int(9, RowCount, TRUE),
f = sample.int(9, RowCount, TRUE))
## Generate an expression to paste an arbitrary list of columns together
ConcatCols <- list("a","b","c","d","e","f")
## Do it 3x as many times
ConcatCols <- c(ConcatCols,ConcatCols,ConcatCols)
## Using stringi::stri_c ---------------------------------------------------
stri_joinStatement <- stri_c('stri_join(',stri_c(ConcatCols,collapse = ","),', sep="", collapse=NULL, ignore_null=TRUE)')
DT[, State := eval(parse(text = stri_joinStatement))]
## Using sprintf -----------------------------------------------------------
sprintfStatement <- stri_c("sprintf('",stri_flatten(rep("%i",length(ConcatCols))),"', ",stri_c(ConcatCols,collapse = ","),")")
DT[,State_sprintf_i := eval(parse(text = sprintfStatement))]
生成的语句如下:
> cat(stri_joinStatement)
stri_join(a,b,c,d,e,f,a,b,c,d,e,f,a,b,c,d,e,f, sep="", collapse=NULL, ignore_null=TRUE)
> cat(sprintfStatement)
sprintf('%i%i%i%i%i%i%i%i%i%i%i%i%i%i%i%i%i%i', a,b,c,d,e,f,a,b,c,d,e,f,a,b,c,d,e,f)
更新 3:R 不必很慢。
根据@Martin Modrák 的回答,我根据data.table 内部专门针对专门的“个位整数”案例:fastConcat 组装了一个小马包。 (不要很快在 CRAN 上寻找它,但您可以通过从 github repo 安装来使用它,风险自负,msummersgill/fastConcat。)
如果有人更了解c,这可能会进一步改进,但目前,它在 2.5 秒 内运行与更新 2 相同的情况 - 大约 8x 比 sprintf() 快,并且比我最初使用的 stringi::stri_c() 方法快 11.5x。
对我来说,这凸显了在 R 中一些最简单的操作的性能改进的巨大机会比如基本的字符串向量连接 和更好的调整 c。我猜像@Matt Dowle 这样的人已经看到这个很多年了——要是他有时间重写所有R,而不仅仅是data.frame。
【问题讨论】:
-
所有
stri_c立即都是一个用于连接字符串的C++ 函数。我认为你无法超越它在 R 中的性能。即使paste可以非常快速地编译代码,因此它的性能几乎一样好。 -
使用命令行工具对数据进行预处理或后处理可能对您有用吗?或者在 SQL 或 Hadoop 中连接数据,或者您正在加载它?
-
几个想法:(a) 在从 Hadoop 中提取数据时合并列。 Hive、Pig 和 Spark 都支持列连接(据我所知)。 (b) 不幸的是,
fread不允许使用空白分隔符,但readr::write_delim可以。它可能太慢了,但值得一试。 (c)sed可能是您可以从命令行执行的最快操作,但 answers to this question 建议您可以使用不同的语法获得一些加速,特别是如果您复制文件而不是在原地编辑它。 -
(d) 不知道这是否可行,但看起来
fwrite中的单行输入检查使您无法将""指定为分隔符。您可以尝试使用fixInNamespace删除该行,然后看看它是否允许您使用fwrite和sep = ""。我以前从未使用过fixInNamespace,但这应该是可行的。悬而未决的问题是sep不是空字符串是否有更深层次的原因。 -
提交FR支持
sep = ""imo。
标签: r data.table concatenation