Декартово произведение с фильтром data.table
Я пытаюсь заменить декартово произведение, созданное SQL, на вызов data.table.
У меня большая история с активами и ценностями, и мне нужно подмножество всех комбинаций.
Скажем, что у меня есть таблица a с T = [дата, контракт, значение]. В SQL это выглядит как
SELECT a.date, a.contract, a.value, b.contract. b.value
FROM T a, T b
WHERE a.date = b.date AND a.contract <> b.contract AND a.value + b.value < 4
В R у меня теперь есть
library(data.table)
n <- 1500
dt <- data.table(date = rep(seq(Sys.Date() - n+1, Sys.Date(), by = "1 day"), 3),
contract = c(rep("a", n), rep("b", n), rep("c", n)),
value = c(rep(1, n), rep(2, n), rep(3, n)))
setkey(dt, date)
dt[dt, allow.cartesian = TRUE][(contract != i.contract) & (value + i.value < 4)]
Я считаю, что мое решение сначала создает все комбинации (в данном случае 13 500 строк), а затем фильтрует (до 3000). Однако SQL (и, возможно, я ошибаюсь), присоединяется к подмножеству, и, что более важно, не загружайте все комбинации в ОЗУ. Любые идеи о том, как использовать data.table более эффективно?
Ответы
Ответ 1
Используйте by =.EACHI. В data.table
соединения и подмножества очень тесно связаны; т.е. соединение - это просто другое подмножество - используя data.table
- вместо обычных имен целых/логических/строк. Они разработаны таким образом с учетом этих случаев.
Соединения на основе подмножества позволяют включать j
-выражения и группировать операции при соединении.
require(data.table)
dt[dt, .SD[contract != i.contract & value + i.value < 4L], by = .EACHI, allow = TRUE]
Это идиоматический способ (если вы хотите использовать i.*
cols только для условия, но не возвращать их также), однако .SD
еще не оптимизирован и оценка j
-выражение на .SD
для каждой группы является дорогостоящим.
system.time(dt[dt, .SD[contract != i.contract & value + i.value < 4L], by = .EACHI, allow = TRUE])
# user system elapsed
# 2.874 0.020 2.983
Некоторые случаи с использованием .SD
уже оптимизированы. Пока эти случаи не позаботятся, вы можете обойти это так:
dt[dt, {
idx = contract != i.contract & value + i.value < 4L
list(contract = contract[idx],
value = value[idx],
i.contract = i.contract[any(idx)],
i.value = i.value[any(idx)]
)
}, by = .EACHI, allow = TRUE]
И для этого требуется 0,045 секунд, а не 0.005 секунд. Но by = .EACHI
каждый раз оценивает j
-выражение (и, следовательно, эффективность памяти). Это компромисс, который вам придется принять.
Ответ 2
Начиная с версии v1.9.8 (по CRAN 25 нояб. 2016), с помощью data.table
можно использовать неравновесные соединения, которые могут быть использованы здесь.
Кроме того, подход OP создает "симметричные дубликаты" (a, b) и (b, a). Избегание дубликатов уменьшало бы размер набора результатов без потери информации (сравните ?combn
)
Если это намерение OP, мы можем использовать неравновесные объединения, чтобы избежать этих симметричных дубликатов:
library(data.table)
dt[, rn := .I][dt, on = .(date, rn < rn), nomatch = 0L][value + i.value < 4]
который дает
date contract value rn i.contract i.value
1: 2013-09-24 a 1 1501 b 2
2: 2013-09-25 a 1 1502 b 2
3: 2013-09-26 a 1 1503 b 2
4: 2013-09-27 a 1 1504 b 2
5: 2013-09-28 a 1 1505 b 2
---
1496: 2017-10-28 a 1 2996 b 2
1497: 2017-10-29 a 1 2997 b 2
1498: 2017-10-30 a 1 2998 b 2
1499: 2017-10-31 a 1 2999 b 2
1500: 2017-11-01 a 1 3000 b 2
в отличие от результата с использованием кода OP
date contract value i.contract i.value
1: 2013-09-24 b 2 a 1
2: 2013-09-24 a 1 b 2
3: 2013-09-25 b 2 a 1
4: 2013-09-25 a 1 b 2
5: 2013-09-26 b 2 a 1
---
2996: 2017-10-30 a 1 b 2
2997: 2017-10-31 b 2 a 1
2998: 2017-10-31 a 1 b 2
2999: 2017-11-01 b 2 a 1
3000: 2017-11-01 a 1 b 2
Следующий шаг - дальнейшее уменьшение количества созданных пар, которые необходимо отфильтровать впоследствии:
dt[, val4 := 4 - value][dt, on = .(date, rn < rn, val4 > value), nomatch = 0L]
который возвращает тот же результат, что и выше.
Обратите внимание, что условие фильтра value + i.value < 4
заменяется другим условием объединения val4 > value
, где val4
- специально созданный вспомогательный столбец.
Benchmark
Для тестового случая n <- 150000L
, приводящего к 450 k строк в dt
, тайминги:
n <- 150000L
dt <- data.table(date = rep(seq(Sys.Date() - n+1, Sys.Date(), by = "1 day"), 3),
contract = c(rep("a", n), rep("b", n), rep("c", n)),
value = c(rep(1, n), rep(2, n), rep(3, n)))
dt0 <- copy(dt)
microbenchmark::microbenchmark(
OP = {
dt <- copy(dt0)
dt[dt, on = .(date), allow.cartesian = TRUE][
(contract != i.contract) & (value + i.value < 4)]
},
nej1 = {
dt <- copy(dt0)
dt[, rn := .I][dt, on = .(date, rn < rn), nomatch = 0L][value + i.value < 4]
},
nej2 = {
dt <- copy(dt0)
dt[, rn := .I][, val4 := 4 - value][dt, on = .(date, rn < rn, val4 > value), nomatch = 0L]
},
times = 20L
)
Unit: milliseconds
expr min lq mean median uq max neval cld
OP 136.3091 143.1656 246.7349 298.8648 304.8166 311.1141 20 b
nej1 127.9487 133.1772 160.8096 136.0825 146.0947 298.3348 20 a
nej2 180.4189 183.9264 219.5171 185.9385 198.7846 351.3038 20 b
Итак, сделайте проверку value + i.value < 4
после того, как соединение, похоже, будет быстрее, чем включение в неравновое соединение.