Рулон соединяется с начальным/конечным окном

Рассмотрим следующие data.table s. Первый определяет набор областей с начальным и конечным положениями для каждой группы

library(data.table)

d1 <- data.table(x=letters[1:5], start=c(1,5,19,30, 7), end=c(3,11,22,39,25))
setkey(d1, x,start)

#    x start end
# 1: a     1   3
# 2: b     5  11
# 3: c    19  22
# 4: d    30  39
# 5: e     7  25

Вторая представляет собой наблюдения для каждой группы

d2 <- data.table(x=letters[c(1,1,2,2,3:5)], pos=c(2,3,3,12,20,52,10))
setkey(d2, x,pos)

#    x pos
# 1: a   2
# 2: a   3
# 3: b   3
# 4: b  12
# 5: c  20
# 6: d  52
# 7: e  10

В конечном счете, я хотел бы иметь возможность извлекать строки в d2, которые находятся в области для соответствующего значения x в d1. Желаемый результат:

#    x pos start  end
# 1: a   2     1    3
# 2: a   3     1    3
# 3: c  20    19   22
# 4: e  10     7   25

Позиции начала/конца для любой группы x никогда не будут перекрываться, но могут быть пробелы значений не в какой-либо области.

Теперь я считаю, что должен использовать скользящее соединение. Из того, что я могу сказать, я не могу использовать столбец "end" в соединении.

Я пробовал

d1[d2, roll=T, nomatch=0, mult="all"][start<=end]

и получил

#    x start end
# 1: a     2   3
# 2: a     3   3
# 3: c    20  22
# 4: e    10  25

который является правильным набором строк, которые я хочу; Однако "pos" стал "стартом", и исходный "старт" был потерян. Есть ли способ сохранить все столбцы с объединением валов, чтобы я мог сообщать "start", "pos", "end" по желанию?

Ответ 1

Объединение переходов реализовано с commit 1375 в data.table v1.9.3 и доступен в текущей стабильной версии, v1.9.4. Функция называется foverlaps. Из НОВОСТИ:

29) Overlap joins # 528 теперь здесь, наконец!! За исключением аргументов type="equal" и maxgap и minoverlap, все остальное реализовано. Проверьте ?foverlaps и примеры там, где он используется. Это существенное дополнение к data.table.

Рассмотрим х, интервал, определенный как [a, b], где a <= b и y, другой интервал, определенный как [c, d], где c <= d. Интервал y называется перекрытием x вообще, iff d >= a и c <= b 1. И y полностью содержится внутри x, iff a <= c,d <= b 2. Для различных типов перекрытий, пожалуйста, посмотрите ?foverlaps.

Ваш вопрос является частным случаем объединения перекрытия: в d1 у вас есть истинные физические интервалы с позициями start и end. В d2, с другой стороны, есть только позиции (pos), а не интервалы. Чтобы иметь возможность выполнить перекрытие, нам нужно также создавать интервалы в d2. Это достигается путем создания дополнительной переменной pos2, которая идентична pos (d2[, pos2 := pos]). Таким образом, теперь мы имеем интервал в d2, хотя и с одинаковыми начальными и конечными координатами. Этот "виртуальный интервал нулевой ширины" в d2 может быть затем использован в foverlap для соединения с перекрытием с помощью d1:

require(data.table) ## 1.9.3
setkey(d1)
d2[, pos2 := pos]
foverlaps(d2, d1, by.x = names(d2), type = "within", mult = "all", nomatch = 0L)
#    x start end pos pos2
# 1: a     1   3   2    2
# 2: a     1   3   3    3
# 3: c    19  22  20   20
# 4: e     7  25  10   10

by.y по умолчанию - key(y), поэтому мы его пропустили. by.x по умолчанию принимает key(x), если он существует, а если не принимает key(y). Но для d2 ключа нет, и мы не можем установить столбцы из y, потому что у них нет одинаковых имен. Итак, мы явно устанавливаем by.x.

Тип перекрытия внутри, и мы хотели бы иметь все совпадения, только если есть совпадение.

NB: foverlaps использует функцию бинарного поиска data.table(вместе с roll при необходимости) под капотом, но некоторые аргументы функции (типы перекрытий, maxgap, minoverlap и т.д.) вдохновлены функцией findOverlaps() из пакета Bioconductor IRanges, отличный пакет (и, следовательно, GenomicRanges, который расширяет IRanges для геномики).


Итак, какое преимущество?

Тест на код выше на ваших данных приводит к foverlaps() медленнее, чем ответ Габора (Timings: Gabor data.table solution = 0.004 vs foverlaps = 0.021 секунд). Но действительно ли это имеет значение в этой гранулярности?

Что было бы действительно интересно, так это то, насколько хорошо он масштабируется - как по скорости, так и по памяти. В ответе Gabor мы присоединяемся к ключевому столбцу x. А затем отфильтруйте результаты.

Что делать, если d1 имеет около 40K строк и d2 имеет 100K строк (или больше)? Для каждой строки в d2, которая соответствует x в d1, все эти строки будут сопоставлены и возвращены, только для фильтрации позже. Здесь пример вашего Q слегка масштабируется:

Сгенерировать данные:

require(data.table)
set.seed(1L)
n = 20e3L; k = 100e3L
idx1 = sample(100, n, TRUE)
idx2 = sample(100, n, TRUE)
d1 = data.table(x = sample(letters[1:5], n, TRUE), 
                start = pmin(idx1, idx2), 
                end = pmax(idx1, idx2))

d2 = data.table(x = sample(letters[1:15], k, TRUE), 
                pos1 = sample(60:150, k, TRUE))

foverlaps:

system.time({
    setkey(d1)
    d2[, pos2 := pos1]
    ans1 = foverlaps(d2, d1, by.x=1:3, type="within", nomatch=0L)
})
# user  system elapsed 
#   3.028   0.635   3.745 

Для этого потребовалось ~ 1 ГБ памяти, из которых ans1 - 420 МБ. Большая часть времени, проведенного здесь, действительно на подмножестве. Вы можете проверить это, установив аргумент verbose=TRUE.

Решение Gabor:

## new session - data.table solution
system.time({
    setkey(d1, x)
    ans2 <- d1[d2, allow.cartesian=TRUE, nomatch=0L][between(pos1, start, end)]
})
#   user  system elapsed 
# 15.714   4.424  20.324 

И это заняло в общей сложности ~ 3,5 ГБ.

Я только отметил, что Габор уже упоминает память, необходимую для промежуточных результатов. Итак, попробуйте sqldf:

# new session - sqldf solution
system.time(ans3 <- sqldf("select * from d1 join 
            d2 using (x) where pos1 between start and end"))
#   user  system elapsed 
# 73.955   1.605  77.049 

В общей сложности было 1,4 ГБ. Таким образом, он определенно использует меньше памяти, чем показанный выше.

[Ответы были проверены как идентичные после удаления pos2 из ans1 и установки ключа для обоих ответов.]

Обратите внимание, что это соединение перекрытия спроектировано с проблемами, при которых d2 не обязательно имеет одинаковые начальные и конечные координаты (например: genomics, поле, откуда я родом, где d2 обычно составляет около 30-150 миллионов или больше строк).


foverlaps() является стабильным, но все еще находится в разработке, что означает, что некоторые аргументы и имена могут быть изменены.

NB: поскольку я упомянул выше GenomicRanges, он также отлично справляется с этой проблемой. Он использует интервальные деревья под капотом и также обладает достаточной памятью. В моих тестах по данным геномики foverlaps() выполняется быстрее. Но это для другого (блога) сообщения, в другое время.

Ответ 2

1) sqldf Это не data.table, но сложные критерии соединения легко задаются прямо в SQL:

library(sqldf)

sqldf("select * from d1 join d2 using (x) where pos between start and end")

даяние:

  x start end pos
1 a     1   3   2
2 a     1   3   3
3 c    19  22  20
4 e     7  25  10

2) data.table Для ответа data.table попробуйте следующее:

library(data.table)

setkey(d1, x)
setkey(d2, x)
d1[d2][between(pos, start, end)]

даяние:

   x start end pos
1: a     1   3   2
2: a     1   3   3
3: c    19  22  20
4: e     7  25  10

Обратите внимание, что это имеет недостаток в формировании возможного большого результата промежуточного результата d1[d2], который SQL может не делать. Остальные решения могут также иметь эту проблему.

3) dplyr Это говорит о соответствующем решении dplyr. Мы также используем between из data.table:

library(dplyr)
library(data.table) # between

d1 %>% 
   inner_join(d2) %>% 
   filter(between(pos, start, end))

даяние:

Joining by: "x"
  x start end pos
1 a     1   3   2
2 a     1   3   3
3 c    19  22  20
4 e     7  25  10

4) merge/subset Используя только базу R:

subset(merge(d1, d2), start <= pos & pos <= end)

даяние:

   x start end pos
1: a     1   3   2
2: a     1   3   3
3: c    19  22  20
4: e     7  25  10

Добавлено Обратите внимание, что решение таблицы данных здесь намного быстрее, чем в другом ответе:

dt1 <- function() {
 d1 <- data.table(x=letters[1:5], start=c(1,5,19,30, 7), end=c(3,11,22,39,25))
 d2 <- data.table(x=letters[c(1,1,2,2,3:5)], pos=c(2,3,3,12,20,52,10))
 setkey(d1, x, start)
 idx1 = d1[d2, which=TRUE, roll=Inf] # last observation carried forwards

 setkey(d1, x, end)
 idx2 = d1[d2, which=TRUE, roll=-Inf] # next observation carried backwards

 idx = which(!is.na(idx1) & !is.na(idx2))
 ans1 <<- cbind(d1[idx1[idx]], d2[idx, list(pos)])
}

dt2 <- function() {
 d1 <- data.table(x=letters[1:5], start=c(1,5,19,30, 7), end=c(3,11,22,39,25))
 d2 <- data.table(x=letters[c(1,1,2,2,3:5)], pos=c(2,3,3,12,20,52,10))
 setkey(d1, x)
 ans2 <<- d1[d2][between(pos, start, end)]
}

all.equal(as.data.frame(ans1), as.data.frame(ans2))
## TRUE

benchmark(dt1(), dt2())[1:4]
##     test replications elapsed relative
##  1 dt1()          100    1.45    1.667  
##  2 dt2()          100    0.87    1.000  <-- from (2) above

Ответ 3

data.table v1.9.8+ имеет новую функцию - объединения без equi. При этом эта операция становится еще более простой:

require(data.table) #v1.9.8+
# no need to set keys on `d1` or `d2`
d2[d1, .(x, pos=x.pos, start, end), on=.(x, pos>=start, pos<=end), nomatch=0L]
#    x pos start end
# 1: a   2     1   3
# 2: a   3     1   3
# 3: c  20    19  22
# 4: e  10     7  25