Операции со строками в data.table с использованием `by = .I`
Вот хорошее SO объяснение о строковых операциях вdata.table
Одна альтернатива, которая пришла мне в голову, заключается в использовании уникального id
для каждой строки, а затем применить функцию, используя by
аргумент. Как это:
library(data.table)
dt <- data.table(V0 =LETTERS[c(1,1,2,2,3)],
V1=1:5,
V2=3:7,
V3=5:1)
# create a column with row positions
dt[, rowpos := .I]
# calculate standard deviation by row
dt[ , sdd := sd(.SD[, -1, with=FALSE]), by = rowpos ]
Вопросы:
Есть ли веская причина не использовать этот подход? возможно, другие более эффективные альтернативы?
Почему используется
by = .I
не работает так же?dt[ , sdd := sd(.SD[, -1, with=FALSE]), by = .I ]
1 ответ
1) Ну, одна из причин не использовать его, по крайней мере, для rowsums
Примером является производительность и создание ненужного столбца. Сравните с опцией f2 ниже, которая почти в 4 раза быстрее и не нуждается в столбце rowpos:
dt <- data.table(V0 =LETTERS[c(1,1,2,2,3)], V1=1:5, V2=3:7, V3=5:1)
f1 <- function(dt){
dt[, rowpos := .I]
dt[ , sdd := rowSums(.SD[, 2:4, with=FALSE]), by = rowpos ] }
f2 <- function(dt){dt[, sdd := rowSums(dt[, 2:4, with=FALSE])]}
library(microbenchmark)
microbenchmark(f1(dt),f2(dt))
# Unit: milliseconds
# expr min lq mean median uq max neval cld
# f1(dt) 3.669049 3.732434 4.013946 3.793352 3.972714 5.834608 100 b
# f2(dt) 1.052702 1.085857 1.154132 1.105301 1.138658 2.825464 100 a
2) На ваш второй вопрос, хотя dt[, sdd := sum(.SD[, 2:4, with=FALSE]), by = .I]
не работает, dt[, sdd := sum(.SD[, 2:4, with=FALSE]), by = 1:NROW(dt)]
работает отлично. Учитывая, что согласно ?data.table
".I - целочисленный вектор, равный seq_len (nrow (x))", можно ожидать, что они эквивалентны. Разница, однако, заключается в том, что .I
для использования в j
, не в by
потому что это значение возвращается by
а не оценивается заранее.
Также можно ожидать (см. Комментарий на вопрос выше от @eddi), что by = .I
надо просто скинуть ошибку. Но этого не происходит, потому что загрузка data.table
пакет создает объект .I
в пространстве имен data.table, которое доступно из глобальной среды и чье значение NULL
, Вы можете проверить это, набрав .I
в командной строке. (Обратите внимание, то же самое относится к .SD
, .EACHI
, .N
, .GRP
, а также .BY
)
.I
# Error: object '.I' not found
library(data.table)
.I
# NULL
data.table::.I
# NULL
Результатом этого является то, что поведение by = .I
эквивалентно by = NULL
,
3) Хотя мы уже видели в части 1, что в случае rowSums
, который уже выполняет цикл по строкам, есть гораздо более быстрые способы, чем создание столбца rowpos. Но как насчет цикла, когда у нас нет быстрой построчной функции?
Бенчмаркинг by = rowpos
а также by = 1:NROW(dt)
версии против for
цикл с set()
является информативным и демонстрирует, что версия цикла работает быстрее, чем любая из by =
подходы:
f.rowpos <- function(){
dt <- data.table(V0 = rep(LETTERS[c(1,1,2,2,3)], 1e3), V1=1:5, V2=3:7, V3=5:1)
dt[, rowpos := .I]
dt[ , sdd := sum(.SD[, 2:4, with=FALSE]), by = rowpos ][]
}
f.nrow <- function(){
dt <- data.table(V0 = rep(LETTERS[c(1,1,2,2,3)], 1e3), V1=1:5, V2=3:7, V3=5:1)
dt[, sdd := sum(.SD[, 2:4, with=FALSE]), by = 1:NROW(dt) ][]
}
f.forset<- function(){
dt <- data.table(V0 = rep(LETTERS[c(1,1,2,2,3)], 1e3), V1=1:5, V2=3:7, V3=5:1)
dt[, sdd:=0L]
for (i in 1L:NROW(dt)) {
set(dt, i, 5L, sum(dt[i, 2:4]))
}
dt
}
microbenchmark(f.rowpos(),f.nrow(), f.forset(), times = 5)
Unit: seconds
expr min lq mean median uq max neval cld
f.rowpos() 4.465371 4.503614 4.510916 4.505922 4.521629 4.558042 5 b
f.nrow() 4.499120 4.499920 4.541131 4.558701 4.571267 4.576647 5 b
f.forset() 2.540556 2.603505 2.654036 2.606108 2.750719 2.769292 5 a
Итак, в заключение, даже в ситуациях, когда нет оптимизированной функции, такой как rowSums
который уже работает по строкам, всегда есть альтернативы использованию столбца rowpos, которые быстрее, но не требуют создания избыточного столбца.