在 R 中聚合超过 80K 的唯一 ID

Posted

技术标签:

【中文标题】在 R 中聚合超过 80K 的唯一 ID【英文标题】:Aggregating in R over 80K unique ID's 【发布时间】:2013-01-29 19:22:03 【问题描述】:

另一个关于大数据的新手问题。我正在使用包含时间序列数据的大型数据集(3.5m 行)。我想创建一个 data.table ,其中有一列可以找到唯一标识符第一次出现的时间。

df 是 data.tabledf$timestampPOSIXct 类中的日期,df$id 是唯一的数字标识符。我正在使用以下代码:

# UPDATED - DATA KEYED
setkey(df, id)
sub_df<-df[,(min(timestamp)), by=list(id)] # Finding first timestamp for each unique ID

这就是问题所在。我正在汇总超过 80k 的唯一 ID。 R 窒息。我可以做些什么来优化我的方法?

【问题讨论】:

在哪个命令上 R 慢?从你的描述我看不出来。 您的data.table 是否已键入?如果没有,那将有很大帮助。 另外注意:您不需要在J 中说df$timestamp。只需min(timestamp) 就足够了。 为什么不使用 !duplicated(df$id) (仅当您的数据集按日期排序时)? @Arun 显然键入了data.table 【参考方案1】:

这里有一个小代码来测试哪种行为需要 LOT 时间

require(data.table)
dt <- data.table(sample(seq(as.Date("2012-01-01"), as.Date("2013-12-31"), 
          by="days"), 1e5, replace=T), val=sample(1e4, 1e5, replace = T))

FUN1 <- function() 
    out <- dt[, min(dt$V1), by=val]  # min of entire V1 for each group i.e. wrong


FUN2 <- function() 
    out <- dt[, min(V1), by=val]     # min of V1 within group as intended


require(rbenchmark)
> benchmark(FUN1(), FUN2(), replications = 1, order="elapsed")
#     test replications elapsed relative user.self sys.self user.child sys.child
# 2 FUN2()            1   0.271    1.000     0.242    0.002          0         0
# 1 FUN1()            1  38.378  141.616    32.584    4.153          0         0

很明显FUN2() 的速度非常快。请记住,在这两种情况下,都没有设置 KEY

【讨论】:

触摸!对于后代,在操作之前使用setkey 对函数进行基准测试与您的FUN2 相同。 不确定 FUN1 和 FUN2 的结果是否相同。 FUN1 不是每次都在整个 V1 上查找和重新查找相同的全局最小值吗?但 FUN2 得到了正确的答案(每组中的最小值)。 @MatthewDowle,是的。发现。它们是不相同的。但这是 OP 早些时候运行的,并认为他做得对。除了结果不正确之外,他的速度欠缺也是因为我想我应该说的。【参考方案2】:

正如@Arun 所提到的,真正的关键(不是双关语)是使用正确的data.table 语法而不是setkey

df[, min(timestamp), by=id]

虽然 80k 唯一 ID 听起来很多,但使用 data.tablekey 功能可以使其成为易于管理的前景。

setkey(df, id)

然后像以前一样处理。对于它的价值,您通常可以使用键的令人愉快的副作用,即排序。

set.seed(1)
dat <- data.table(x = sample(1:10, 10), y = c('a', 'b'))

    x y
 1:  3 a
 2:  4 b
 3:  5 a
 4:  7 b
 5:  2 a
 6:  8 b
 7:  9 a
 8:  6 b
 9: 10 a
10:  1 b

setkey(dat, y, x)

     x y
 1:  2 a
 2:  3 a
 3:  5 a
 4:  9 a
 5: 10 a
 6:  1 b
 7:  4 b
 8:  6 b
 9:  7 b
10:  8 b

那么min或者其他更复杂的函数只是一个子集运算:

dat[, .SD[1], by=y]

【讨论】:

(+1) 嗯.. 我现在对常见问题解答here 的第 3.2 节感到困惑。有什么想法吗? 虽然 data.table 在使用 ad-hoc 键时具有快速排序,但它不如键控表快,尤其是在键首先排序的情况下。 Dowle 先生或data.table 的其他大师可以进一步扩展。 我只是跑了,没有在data.table 上设置密钥,并且永远使用df$V1。请暂停。基准测试结束后我会写帖子。 立即查看帖子。你应该能够重现它。 贾斯汀,请参阅其他答案的编辑。 .SD[1] 是避免POSIXct 上的min() 的另一种方法,这就是您可能更快看到的原因。但原则上我希望这比df[, min(colDoubleType), by=id] 慢,因为避免了.SD(如果使用它,目前必须通过data.table 为每个组完全创建)。【参考方案3】:

作为对Arun 的answer 的补充,这里有一个与 OP(350 万行,80K ID)大小相似的数据集,表明键控/非键控聚合并没有太大的不同。所以加速可能是由于避免使用$ 运算符。

set.seed(10)
eg <- function(x) data.table(id=sample(8e4,x,replace=TRUE),timestamp=as.POSIXct(runif(x,min=ISOdatetime(2013,1,1,0,0,0) - 60*60*24*30, max=ISOdatetime(2013,1,1,0,0,0)),origin="1970-01-01"))
df <- eg(3.5e6)
dfk <- copy(df)
setkey(dfk,id)
require(microbenchmark)
microbenchmark(
    unkeyed = df[,min(timestamp),by=id][,table(weekdays(V1))]
    ,keyed = dfk[,min(timestamp),by=id][,table(weekdays(V1))]
    ,times=5
)
#Unit: seconds
#     expr      min       lq   median       uq      max
#1   keyed 7.330195 7.381879 7.476096 7.486394 7.690694
#2 unkeyed 7.882838 7.888880 7.924962 7.927297 7.931368

由 Matthew 编辑。

其实上面几乎完全和POSIXct这个类型有关。

> system.time(dfk[,min(timestamp),by=id])
   user  system elapsed 
   8.71    0.02    8.72 
> dfk[,timestamp:=as.double(timestamp)]  # discard POSIXct type to demonstrate
> system.time(dfk[,min(timestamp),by=id])
   user  system elapsed 
   0.14    0.02    0.15     # that's more like normal data.table speed

恢复到 POSIXct 并使用 Rprof 显示该类型在 min() 内的 97%(即与 data.table 无关):

$by.total
                total.time total.pct self.time self.pct
system.time           8.70    100.00      0.00     0.00
[.data.table          8.64     99.31      0.12     1.38
[                     8.64     99.31      0.00     0.00
min                   8.46     97.24      0.46     5.29
Summary.POSIXct       8.00     91.95      0.86     9.89
do.call               5.86     67.36      0.26     2.99
check_tzones          5.46     62.76      0.20     2.30
unique                5.26     60.46      2.04    23.45
sapply                3.74     42.99      0.46     5.29
simplify2array        2.38     27.36      0.16     1.84
NextMethod            1.28     14.71      1.28    14.71
unique.default        1.10     12.64      0.92    10.57
lapply                1.10     12.64      0.76     8.74
unlist                0.60      6.90      0.28     3.22
FUN                   0.24      2.76      0.24     2.76
match.fun             0.22      2.53      0.22     2.53
is.factor             0.18      2.07      0.18     2.07
parent.frame          0.14      1.61      0.14     1.61
gc                    0.06      0.69      0.06     0.69
duplist               0.04      0.46      0.04     0.46
[.POSIXct             0.02      0.23      0.02     0.23

注意dfk的对象大小:

> object.size(dfk)
40.1 Mb

data.table 中,对于这么小的尺寸,任何事情都不应该花费 7 秒!它需要大 100 倍 (4GB),j 没有缺陷,然后你可以看到 keyed by 和 ad hoc by 之间的区别。

从 Blue Magister 编辑:

考虑到 Matthew Dowle 的回答,键控/非键控命令之间存在差异。

df <- eg(3.5e6)
df[,timestamp := as.double(timestamp)]
dfk <- copy(df)
setkey(dfk,id)
require(microbenchmark)
microbenchmark(
    unkeyed = df[,min(timestamp),by=id][,table(weekdays(as.POSIXct(V1,origin="1970-01-01")))]
    ,keyed = dfk[,min(timestamp),by=id][,table(weekdays(as.POSIXct(V1,origin="1970-01-01")))]
    ,times=10
)
#Unit: milliseconds
#     expr      min       lq   median       uq       max
#1   keyed 340.3177 346.8308 348.7150 354.7337  358.1348
#2 unkeyed 886.1687 888.7061 901.1527 945.6190 1036.3326

【讨论】:

@MatthewDowle 感谢您清理它。我调整了我的基准以将POSIXct 命令移到聚合步骤之外。与以前相比,我看到了巨大的改进,键控 min 的速度比非键控 min 提高了 2 倍。【参考方案4】:

这是一个 data.table 版本

dtBy <- function(dt)
    dt[, min(timestamp), by=id]

有点老派,这是一个返回每个组的最小值的函数

minBy <- function(x, by) 
    o <- order(x)
    by <- by[o]
    idx <- !duplicated(by)
    data.frame(by=by[idx], x=x[o][idx])

并且对于BlueMagister的样本数据似乎具有合理的性能

> system.time(res0 <- dtBy(dt))
   user  system elapsed 
 11.165   0.216  11.894 
> system.time(res1 <- minBy(dt$timestamp, dt$id))
   user  system elapsed 
  4.784   0.036   4.836 
> all.equal(res0[order(res0$id),], res1[order(res1$by),],
+           check.attributes=FALSE)
[1] TRUE

由 Matthew 编辑(大部分)

是的,这是因为 minBy 避免了 POSIXct 类型上的 min(),这是重复的非常慢的部分。这与data.table无关。

使用 Blue M 的回答中的dfk

dfk[,timestamp:=as.double(timestamp)]  # discard POSIXct type to demonstrate

system.time(res0 <- dtBy(dfk))
   user  system elapsed 
   0.16    0.02    0.17 
system.time(res1 <- minBy(dfk$timestamp, dfk$id))
   user  system elapsed 
   4.87    0.04    4.92 

现在与 data.table 相比,老式方法看起来非常慢。所有时间都花在了 POSIXct 类型的min() 上。请参阅 Arun 对 Rprof 输出的回答的编辑。

【讨论】:

以上是关于在 R 中聚合超过 80K 的唯一 ID的主要内容,如果未能解决你的问题,请参考以下文章

SQL 聚合唯一对

是否可以在 MongoDB 聚合期间为每个文档创建唯一 ID?

将分组的聚合唯一列添加到熊猫数据框

SQL聚合函数选择唯一值

如何使用 MongoDB 聚合对唯一 ID 求和?

按唯一标识符聚合并将相关值连接成一个字符串[重复]