#r #join #merge #data.table
#r #Присоединиться #слияние #данные.таблица
Вопрос:
Рассмотрим следующие data.table
s. Первый определяет набор областей с начальной и конечной позициями для каждой группы ‘x’:
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
Второй набор данных имеет ту же группирующую переменную ‘x’ и позиции ‘pos’ внутри каждой группы:
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’, где ‘pos’ попадает в диапазон, определяемый ‘start’ и ‘end’, внутри каждой группы x
. Желаемый результат
# 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 = TRUE, 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», а исходный «start» был потерян. Есть ли способ сохранить все столбцы с объединением по рулону, чтобы я мог сообщить «начало», «pos», «конец» по желанию?
Ответ №1:
Перекрытие соединения было реализовано с фиксацией 1375 в data.table версии v1.9.3 и доступно в текущей стабильной версии v1.9.4. Вызывается функция foverlaps
. Из НОВОСТЕЙ:
29)
Overlap joins
# 528 наконец-то здесь!! За исключениемtype="equal"
maxgap
minoverlap
аргументов and и, все остальное реализовано. Ознакомьтесь?foverlaps
с примерами его использования. Это важное дополнение к функцииdata.table
.
Давайте рассмотрим x, интервал, определенный как [a, b]
, где a <= b
, и y, другой интервал, определенный как [c, d]
, где c <= d
. Говорят, что интервал y перекрывает x вообще, если d >= a
и c <= b
1. И y полностью содержится в x, если 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
явно.
Тип перекрытия находится внутри, и мы хотели бы иметь все совпадения, только если совпадение есть.
ПРИМЕЧАНИЕ: foverlaps
использует функцию двоичного поиска data.table (вместе с roll
тем, где это необходимо) под капотом, Но некоторые аргументы функции (типы перекрытий, maxgap, minoverlap и т. Д.) Вдохновлены функцией findOverlaps()
из пакета Bioconductor IRanges
, отличного пакета (и so is GenomicRanges
, который распространяется IRanges
на геномику).
Так в чем же преимущество?
Тест на приведенном выше коде для ваших данных выполняется foverlaps()
медленнее, чем ответ Габора (тайминги: решение Габора data.table = 0,004 против foverlaps = 0,021 секунды). Но действительно ли это имеет значение при такой степени детализации?
Что было бы действительно интересно, так это посмотреть, насколько хорошо это масштабируется — с точки зрения скорости и памяти. В ответе Габора мы объединяемся на основе ключевого столбца x
. А затем отфильтруйте результаты.
Что, если d1
имеет около 40 тыс. строк и d2
имеет 100 тыс. строк (или более)? Для каждой строки в d2
, которая соответствует x
in 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
.
Решения Габора:
## 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
не обязательно иметь одинаковые начальные и конечные координаты (например: геномика, область, откуда я родом, где d2
обычно около 30-150 миллионов или более строк).
foverlaps()
является стабильным, но все еще находится в стадии разработки, что означает, что некоторые аргументы и имена могут быть изменены.
ПРИМЕЧАНИЕ: Поскольку я упоминал GenomicRanges
выше, он также вполне способен решить эту проблему. Он использует интервальные деревья под капотом и также довольно эффективен с точки зрения памяти. В моих тестах по данным геномики foverlaps()
это быстрее. Но это для другого поста (в блоге), в другой раз.
Комментарии:
1. Вы когда-нибудь делали это сообщение в блоге? Я пытаюсь перекрыть почти каждый нуклеотид в геноме человека (оцененный по соответствующей метрике) с набором диапазонов, и я убил findOverlaps GRanges примерно через четыре часа, чтобы начать с касательной foverlaps. Наличие этого готового сообщения в блоге с инструкциями сэкономило бы мне сегодня много времени. : D
Ответ №2:
data.table v1.9.8
имеет новую функцию — неравные соединения. При этом эта операция становится еще более простой:
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
Комментарии:
1. не могли бы вы добавить использование памяти и время для этого подхода
2. Здесь полезно отметить, что x имеет в виду две вещи. Первый x — это столбец «x», а второй x в x.pos относится к d2.
3. @RyanWardValverde Почему это необходимо включить
pos=x.pos
? Допустим, вd2
таких столбцах, как ‘a’ и ‘b’, больше столбцов. При выборе столбцов; a и b не обязательно должны иметь префикс x, однакоpos
имеет. Кроме того, если вы не ставите префикс pos с x тогдаpos==start
. Вы знаете, почему это ожидается?4. Похоже на побочный продукт порядка операций. Удаление всего, что находится между скобками в
j
позиции, т. Е..(...)
Дает намd2[d1, on = .(x, pos>=start, pos<=end), nomatch = NULL]
, и дает столбцы с именамиpos
иpos.1
, которые соответствуютstart
иend
, что для меня является неожиданным поведением. Это, по-видимому, указывает на то, что соединение выполняется первым, и для доступа кdata.table
позиции вx
позиции его необходимо вызвать явно.
Ответ №3:
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
from 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) объединение / подмножество с использованием только основания R:
subset(merge(d1, d2), start <= pos amp; 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) amp; !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
Комментарии:
1. Я ценю вашу всесторонность. Я просто очень хотел, чтобы что-то использовало преимущества оптимизированного поиска по дереву, реализованного в объединении данных.таблицы. Спасибо за
sqldf
предложение. Я привык писать объединения, какsqldf("select * from d1 join d2 on d1.x==d2.x and d2.pos>=d1.start and d2.pos<=d1.end")
в SQL Server, которые были хороши, когда вы могли добавлять индексы в таблицу. Я думаю, что это может избежать создания полного внешнего соединения в памяти (но не тестировалось)2. Однако обратите внимание, что код data.table здесь короче и быстрее.
3. Вы правы,
between
это быстрее. Я также тестировалd1[d2, roll=T, nomatch=0, mult="all"][start<=end]
с вашим жгутом и был близок к промежуточному. Это просто показывает, что вам нужно все протестировать, потому что вы никогда не знаете, что будет быстрее. Спасибо, что нашли время, чтобы проверить это. Очень интересно. Может быть, когда @Arun реализует «объединение диапазона» в data.table, он сможет сделать это быстрее.4. Не могли бы вы объяснить, как работает «d1 [d2] [между (pos, start, end)]», пожалуйста?
5.
d1[d2]
соединяетd1
иd2
вдольx
и[between(...)]
выбирает те строки, для которых значениеbetween(...)
TRUE .
Ответ №4:
Соединения с перекрытием доступны dplyr 1.1.0
через функцию join_by
.
С join_by
помощью вы можете выполнить соединение с перекрытием between
с помощью или вручную с >=
помощью и <=
:
library(dplyr)
inner_join(d2, d1, by = join_by(x, between(pos, start, end)))
# x pos start end
#1 a 2 1 3
#2 a 3 1 3
#3 c 20 19 22
#4 e 10 7 25
inner_join(d2, d1, by = join_by(x, pos >= start, pos <= end))
# x pos start end
#1 a 2 1 3
#2 a 3 1 3
#3 c 20 19 22
#4 e 10 7 25
Ответ №5:
Использование fuzzyjoin
:
result <- fuzzyjoin::fuzzy_inner_join(d1, d2,
by = c('x', 'pos' = 'start', 'pos' = 'end'),
match_fun = list(`==`, `>=`, `<=`))
result
# x.x pos x.y start end
# <chr> <dbl> <chr> <dbl> <dbl>
#1 a 2 a 1 3
#2 a 3 a 1 3
#3 c 20 c 19 22
#4 e 10 e 7 25
Поскольку fuzzyjoin
возвращает все столбцы, нам может потребоваться выполнить некоторую очистку, чтобы сохранить нужные столбцы.
library(dplyr)
result %>% select(x = x.x, pos, start, end)
# A tibble: 4 x 4
# x pos start end
# <chr> <dbl> <dbl> <dbl>
#1 a 2 1 3
#2 a 3 1 3
#3 c 20 19 22
#4 e 10 7 25