Среднее значение столбца data.table, заданное с помощью матрицы

У меня есть таблица данных, содержащая значения x, y, z 10000 точек (для этого примера) в единичном кубе, и каждая точка имеет соответствующий атрибут (называемый P). Я использовал nn2 из пакета RANN, чтобы найти k-соседей (до 50) индексов каждой точки в радиусе 0,075 единиц от исходного data.frame (который возвращается в виде матрицы).

library(RANN)
library(data.table)

set.seed(1L) # for reproducible data
DATA <- data.table(runif(10000, 0,1), 
                   runif(10000, 0,1), 
                   runif(10000, 0,1), 
                   runif(10000, 10,30))
colnames(DATA)<-c("x","y","z","P")
nn.idx <- nn2(DATA[,1:3], DATA[,1:3], k=50, 
              treetype = "kd", searchtype = "radius", 
              radius = 0.075)$nn.idx

Следующий цикл for выполняет свою работу, но мне было интересно, есть ли способ ускорить это путем векторизации, поскольку это не будет масштабироваться при применении к> миллионам точек? Проще говоря, я хочу использовать nn.idx, чтобы получить соответствующие значения P из DATA и вычислить среднее значение P, которое затем присваивается новому столбцу в DATA с именем mean.P

for(index in 1:nrow(DATA))
  DATA$mean.P[index]<-mean(DATA[nn.idx[index,], P])

В иллюстративных целях следующий код иллюстрирует то, что я пытаюсь вычислить - для всех точек (серые точки) вычислить среднее значение для всех точек (оранжевые + красные точки) в сфере вокруг данной точки (красная точка) и назначить до этой точки (красная точка). Итерируйте по всем точкам, но делайте это эффективно, чтобы можно было масштабировать большие наборы данных.

library(rgl)
rgl.open()
rgl.points(DATA[1500,1], DATA[1500,2], DATA[1500,3], color ="red")
rgl.points(DATA[nn.idx[1500,],1:3], color ="orange", add=T)
rgl.points(DATA[,1:3], color ="lightgray", alpha=0.1, add=T)

введите здесь описание изображения

Я никогда в жизни не тратил столько времени, пытаясь эффективно векторизовать один цикл! Кроме того, я не против того, чтобы использовать punting и просто делать это с помощью c ++ и Rcpp, но я подумал, что сначала спрошу здесь, чтобы узнать, есть ли в R способ сделать его масштабируемым и быстрее. Заранее спасибо!


person Andre Sandor    schedule 24.09.2017    source источник
comment
Если с этим можно справиться с учетом памяти, с вашими большими данными, вы можете извлечь все значения сразу -x = DATA[c(nn.idx), P]- и найти среднее значение с помощью by = row(nn.idx)[as.logical(nn.idx)]: meanP = c(rowsum(x, by)) / tabulate(by)   -  person alexis_laz    schedule 24.09.2017
comment
Оба решения пока кажутся жизнеспособными, поэтому мне нужно еще протестировать. Тестируя их оба на одном ядре моей машины (Dell 2016 с Xeon E5-2620 2,10 ГГц), решение Uwe является самым быстрым за счет второй таблицы data.table (которая действительно стала огромной), а bdemarest работает адекватно быстрее с гораздо большим более дешевый вектор. Итак, 1Mil pts и k = 100: # bdemarest solution; elapsed = 16.22 и # Uwe solution; elapsed = 4.94 с соответствующими объектами размера: # Int_vec = 8,000,040 long = 1,200,007,392   -  person Andre Sandor    schedule 24.09.2017


Ответы (2)


Вот решение, которое дает почти 100-кратное увеличение скорости. Я не совсем понимаю, почему улучшение так велико, но, возможно, один из реальных экспертов data.table может это прокомментировать.

library(RANN)
library(data.table)

set.seed(1L) # for reproducible data
DATA <- data.table(runif(10000, 0,1), 
                   runif(10000, 0,1), 
                   runif(10000, 0,1), 
                   runif(10000, 10,30))
colnames(DATA)<-c("x","y","z","P")
nn.idx <- nn2(DATA[,1:3], DATA[,1:3], k=50, 
              treetype = "kd", searchtype = "radius", 
              radius = 0.075)$nn.idx

# (1)
# Timing for original loop.
system.time(for(index in 1:nrow(DATA)) {
    DATA$mean.P[index] <- mean(DATA[nn.idx[index,], P])
})
#    user  system elapsed 
#   7.830   0.850   8.684 

# (2)
# Use `set()` instead of `$<-` and `[<-`.
system.time({for(index in 1:nrow(DATA)) {
    set(DATA, i=index, j="mean_P_2", value=mean(DATA[nn.idx[index, ], P]))
}})
#    user  system elapsed 
#   3.405   0.008   3.417 

Как видите, можно добиться двукратного улучшения, просто заменив специфичную для data.table функцию set() в исходном цикле.

Затем я попытался поместить всю функциональность в функции, специфичные для data.table (в основном внутри синтаксиса data.table []). Я также поместил значения P в вектор, потому что доступ к значениям в векторах обычно намного быстрее, чем аналогичные операции с data.frames или data.tables.

# (3)
# Add row index.
DATA[, row_idx:=seq(nrow(DATA))]

# Isolate P values in a vector, because vector access is cheaper
# than data.table or data.frame access.
P_vec = DATA$P

system.time({
    # Create a list column where each element is a vector of 50 integer indexes.
    DATA[, nn_idx:=lapply(row_idx, function(i) nn.idx[i, ])]
    # Use `:=` and `by=` to internalize the loop within `[.data.table`.
    DATA[, mean_P_3:=mean(P_vec[nn_idx[[1]]]), by=row_idx]
})
#    user  system elapsed 
#   0.092   0.002   0.095 

# All results are identical.
all.equal(DATA$mean.P, DATA$mean_P_2)
# [1] TRUE
all.equal(DATA$mean.P, DATA$mean_P_3)
# [1] TRUE

Это дает почти 100-кратное увеличение скорости по сравнению с исходной петлей.

Кажется, он неплохо масштабируется до 1 миллиона точек данных:

# Try with 1 million data points.
set.seed(1L) # for reproducible data
DATA2 <- data.table(runif(1e6, 0,1), 
                    runif(1e6, 0,1), 
                    runif(1e6, 0,1), 
                    runif(1e6, 10,30))
colnames(DATA2) <- c("x","y","z","P")

system.time({
    nn.idx2 <- nn2(DATA2[,1:3], DATA2[,1:3], k=50, 
                   treetype = "kd", searchtype = "radius", 
                   radius = 0.075)$nn.idx
})
#    user  system elapsed 
# 346.603   1.883 349.708 


DATA2[, row_idx:=seq(nrow(DATA2))]
P_vec = DATA2$P

system.time({
    DATA2[, nn_idx:=lapply(row_idx, function(i) nn.idx2[i, ])]
    DATA2[, mean_P:=mean(P_vec[nn_idx[[1]]]), by=row_idx]
})
#    user  system elapsed 
#  15.685   0.587  16.297 

Тайминги были выполнены на одном ядре MacBook Pro 2011 года (Sandy Bridge 2.2Ghz). Объем оперативной памяти остался ниже 1,5 ГБ.

person bdemarest    schedule 24.09.2017

Вот еще одно решение, использующее melt() для преобразования матрицы индекса в длинный формат, объединение и агрегирование:

long <- melt(as.data.table(nn.idx)[, pt := .I], measure.vars = patterns("V"))
tmp <- long[DATA[, pt := .I], on = .(value = pt)][, mean(P), by = .(pt)][order(pt), V1]
DATA[, mean.P := tmp][, pt := NULL][]

Объяснение

Индексная матрица nn.idx преобразуется в таблицу данных и получает столбец pt, который является идентификатором строки точек. Затем формат матрицы изменяется с широкого на длинный.

tmp - вектор средних значений соседних точек. Их можно найти путем правого соединения DATA с long, чтобы сопоставить индексы ближайших соседних точек (в столбце value) с индексом точки, предварительно добавленным к DATA.

Последний шаг - добавить результат в виде нового столбца в DATA.

Вариант 2

В качестве альтернативы промежуточный результат можно добавить с помощью второго соединения:

long <- melt(as.data.table(nn.idx)[, pt := .I], measure.vars = patterns("V"))
    long[DATA[, pt := .I], on = .(value = pt)][, mean(P), by = .(pt)][DATA, on = "pt"]
person Uwe    schedule 24.09.2017