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

时间:2013-01-29 19:22:03

标签: r data.table

关于大数据的另一个新手问题。我正在使用带有时间序列数据的大型数据集(3.5米行)。我想创建一个data.table,其中的列会在第一次显示唯一标识符时找到。

df是data.tabledf$timestamp是类POSIXct中的日期,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很窒息。我能做些什么来优化我的方法?

4 个答案:

答案 0 :(得分:6)

这里有一些代码可以测试哪种行为需要 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

答案 1 :(得分:5)

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

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

虽然80k的独特ID听起来很多,但使用key的{​​{1}}功能可以使其成为一个可管理的前景。

data.table

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

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

答案 2 :(得分:4)

作为Arunanswer的补充,这里的数据集大小与OP(3.5M行,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

马修编辑。

实际上,上述内容几乎与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然后你可以看到键入和ad hoc之间的区别。

从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

答案 3 :(得分:2)

这是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

马修编辑(主要是)

是的,这是因为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相比,旧学校方法看起来非常慢。始终在min()上花费在POSIXct类型上。请参阅Arun对Rprof输出的答案的编辑。