Подтвердить что ты не робот

Сложная сводная функция - можно ли решить с помощью пакета данных r.table?

Я перезаписываю некоторые R-скрипты, которые анализируют большие объемы данных (~ 17 миллионов строк), и я думал, что попытаюсь улучшить его эффективность памяти, используя пакет data.table (который я только изучаю!).

Одна часть кода несколько озадачивала меня. Я не могу опубликовать свое оригинальное решение, потому что (1) это дерьмо (медленное!) И (2) оно очень тонкое по отношению к данным и просто усложнит этот вопрос.

Вместо этого я сделал этот пример с игрушкой (и это действительно пример игрушки):

ds <- data.table(ID=c(1,1,1,1,2,2,2,3,3,3),
Obs=c(1.5,2.5,0.0,1.25,1.45,1.5,2.5,0.0,1.25,1.45), 
Pos=c(1,3,5,6,2,3,5,2,3,4))

Что выглядит так:

    ID  Obs Pos
 1:  1 1.50   1
 2:  1 2.50   3
 3:  1 0.00   5
 4:  1 1.25   6
 5:  2 1.45   2
 6:  2 1.50   3
 7:  2 2.50   5
 8:  3 0.00   2
 9:  3 1.25   3
10:  3 1.45   4

Для удобства объяснения я буду притворяться, что мы наблюдаем поезда (каждый поезд имеет свой собственный ID), перемещаясь по линейной односторонней дорожке с наблюдениями (некоторое значение, а не импорт на вопрос) о поезд, который делается на заданных позициях ( pos, здесь от 1 до 6) вдоль дорожки. Не ожидается, что поезд будет делать его по всей длине трека (возможно, он взорвался, прежде чем перейти к пункту 6), и иногда наблюдение упускается наблюдателем... Позиции последовательны (при этом, если мы пропустили наблюдение поезд в положении 4, но мы наблюдали его в положении 5, мы знаем, что он должен пройти через позицию 4).

Из приведенной выше таблицы данных мне нужно создать такую ​​таблицу:

   Pos Count
1:   1     3
2:   2     3
3:   3     3
4:   4     3
5:   5     2
6:   6     1

Где для каждого уникального Pos в моей data.table ds, у меня есть счет количества поездов, которые попали в эту позицию на треке (или дальше), независимо от того, было ли наблюдение был сделан в этом положении на трассе.

Если у кого-нибудь есть идеи или предложения относительно того, как справиться с этим, это было бы очень признательно. К сожалению, я недостаточно разбираюсь в data.table, чтобы узнать, можно ли это сделать! Или это может быть невероятно простая проблема для решения, и я просто медленно:)

4b9b3361

Ответ 1

Отличный вопрос!! Данные примера особенно хорошо разработаны и хорошо объяснены.

Сначала я покажу этот ответ, затем я объясню его шаг за шагом.

> ids = 1:3   # or from the data: unique(ds$ID)
> pos = 1:6   # or from the data: unique(ds$Pos)
> setkey(ds,ID,Pos)

> ds[CJ(ids,pos), roll=-Inf, nomatch=0][, .N, by=Pos]
   Pos N
1:   1 3
2:   2 3
3:   3 3
4:   4 3
5:   5 2
6:   6 1
> 

Это также должно быть очень эффективным для ваших больших данных.

Шаг за шагом

Сначала я попробовал Cross Join (CJ); то есть для каждого поезда для каждой позиции.

> ds[CJ(ids,pos)]
    ID Pos  Obs
 1:  1   1 1.50
 2:  1   2   NA
 3:  1   3 2.50
 4:  1   4   NA
 5:  1   5 0.00
 6:  1   6 1.25
 7:  2   1   NA
 8:  2   2 1.45
 9:  2   3 1.50
10:  2   4   NA
11:  2   5 2.50
12:  2   6   NA
13:  3   1   NA
14:  3   2 0.00
15:  3   3 1.25
16:  3   4 1.45
17:  3   5   NA
18:  3   6   NA

Я вижу 6 рядов на поезд. Я вижу 3 поезда. У меня есть 18 строк, как я ожидал. Я вижу NA, где этот поезд не наблюдался. Хорошо. Проверьте. Кажется, что крест-соединение работает. Теперь создадим запрос.

Вы писали, что если поезд находится в позиции n, он должен был пройти предыдущие позиции. Сразу же я думаю roll. Попробуем это.

ds[CJ(ids,pos), roll=TRUE]
    ID Pos  Obs
 1:  1   1 1.50
 2:  1   2 1.50
 3:  1   3 2.50
 4:  1   4 2.50
 5:  1   5 0.00
 6:  1   6 1.25
 7:  2   1   NA
 8:  2   2 1.45
 9:  2   3 1.50
10:  2   4 1.50
11:  2   5 2.50
12:  2   6 2.50
13:  3   1   NA
14:  3   2 0.00
15:  3   3 1.25
16:  3   4 1.45
17:  3   5 1.45
18:  3   6 1.45

Hm. Это свернуло наблюдения вперед для каждого поезда. Он оставил несколько NA в положении 1 для поездов 2 и 3, но вы сказали, что если поезд находится в позиции 2, он должен был пройти положение 1. Он также перевернул последнее наблюдение для поездов 2 и 3 в положение 6, но вы сказали, что поезда могут взорваться. Итак, мы хотим откинуться назад! Это roll=-Inf. Это сложный -Inf, потому что вы также можете контролировать, как далеко отбросить назад, но нам это не нужно для этого вопроса; мы просто хотим откатиться назад бесконечно. Попробуйте roll=-Inf и посмотрим, что произойдет.

> ds[CJ(ids,pos), roll=-Inf]
    ID Pos  Obs
 1:  1   1 1.50
 2:  1   2 2.50
 3:  1   3 2.50
 4:  1   4 0.00
 5:  1   5 0.00
 6:  1   6 1.25
 7:  2   1 1.45
 8:  2   2 1.45
 9:  2   3 1.50
10:  2   4 2.50
11:  2   5 2.50
12:  2   6   NA
13:  3   1 0.00
14:  3   2 0.00
15:  3   3 1.25
16:  3   4 1.45
17:  3   5   NA
18:  3   6   NA

Это лучше. Почти готово. Все, что нам нужно сделать, - это счет. Но, эти надоедливые NA есть после того, как поезда 2 и 3 взорвались. Удалите их.

> ds[CJ(ids,pos), roll=-Inf, nomatch=0]
    ID Pos  Obs
 1:  1   1 1.50
 2:  1   2 2.50
 3:  1   3 2.50
 4:  1   4 0.00
 5:  1   5 0.00
 6:  1   6 1.25
 7:  2   1 1.45
 8:  2   2 1.45
 9:  2   3 1.50
10:  2   4 2.50
11:  2   5 2.50
12:  3   1 0.00
13:  3   2 0.00
14:  3   3 1.25
15:  3   4 1.45

Btw, data.table любит как можно больше находиться внутри одного DT[...], так как он оптимизирует запрос. Внутри он не создает NA, а затем удаляет их; он никогда не создает NA в первую очередь. Эта концепция важна для эффективности.

Наконец, все, что нам нужно сделать, это считать. Мы можем просто придерживаться этого в конце как сложный запрос.

> ds[CJ(ids,pos), roll=-Inf, nomatch=0][, .N, by=Pos]
   Pos N
1:   1 3
2:   2 3
3:   3 3
4:   4 3
5:   5 2
6:   6 1

Ответ 2

data.table звучит как отличное решение. Из того, как упорядочены данные, можно найти максимум каждого поезда с помощью

maxPos = ds$Pos[!duplicated(ds$ID, fromLast=TRUE)]

Затем вставьте в таблицу поезда, которые достигают этой позиции

nAtMax = tabulate(maxPos)

и вычислить суммарную сумму поездов в каждой позиции, считая с конца

rev(cumsum(rev(nAtMax)))
## [1] 3 3 3 3 2 1

Я думаю, что это будет довольно быстро для больших данных, хотя и не полностью эффективно с точки зрения памяти.

Ответ 3

Вы можете попробовать, как показано ниже. Я целенаправленно разбил его на многоступенчатое решение для лучшего понимания. Вы можете, возможно, объединить все их в один шаг, просто цепочки [].

Логика здесь заключается в том, что сначала мы находим конечную позицию для каждого идентификатора. Затем мы объединяем данные, чтобы найти количество идентификаторов для каждой конечной позиции. Поскольку все идентификаторы конечной позиции 6 также должны учитываться для конечной позиции 5, мы используем cumsum, чтобы добавить все более высокие идентификаторы ID к их нижним идентификаторам.

ds2 <- ds[, list(FinalPos=max(Pos)), by=ID]

ds2 
##    ID FinalPos
## 1:  1        6
## 2:  2        5
## 3:  3        4

ds3 <- ds2[ , list(Count = length(ID)), by = FinalPos][order(FinalPos, decreasing=TRUE), list(FinalPos, Count = cumsum(Count))]

ds3
##    FinalPos Count
## 1:        4     3
## 2:        5     2
## 3:        6     1

setkey(ds3, FinalPos)

ds3[J(c(1:6)), roll = 'nearest']

##    FinalPos Count
## 1:        1     3
## 2:        2     3
## 3:        3     3
## 4:        4     3
## 5:        5     2
## 6:        6     1

Ответ 4

Некоторые сроки для справки:

временный код:

library(data.table)
set.seed(0L)
nr <- 2e7
nid <- 1e6
npos <- 20
ds <- unique(data.table(ID=sample(nid, nr, TRUE), Pos=sample(npos, nr, TRUE)))
# ds <- data.table(ID=c(1,1,1,1,2,2,2,3,3,3),
#     Obs=c(1.5,2.5,0.0,1.25,1.45,1.5,2.5,0.0,1.25,1.45),
#     Pos=c(1,3,5,6,2,3,5,2,3,4))
setkey(ds, ID, Pos)

ids = ds[, sort(unique(ID))]   # or from the data: unique(ds$ID)
pos = ds[, sort(unique(Pos))]   # or from the data: unique(ds$Pos)

mtd0 <- function() ds[CJ(ids, pos), roll=-Inf, nomatch=0][, .N, by=Pos]
mtd1 <- function() ds[,max(Pos),by=ID][,rev(cumsum(rev(tabulate(V1))))]
mtd2 <- function() ds[, .(Pos=1:Pos[.N]), ID][, .N, by=Pos]
bench::mark(mtd0(), mtd1(), mtd2(), check=FALSE)

identical(mtd0()$N, mtd2()$N)
#[1] TRUE

identical(mtd1(), mtd2()$N)
#[1] TRUE

тайминги:

# A tibble: 3 x 13
  expression      min   median 'itr/sec' mem_alloc 'gc/sec' n_itr  n_gc total_time result            memory               time     gc              
  <bch:expr> <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl> <int> <dbl>   <bch:tm> <list>            <list>               <list>   <list>          
1 mtd0()        2.14s    2.14s     0.468    1.26GB     1.40     1     3      2.14s <df[,2] [20 x 2]> <df[,3] [41 x 3]>    <bch:tm> <tibble [1 x 3]>
2 mtd1()     281.54ms 284.89ms     3.51   209.24MB     1.76     2     1   569.78ms <int [20]>        <df[,3] [24 x 3]>    <bch:tm> <tibble [2 x 3]>
3 mtd2()        1.63s    1.63s     0.613  785.65MB     7.35     1    12      1.63s <df[,2] [20 x 2]> <df[,3] [9,111 x 3]> <bch:tm> <tibble [1 x 3]>