Более быстрая версия combn

Есть ли способ ускорить команду combn, чтобы получить все уникальные комбинации двух элементов, взятых из вектора?

Обычно это делается так:

# Get latest version of data.table
library(devtools)
install_github("Rdatatable/data.table",  build_vignettes = FALSE)  
library(data.table)

# Toy data
d <- data.table(id=as.character(paste0("A", 10001:15000))) 

# Transform data 
system.time({
d.1 <- as.data.table(t(combn(d$id, 2)))
})

Однако combn в 10 раз медленнее (23 секунды против 3 секунд на моем компьютере), чем вычисление всех возможных комбинаций с использованием data.table.

system.time({
d.2 <- d[, list(neighbor=d$id[-which(d$id==id)]), by=c("id")]
})

Имея дело с очень большими векторами, я ищу способ сэкономить память, вычисляя только уникальные комбинации (например, combn), но со скоростью data.table (см. Второй фрагмент кода).

Я ценю любую помощь.


person majom    schedule 09.11.2014    source источник
comment
Почему вы устанавливаете data.table с github?   -  person David Arenburg    schedule 09.11.2014
comment
Из-за ошибки в 1.9.4, которая создаст проблемы при выполнении второго фрагмента кода. Однако это уже было исправлено в версии 1.9.5.   -  person majom    schedule 09.11.2014
comment
Вы уверены, что d.2 <- d[, list(neighbor=d$id[-which(d$id==id)]), by=c("id")] дает результат, идентичный d.1 <- as.data.table(t(combn(d$id, 2)))? Я получаю вдвое больше данных. Я мог воспроизвести cmbn с data.table, используя CJ, что-то вроде CJ(d$id, d$id), V1, V2)[V2 > V1]   -  person David Arenburg    schedule 09.11.2014
comment
Верный. Набор данных будет вдвое больше для кода data.table. При использовании этого подхода каждая комбинация включается дважды. Мой вопрос заключался в том, как избежать добавления этих дубликатов, поскольку это критично, если вектор становится большим.   -  person majom    schedule 09.11.2014
comment
Для каждой комбинации дважды вы можете просто сделать CJ(d$id, d$id), который будет выполняться менее секунды   -  person David Arenburg    schedule 09.11.2014
comment
Приведенный выше код не включает петли, тогда как CJ(d$id, d$id) будет включать их. Извините, если я не смог это прояснить.   -  person majom    schedule 09.11.2014
comment
Я имел в виду, что d.2 <- d[, list(neighbor=d$id[-which(d$id==id)]), by=c("id")] эквивалентно CJ(d$id, d$id). В то время как d.1 <- as.data.table(t(combn(d$id, 2))) равно CJ(d$id, d$id)[V2 > V1]   -  person David Arenburg    schedule 09.11.2014
comment
Не совсем, d.2 <- d[, list(neighbor=d$id[-which(d$id==id)]), by=c("id")] и CJ(d$id, d$id) не эквивалентны. Первое не включает самопетли, тогда как второе включает.   -  person majom    schedule 09.11.2014
comment
Да, вы правы, CJ(d$id, d$id)[V1 != V2] было бы равноценно, но в два раза быстрее   -  person David Arenburg    schedule 09.11.2014


Ответы (5)


Вы можете использовать combnPrim из _ 2_

source("http://bioconductor.org/biocLite.R")
biocLite("gRbase") # will install dependent packages automatically.
system.time({
 d.1 <- as.data.table(t(combn(d$id, 2)))
 })
#   user  system elapsed 
# 27.322   0.585  27.674 

system.time({
d.2 <- as.data.table(t(combnPrim(d$id,2)))
 })
#   user  system elapsed 
#  2.317   0.110   2.425 

identical(d.1[order(V1, V2),], d.2[order(V1,V2),])
#[1] TRUE
person akrun    schedule 09.11.2014
comment
Я бы предложил https://bioconductor.org/biocLite.R вместо http, немного безопаснее. - person Aurèle; 05.09.2017
comment
кстати, а почему устанавливать из BioConductor, а не из CRAN? Была ли эта функция недоступна в версии CRAN в то время? - person Aurèle; 05.09.2017
comment
@ Aurèle Пакет не был основан на репозитории CRAN. В то время, когда я писал ответ. Теперь я думаю, что это то же самое, что это - person akrun; 05.09.2017

Вот способ использования data.table функции foverlaps(), который также оказывается быстрым!

require(data.table) ## 1.9.4+
d[, `:=`(id1 = 1L, id2 = .I)] ## add interval columns for overlaps
setkey(d, id1, id2)

system.time(olaps <- foverlaps(d, d, type="within", which=TRUE)[xid != yid])
#  0.603   0.062   0.717

Обратите внимание, что foverlaps() не вычисляет все перестановки. Подмножество xid != yid необходимо для удаления самоперекрытия. Подмножество можно было бы обрабатывать внутри более эффективно, реализовав аргумент ignoreSelf - аналогично IRanges::findOverlaps.

Теперь остается лишь выполнить подмножество с использованием полученных идентификаторов:

system.time(ans <- setDT(list(d$id[olaps$xid], d$id[olaps$yid])))
#   0.576   0.047   0.662 

Итого ~ 1,4 секунды.


Преимущество состоит в том, что вы можете сделать то же самое, даже если ваша таблица data.table d имеет более одного столбца, для которого вы должны получить комбинации, и используя тот же объем памяти (поскольку мы возвращаем индексы). В этом случае вы просто сделаете:

cbind(d[olaps$xid, ..your_cols], d[olaps$yid, ..your_cols])

Но это ограничивается заменой только combn(., 2L). Не более 2л.

person Arun    schedule 09.11.2014
comment
Вау, это вышло очень быстро. Я пытался сделать это с CJ, но это было намного медленнее, что-то вроде ans <- setkey(CJ(d$id, d$id), V1, V2)[V2 > V1] - person David Arenburg; 09.11.2014
comment
CJ уже сортирует. Так что вам не нужно setkey обходить это стороной. И CJ(d$id, d$id) должен быть быстрым (у меня занимает 0,9 секунды). Но это подмножество кажется дорогостоящим, поскольку CJ возвращает все перестановки, в отличие от foverlaps(). - person Arun; 09.11.2014
comment
Да, CJ(d$id, d$id) быстро, часть [V2 > V1] - это то, что так долго (~ 9 секунд). Интересно, почему [xid != yid] в вашем коде не имеет такого же эффекта. Наверное, потому что в нем меньше ценностей - person David Arenburg; 09.11.2014
comment
Fwiw, это тоже довольно быстро: system.time(res <- d[, r := .I ][d, on=.(r > r), nomatch=0] ) из stackoverflow.com/a/43315317 - person Frank; 10.04.2017

Сообщение с любым вариантом слова Fast в заголовке будет неполным без тестов. Прежде чем мы опубликуем какие-либо тесты, я просто хотел бы упомянуть, что с тех пор, как был опубликован этот вопрос, для R были выпущены два высоко оптимизированных пакета arrangements и RcppAlgos (я являюсь автором) для генерации комбинаций. Обратите внимание, что начиная с версии 2.3.0 для RcppAlgos мы можем использовать преимущества нескольких потоков для еще большей эффективности.

Чтобы дать вам представление об их скорости по сравнению с combn и gRbase::combnPrim, вот базовый тест:

## We test generating just over 3 million combinations
choose(25, 10)
[1] 3268760

microbenchmark(arrngmnt = arrangements::combinations(25, 10),
               combn = combn(25, 10),
               gRBase = gRbase::combnPrim(25, 10),
               serAlgos = RcppAlgos::comboGeneral(25, 10),
               parAlgos = RcppAlgos::comboGeneral(25, 10, nThreads = 4),
               unit = "relative", times = 20)
Unit: relative
    expr        min         lq       mean     median         uq        max neval
arrngmnt   2.979378   3.072319   1.898390   3.756307   2.139258  0.4842967    20
   combn 226.470755 230.410716 118.157110 232.905393 125.718512 17.7778585    20
  gRBase  34.219914  34.209820  18.789954  34.218320  19.934485  3.6455493    20
serAlgos   2.836651   3.078791   2.458645   3.703929   2.231475  1.1652445    20
parAlgos   1.000000   1.000000   1.000000   1.000000   1.000000  1.0000000    20

Теперь мы проводим сравнительный анализ других функций, опубликованных для очень конкретного случая создания комбинаций select 2 и создания объекта data.table.

Функции следующие:

funAkraf <- function(d) {
    a <- comb2.int(length(d$id))      ## comb2.int from the answer given by @akraf
    setDT(list(V1 = d$id[a[,1]], V2 = d$id[a[,2]]))
}

funAnirban <- function(d) {
    indices <- combi2inds(d$id)
    ans2 <- setDT(list(d$id[indices$xid], d$id[indices$yid]))
    ans2
}

funArun <- function(d) {
    d[, `:=`(id1 = 1L, id2 = .I)] ## add interval columns for overlaps
    setkey(d, id1, id2)
    olaps <- foverlaps(d, d, type="within", which=TRUE)[xid != yid]
    ans <- setDT(list(d$id[olaps$xid], d$id[olaps$yid]))
    ans
}

funArrangements <- function(d) {
  a <- arrangements::combinations(x = d$id, k = 2)
  setDT(list(a[, 1], a[, 2]))
}

funGRbase <- function(d) {
  a <- gRbase::combnPrim(d$id,2)
  setDT(list(a[1, ], a[2, ]))
}

funOPCombn <- function(d) {
  a <- combn(d$id, 2)
  setDT(list(a[1, ], a[2, ]))
}

funRcppAlgos <- function(d) {
  a <- RcppAlgos::comboGeneral(d$id, 2, nThreads = 4)
  setDT(list(a[, 1], a[, 2]))
}

Бенчмарк с данными OP

И вот тесты на примере, приведенном OP:

d <- data.table(id=as.character(paste0("A", 10001:15000))) 

microbenchmark(funAkraf(d),
               funAnirban(d),
               funArrangements(d),
               funArun(d),
               funGRbase(d),
               funOPCombn(d),
               funRcppAlgos(d),
               times = 10, unit = "relative")
    Unit: relative
              expr       min        lq      mean    median        uq       max neval
       funAkraf(d)  3.220550  2.971264  2.815023  2.665616  2.344018  3.383673    10
     funAnirban(d)  1.000000  1.000000  1.000000  1.000000  1.000000  1.000000    10
funArrangements(d)  1.464730  1.689231  1.834650  1.960233  1.932361  1.693305    10
        funArun(d)  3.256889  2.908075  2.634831  2.729180  2.432277  2.193849    10
      funGRbase(d)  3.513847  3.340637  3.327845  3.196399  3.291480  3.129362    10
     funOPCombn(d) 30.310469 26.255374 21.656376 22.386270 18.527904 15.626261    10
   funRcppAlgos(d)  1.676808  1.956696  1.943773  2.085968  1.949133  1.804180    10

Мы видим, что функция, предоставляемая @AnirbanMukherjee, является самой быстрой для этой задачи, за ней следует _12 _ / _ 13_. Для этой задачи nThreads не действует, поскольку переданный вектор - это character, что не является потокобезопасным. Что, если бы вместо этого мы преобразовали id в коэффициент?

Контрольные показатели с факторами (т. Е. Категориальными переменными)

dFac <- d
dFac$id <- as.factor(dFac$id)

library(microbenchmark)
microbenchmark(funAkraf(dFac),
               funAnirban(dFac),
               funArrangements(dFac),
               funArun(dFac),
               funGRbase(dFac),
               funOPCombn(dFac),
               funRcppAlgos(dFac),
               times = 10, unit = "relative")
Unit: relative
                 expr        min         lq      mean   median        uq       max   neval
       funAkraf(dFac)  10.898202  10.949896  7.589814 10.01369  8.050005  5.557014      10
     funAnirban(dFac)   3.104212   3.337344  2.317024  3.00254  2.471887  1.530978      10
funArrangements(dFac)   2.054116   2.058768  1.858268  1.94507  2.797956  1.691875      10
        funArun(dFac)  10.646680  12.905119  7.703085 11.50311  8.410893  3.802155      10
      funGRbase(dFac)  16.523356  21.609917 12.991400 19.73776 13.599870  6.498135      10
     funOPCombn(dFac) 108.301876 108.753085 64.338478 95.56197 65.494335 28.183104      10
   funRcppAlgos(dFac)   1.000000   1.000000  1.000000  1.00000  1.000000  1.000000      10 

Теперь мы видим, что RcppAlgos примерно на 2x быстрее, чем любое другое решение. В частности, RcppAlgos решение примерно на 3x, чем ранее самое быстрое решение, данное Anirban. Следует отметить, что это повышение эффективности стало возможным, потому что factor переменные действительно integers под капотом с некоторыми дополнительными attributes.

Подтвердите равенство

Все они также дают одинаковый результат. Единственное предостережение: решение gRbase не поддерживает факторы. То есть, если вы передадите factor, он будет преобразован в character. Таким образом, все решения дадут одинаковый результат, если вы пройдете dFac, за исключением решения gRbase:

identical(funAkraf(d), funOPCombn(d))
#[1] TRUE
identical(funAkraf(d), funArrangements(d))
#[1] TRUE
identical(funRcppAlgos(d), funArrangements(d))
#[1] TRUE
identical(funRcppAlgos(d), funAnirban(d))
#[1] TRUE
identical(funRcppAlgos(d), funArun(d))
#[1] TRUE

## different order... we must sort
identical(funRcppAlgos(d), funGRbase(d))
[1] FALSE
d1 <- funGRbase(d)
d2 <- funRcppAlgos(d)

## now it's the same
identical(d1[order(V1, V2),], d2[order(V1,V2),])
#[1] TRUE

Спасибо @Frank за то, что он указал, как сравнить два data.tables, не пытаясь создать новые data.tables и затем расположить их:

fsetequal(funRcppAlgos(d), funGRbase(d))
[1] TRUE
person Joseph Wood    schedule 23.06.2018
comment
Fyi, для независимого от порядка сравнения: DT = data.table(a = 1:2); fsetequal(DT, DT[2:1]) - person Frank; 26.06.2018

Вот решение с использованием Rcpp.

library(Rcpp)
library(data.table)
cppFunction('
Rcpp::DataFrame combi2(Rcpp::CharacterVector inputVector){
    int len = inputVector.size();
    int retLen = len * (len-1) / 2;
    Rcpp::CharacterVector outputVector1(retLen);
    Rcpp::CharacterVector outputVector2(retLen);
    int start = 0;
    for (int i = 0; i < len; ++i){
        for (int j = i+1; j < len; ++j){
            outputVector1(start) = inputVector(i);
            outputVector2(start) = inputVector(j);
            ++start;
            }
        }
    return(Rcpp::DataFrame::create(Rcpp::Named("id") = outputVector1,
                              Rcpp::Named("neighbor") = outputVector2));
};
')

# Toy data
d <- data.table(id=as.character(paste0("A", 10001:15000))) 

system.time({
    d.2 <- d[, list(neighbor=d$id[-which(d$id==id)]), by=c("id")]
    })
#  1.908   0.397   2.389

system.time({
    d[, `:=`(id1 = 1L, id2 = .I)] ## add interval columns for overlaps
    setkey(d, id1, id2)
    olaps <- foverlaps(d, d, type="within", which=TRUE)[xid != yid]
    ans <- setDT(list(d$id[olaps$xid], d$id[olaps$yid]))
    })
#  0.653   0.038   0.705

system.time(ans2 <- combi2(d$id))
#  1.377   0.108   1.495 

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

cppFunction('
Rcpp::DataFrame combi2inds(const Rcpp::CharacterVector inputVector){
const int len = inputVector.size();
const int retLen = len * (len-1) / 2;
Rcpp::IntegerVector outputVector1(retLen);
Rcpp::IntegerVector outputVector2(retLen);
int indexSkip;
for (int i = 0; i < len; ++i){
    indexSkip = len * i - ((i+1) * i)/2;
    for (int j = 0; j < len-1-i; ++j){
        outputVector1(indexSkip+j) = i+1;
        outputVector2(indexSkip+j) = i+j+1+1;
        }
    }
return(Rcpp::DataFrame::create(Rcpp::Named("xid") = outputVector1,
                          Rcpp::Named("yid") = outputVector2));
};
')

system.time({
        indices <- combi2inds(d$id)
        ans2 <- setDT(list(d$id[indices$xid], d$id[indices$yid]))
        })      
#  0.389   0.027   0.425 
person Anirban Mukherjee    schedule 23.06.2015


Вот два решения base-R, если вы не хотите использовать дополнительные зависимости:

  • comb2.int использует rep и другие функции генерации последовательности для генерации желаемого результата.

  • comb2.mat создает матрицу, использует upper.tri() для получения верхнего треугольника и which(..., arr.ind = TRUE) для получения индексов столбца и строки => все комбинации.

Возможность 1: comb2.int

comb2.int <- function(n, rep = FALSE){
  if(!rep){
    # e.g. n=3 => (1,2), (1,3), (2,3)
    x <- rep(1:n,(n:1)-1)
    i <- seq_along(x)+1
    o <- c(0,cumsum((n-2):1))
    y <- i-o[x]
  }else{
    # e.g. n=3 => (1,1), (1,2), (1,3), (2,2), (2,3), (3,3)
    x <- rep(1:n,n:1)
    i <- seq_along(x)
    o <- c(0,cumsum(n:2))
    y <- i-o[x]+x-1
  }
  return(cbind(x,y))
}

Возможность 2: comb2.mat

comb2.mat <- function(n, rep = FALSE){
  # Use which(..., arr.ind = TRUE) to get coordinates.
  m <- matrix(FALSE, nrow = n, ncol = n)
  idxs <- which(upper.tri(m, diag = rep), arr.ind = TRUE)
  return(idxs)
}

Функции дают тот же результат, что и combn(.):

for(i in 2:8){
  # --- comb2.int ------------------
  stopifnot(comb2.int(i) == t(combn(i,2)))
  # => Equal

  # --- comb2.mat ------------------
  m <- comb2.mat(i)
  colnames(m) <- NULL   # difference 1: colnames
  m <- m[order(m[,1]),] # difference 2: output order
  stopifnot(m == t(combn(i,2)))
  # => Equal up to above differences
}

Но в моем векторе есть другие элементы, кроме последовательных целых чисел!

Используйте возвращаемые значения как индексы:

v <- LETTERS[1:5]                                     
c <- comb2.int(length(v))                             
cbind(v[c[,1]], v[c[,2]])                             
#>       [,1] [,2]
#>  [1,] "A"  "B" 
#>  [2,] "A"  "C" 
#>  [3,] "A"  "D" 
#>  [4,] "A"  "E" 
#>  [5,] "B"  "C" 
#>  [6,] "B"  "D" 
#>  [7,] "B"  "E" 
#>  [8,] "C"  "D" 
#>  [9,] "C"  "E" 
#> [10,] "D"  "E"

Контрольный показатель:

время (combn) = ~ 5x раз (comb2.mat) = ~ 80x раз (comb2.int):

library(microbenchmark)

n <- 800
microbenchmark({
  comb2.int(n)
},{
  comb2.mat(n)
},{
  t(combn(n, 2))
})
#>   Unit: milliseconds
#>                    expr        min         lq       mean     median        uq       max neval
#>    {     comb2.int(n) }   4.394051   4.731737   6.350406   5.334463   7.22677  14.68808   100
#>    {     comb2.mat(n) }  20.131455  22.901534  31.648521  24.411782  26.95821 297.70684   100
#>  {     t(combn(n, 2)) } 363.687284 374.826268 391.038755 380.012274 389.59960 532.30305   100
person akraf    schedule 07.03.2018
comment
Я думаю, что ваши комментарии перевернуты в comb2.int. У вас есть в блоке повторения # e.g. n=3 => (1,2), (1,3), (2,3), когда должно быть # e.g. n=3 => (1,1), (1,2), (1,3), (2,2), (2,3), (3,3), и наоборот. - person Joseph Wood; 15.09.2019
comment
@JosephWood, ты прав, спасибо. Отредактировано, чтобы исправить. - person akraf; 03.05.2021