Агрегирование в 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
выход.