Агрегирование в R более 80K уникальных идентификаторов

Еще один вопрос новичка относительно больших данных. Я работаю с большим набором данных (3,5 м строк) с данными временных рядов. Я хочу создать data.table со столбцом, который находит первый раз, когда появляется уникальный идентификатор.

DF является data.table, df$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

Вот подвох. Я собираю более 80 тысяч уникальных идентификаторов. R задыхается. Что я могу сделать, чтобы оптимизировать мой подход?

4 ответа

Решение

Как уже упоминалось @Arun, реальный ключ (без каламбура) - это использование правильного data.table синтаксис, а не setkey,

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

В то время как уникальные идентификаторы 80k звучат как много, используя key особенность 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 или другая более сложная функция - просто операция подмножества:

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

Вот небольшой код, чтобы проверить, какое поведение занимает много времени

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 не был установлен

В дополнение к ответу Arun, есть кое-что с набором данных, схожим по размеру с 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 показывает, что он на 97% внутри min() для этого типа (т.е. не имеет ничего общего с 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

Ничто не должно занять 7 секунд data.table для этого крошечного размера! Он должен быть в 100 раз больше (4 ГБ), с j, а затем вы можете увидеть разницу между keyed by и ad hoc by.

Редактировать из Blue Magister:

Принимая во внимание ответ Мэтью Доула, есть разница между клавишами с ключом / без ключа.

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

Вот версия 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 позволяет избегать min() на тип POSIXct, который является очень медленной частью, чтобы повторить. Это не имеет ничего общего с data.table,

С использованием dfk из ответа Blue M:

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. См редактировать ответ Аруна для Rprof выход.

Другие вопросы по тегам