Декартово произведение с фильтром 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 после того, как соединение, похоже, будет быстрее, чем включение в неравновое соединение.