Самый быстрый способ кросс-таблицы двух массивных логических векторов в R - PullRequest
15 голосов
/ 07 февраля 2012

Для двух логических векторов, x и y, длиной> 1E8, какой самый быстрый способ расчета кросс-таблиц 2x2?

Я подозреваю, что ответ состоит в том, чтобы написать это на C / C ++, но мне интересно, есть ли в R что-то, что уже достаточно умно относится к этой проблеме, поскольку это не редкость.

Пример кода для 300M записей (не стесняйтесь указывать N = 1E8, если 3E8 слишком велик; я выбрал общий размер чуть меньше 2,5 ГБ (2,4 ГБ). Я выбрал плотность 0,02, чтобы сделать его более интересным (можно использовать разреженный вектор, если это поможет, но преобразование типов может занять время).

set.seed(0)
N = 3E8
p = 0.02
x = sample(c(TRUE, FALSE), N, prob = c(p, 1-p), replace = TRUE)
y = sample(c(TRUE, FALSE), N, prob = c(p, 1-p), replace = TRUE)

Некоторые очевидные методы:

  1. table
  2. bigtabulate
  3. Простые логические операции (например, sum(x & y))
  4. Умножение вектора (бу)
  5. data.table
  6. Некоторые из вышеперечисленных, с parallel из пакета multicore (или с новым пакетом parallel)

Я сделал удар по первым трем вариантам (см. Мой ответ), но я чувствую, что должно быть что-то лучше и быстрее.

Я считаю, что table работает очень медленно. bigtabulate кажется излишним для пары логических векторов. Наконец, выполнение ванильных логических операций выглядит как клудж, и он смотрит на каждый вектор слишком много раз (3X? 7X?), Не говоря уже о том, что он заполняет много дополнительной памяти во время обработки, что является огромной тратой времени.

Умножение вектора, как правило, плохая идея, но когда вектор разреженный, можно получить преимущество, сохранив его как таковой, а затем используя умножение вектора.

Не стесняйтесь варьировать N и p, если это продемонстрирует что-нибудь интересное поведение функций табуляции. :)


Обновление 1. Мой первый ответ дает время на трех наивных методах, что является основой для веры table медленно. Тем не менее, главное, что нужно понять, это то, что «логический» метод крайне неэффективен. Посмотрите, что он делает:

  • 4 логических векторных операции
  • 4 преобразования типа (логическое в целое или FP - для sum)
  • 4 векторных суммирования
  • 8 назначений (1 для логической операции, 1 для суммирования)

Не только это, но даже не скомпилировано и не распараллелено. Тем не менее, он все еще бьет штаны от table. Обратите внимание, что bigtabulate с дополнительным преобразованием типов (1 * cbind...) по-прежнему превосходит table.

Обновление 2. Чтобы никто не указал, что логические векторы в R поддерживают NA, и что это будет ключом в системе для этих перекрестных таблиц (что в большинстве случаев верно), я должен указать, что мои векторы родом из is.na() или is.finite(). :) Я отлаживал NA и другие неконечные значения - они были для меня головной болью . Если вы не знаете, являются ли все ваши записи NA, вы можете проверить с any(is.na(yourVector)) - это было бы разумно, прежде чем принять некоторые из идей, возникающих в этом Q & A.


Обновление 3. Брэндон Бертельсен задал в комментариях весьма разумный вопрос: зачем использовать так много данных, когда подвыборка (первоначальный набор, в конце концов, является образцом ;-)) может быть достаточной для целей создания кросс-табуляция? Не зацикливаться на статистике, но данные возникают из случаев, когда наблюдения TRUE очень редки для обеих переменных. Один из них является результатом аномалии данных, другой - из-за возможной ошибки в коде (возможной ошибки, потому что мы видим только результат вычисления - воспринимаем переменную x как «Garbage In», а y как «Garbage Out» Как результат, вопрос заключается в том, являются ли проблемы в выводе, вызванные кодом, исключительно теми случаями, когда данные являются аномальными, или есть ли другие случаи, когда хорошие данные портятся? (Вот почему я задал вопрос о останавливается при обнаружении NaN, NA или Inf .)

Это также объясняет, почему мой пример имеет низкую вероятность для значений TRUE; это действительно происходит гораздо реже, чем в 0,1% случаев.

Предлагает ли это другой путь решения? Да: это говорит о том, что мы можем использовать два индекса (то есть местоположения TRUE в каждом наборе) и пересечение подсчета множества. Я избегал пересечений наборов, потому что я был сожжен некоторое время назад Matlab (да, это R, но потерпите меня), который сначала отсортировал элементы набора перед пересечением. (Я смутно припоминаю, что сложность была еще более смущающей: как O(n^2) вместо O(n log n).)

Ответы [ 5 ]

11 голосов
/ 07 февраля 2012

Если вы выполняете много операций с огромными логическими векторами, взгляните на пакет bit .Это экономит тонну памяти, сохраняя логические значения как истинные 1-битные логические значения.

Это не помогает с table;это на самом деле ухудшает ситуацию, потому что в битовом векторе больше уникальных значений из-за того, как он построен.Но это действительно помогает с логическими сравнениями.

# N <- 3e7
require(bit)
xb <- as.bit(x)
yb <- as.bit(y)
benchmark(replications = 1, order = "elapsed", 
    bit = {res <- func_logical(xb,yb)},
    logical = {res <- func_logical(x,y)}
)
#      test replications elapsed relative user.self sys.self user.child sys.child
# 1     bit            1   0.129  1.00000     0.132    0.000          0         0
# 2 logical            1   3.677 28.50388     2.684    0.928          0         0
10 голосов
/ 07 февраля 2012

Вот результаты для логического метода, table и bigtabulate, для N = 3E8:

         test replications elapsed relative user.self sys.self
2     logical            1  23.861 1.000000     15.36     8.50
3 bigtabulate            1  36.477 1.528729     28.04     8.43
1       table            1 184.652 7.738653    150.61    33.99

В этом случае table является катастрофой.

Для сравнения, здесь N = 3E6:

         test replications elapsed relative user.self sys.self
2     logical            1   0.220 1.000000      0.14     0.08
3 bigtabulate            1   0.534 2.427273      0.45     0.08
1       table            1   1.956 8.890909      1.87     0.09

На данный момент кажется, что написание собственных логических функций лучше, даже если это злоупотребляет sum, и проверяет каждый логический вектор несколько раз.Я еще не пробовал компилировать функции, но это должно дать лучшие результаты.

Обновление 1 Если мы дадим bigtabulate значения, которые уже являются целыми числами, т.е. если мы делаем преобразование типов1 * cbind(v1,v2) вне bigtabulate, тогда множитель N = 3E6 равен 1,80 вместо 2,4.Множество N = 3E8 относительно «логического» метода составляет всего 1,21 вместо 1,53.


Обновление 2

Как отметил Джошуа Ульрих,преобразование в битовые векторы является значительным улучшением - мы выделяем и перемещаемся на ОДНОМ меньшем количестве данных: логические векторы R потребляют 4 байта на запись («Почему?», вы можете спросить ... Ну, я незнаю, но здесь может появиться ответ. ), тогда как битовый вектор потребляет, ну, один бит на запись - т.е. 1/32 столько же данных.Итак, x потребляет 1,2e9 байт, а xb (битовая версия в приведенном ниже коде) потребляет всего 3,75e7 байт.

Я отбросил table и bigtabulate вариации отобновленные тесты (N = 3e8).Обратите внимание, что logicalB1 предполагает, что данные уже являются битовым вектором, а logicalB2 - та же операция со штрафом за преобразование типов.Поскольку мои логические векторы являются результатом операций с другими данными, я не имею преимущества начинать с битового вектора.Тем не менее, подлежащий выплате штраф относительно невелик.[Серия «logic3» выполняет только 3 логические операции, а затем выполняет вычитание.Так как это кросс-табулирование, мы знаем общее количество, как заметил DWin.]

        test replications elapsed  relative user.self sys.self
4 logical3B1            1   1.276  1.000000      1.11     0.17
2  logicalB1            1   1.768  1.385580      1.56     0.21
5 logical3B2            1   2.297  1.800157      2.15     0.14
3  logicalB2            1   2.782  2.180251      2.53     0.26
1    logical            1  22.953 17.988245     15.14     7.82

Теперь мы ускорили это, потратив всего 1,8-2,8 секунды, даже при большом количестве неэффективности.Существует без сомнения , это должно быть выполнимо в течение менее чем 1 секунды, с изменениями, включающими один или несколько из следующих элементов: код на C, компиляция и многоядерная обработка.После того, как все 3 (или 4) различных логических операции могут быть выполнены независимо, хотя это все еще пустая трата вычислительных циклов.

Самый похожий из лучших претендентов, logical3B2, примерно в 80 раз быстрее, чем table.Это примерно в 10 раз быстрее, чем наивная логическая операция.И у него все еще есть много возможностей для улучшения.


Вот код, чтобы произвести выше. NOTE Я рекомендую закомментировать некоторые операции или векторы, если у вас недостаточно ОЗУ - создание x, x1 и xb вместе с соответствующими y объектами, займет немного памяти.

Кроме того, обратите внимание: я должен был использовать 1L в качестве целочисленного множителя для bigtabulate вместо просто 1.В какой-то момент я перезапущу это изменение и рекомендую это изменение всем, кто использует подход bigtabulate.

library(rbenchmark)
library(bigtabulate)
library(bit)

set.seed(0)
N <- 3E8
p <- 0.02

x <- sample(c(TRUE, FALSE), N, prob = c(p, 1-p), replace = TRUE)
y <- sample(c(TRUE, FALSE), N, prob = c(p, 1-p), replace = TRUE)

x1 <- 1*x
y1 <- 1*y

xb <- as.bit(x)
yb <- as.bit(y)

func_table  <- function(v1,v2){
    return(table(v1,v2))
}

func_logical  <- function(v1,v2){
    return(c(sum(v1 & v2), sum(v1 & !v2), sum(!v1 & v2), sum(!v1 & !v2)))
}

func_logicalB  <- function(v1,v2){
    v1B <- as.bit(v1)
    v2B <- as.bit(v2)
    return(c(sum(v1B & v2B), sum(v1B & !v2B), sum(!v1B & v2B), sum(!v1B & !v2B)))
}

func_bigtabulate    <- function(v1,v2){
    return(bigtabulate(1*cbind(v1,v2), ccols = c(1,2)))
}

func_bigtabulate2    <- function(v1,v2){
    return(bigtabulate(cbind(v1,v2), ccols = c(1,2)))
}

func_logical3   <- function(v1,v2){
    r1  <- sum(v1 & v2)
    r2  <- sum(v1 & !v2)
    r3  <- sum(!v1 & v2)
    r4  <- length(v1) - sum(c(r1, r2, r3))
    return(c(r1, r2, r3, r4))
}

func_logical3B   <- function(v1,v2){
    v1B <- as.bit(v1)
    v2B <- as.bit(v2)
    r1  <- sum(v1B & v2B)
    r2  <- sum(v1B & !v2B)
    r3  <- sum(!v1B & v2B)
    r4  <- length(v1) - sum(c(r1, r2, r3))
    return(c(r1, r2, r3, r4))
}

benchmark(replications = 1, order = "elapsed", 
    #table = {res <- func_table(x,y)},
    logical = {res <- func_logical(x,y)},
    logicalB1 = {res <- func_logical(xb,yb)},
    logicalB2 = {res <- func_logicalB(x,y)},

    logical3B1 = {res <- func_logical3(xb,yb)},
    logical3B2 = {res <- func_logical3B(x,y)}

    #bigtabulate = {res <- func_bigtabulate(x,y)},
    #bigtabulate2 = {res <- func_bigtabulate2(x1,y1)}
)
4 голосов
/ 14 февраля 2012

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

N <- 1e8
x <- sample(c(T,F),N,replace=T)
y <- sample(c(T,F),N,replace=T)

func_logical  <- function(v1,v2){
    return(c(sum(v1 & v2), sum(v1 & !v2), sum(!v1 & v2), sum(!v1 & !v2)))
}


library(Rcpp)
library(inline)

doCrossTab1 <- cxxfunction(signature(x="integer", y = "integer"), body='
  Rcpp::LogicalVector Vx(x);
  Rcpp::LogicalVector Vy(y);
  Rcpp::IntegerVector V(4);

  V[0] = sum(Vx*Vy);
  V[1] = sum(Vx*!Vy);
  V[2] = sum(!Vx*Vy);
  V[3] = sum(!Vx*!Vy);
  return( wrap(V));
  '
, plugin="Rcpp")

system.time(doCrossTab1(x,y))

require(bit)
system.time(
{
xb <- as.bit(x)
yb <- as.bit(y)
func_logical(xb,yb)
})

, что приводит к:

> system.time(doCrossTab1(x,y))
   user  system elapsed 
  1.067   0.002   1.069 
> system.time(
+ {
+ xb <- as.bit(x)
+ yb <- as.bit(y)
+ func_logical(xb,yb)
+ })
   user  system elapsed 
  1.451   0.001   1.453 

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

Обновление: в честь Iterator, вот решение Rcpp для итератора:

doCrossTab2 <- cxxfunction(signature(x="integer", y = "integer"), body='
  Rcpp::LogicalVector Vx(x);
  Rcpp::LogicalVector Vy(y);
  Rcpp::IntegerVector V(4);
    V[0]=V[1]=V[2]=V[3]=0;
  LogicalVector::iterator itx = Vx.begin();
  LogicalVector::iterator ity = Vy.begin();
  while(itx!=Vx.end()){
    V[0] += (*itx)*(*ity);
    V[1] += (*itx)*(!*ity);
    V[2] += (!*itx)*(*ity);
    V[3] += (!*itx)*(!*ity);    
    itx++;
    ity++;
  }
  return( wrap(V));
  '
, plugin="Rcpp")

system.time(doCrossTab2(x,y))
#   user  system elapsed 
#  0.780   0.001   0.782 
2 голосов
/ 08 февраля 2012

Другая тактика состоит в том, чтобы рассматривать просто набор пересечений, используя индексы значений TRUE, используя преимущество того, что выборки очень смещены (т. Е. В основном FALSE).

С этой целью япредставить func_find01 и перевод, использующий пакет bit (func_find01B);весь код, который не указан в ответе выше, вставлен ниже.

Я перезапустил полную оценку N = 3e8, кроме забытого использования func_find01B;Я перехожу на более быстрые методы против него, за второй проход.

          test replications elapsed   relative user.self sys.self
6   logical3B1            1   1.298   1.000000      1.13     0.17
4    logicalB1            1   1.805   1.390601      1.57     0.23
7   logical3B2            1   2.317   1.785054      2.12     0.20
5    logicalB2            1   2.820   2.172573      2.53     0.29
2       find01            1   6.125   4.718798      4.24     1.88
9 bigtabulate2            1  22.823  17.583205     21.00     1.81
3      logical            1  23.800  18.335901     15.51     8.28
8  bigtabulate            1  27.674  21.320493     24.27     3.40
1        table            1 183.467 141.345917    149.01    34.41

Только "быстрые" методы:

        test replications elapsed relative user.self sys.self
3     find02            1   1.078 1.000000      1.03     0.04
6 logical3B1            1   1.312 1.217069      1.18     0.13
4  logicalB1            1   1.797 1.666976      1.58     0.22
2    find01B            1   2.104 1.951763      2.03     0.08
7 logical3B2            1   2.319 2.151206      2.13     0.19
5  logicalB2            1   2.817 2.613173      2.50     0.31
1     find01            1   6.143 5.698516      4.21     1.93

Таким образом, find01B самый быстрый среди методов, которые неиспользуйте предварительно преобразованные битовые векторы с небольшим запасом (2,099 секунды против 2,372 секунды).Откуда взялся find02?Впоследствии я написал версию, в которой используются предварительно вычисленные битовые векторы.Сейчас это самый быстрый способ.

В целом, время выполнения подхода «метод индексов» может зависеть от предельных и совместных вероятностей.Я подозреваю, что это было бы особенно конкурентоспособно, когда вероятности еще ниже, но нужно знать, что априори, или через подвыборку.


Обновление 1. Я также приурочил Джош О«Предложение Брайена, используя tabulate() вместо table().По истечении 12 секунд результаты составляют примерно 2X find01 и примерно половину bigtabulate2.Теперь, когда лучшие методы приближаются к 1 секунде, это также относительно медленно:

 user  system elapsed 
7.670   5.140  12.815 

Код:

func_find01 <- function(v1, v2){
    ix1 <- which(v1 == TRUE)
    ix2 <- which(v2 == TRUE)

    len_ixJ <- sum(ix1 %in% ix2)
    len1    <- length(ix1)
    len2    <- length(ix2)
    return(c(len_ixJ, len1 - len_ixJ, len2 - len_ixJ,
             length(v1) - len1 - len2 + len_ixJ))
}

func_find01B <- function(v1, v2){
    v1b = as.bit(v1)
    v2b = as.bit(v2)

    len_ixJ <- sum(v1b & v2b)
    len1 <- sum(v1b)
    len2 <- sum(v2b)

    return(c(len_ixJ, len1 - len_ixJ, len2 - len_ixJ,
             length(v1) - len1 - len2 + len_ixJ))
}

func_find02 <- function(v1b, v2b){
    len_ixJ <- sum(v1b & v2b)
    len1 <- sum(v1b)
    len2 <- sum(v2b)

    return(c(len_ixJ, len1 - len_ixJ, len2 - len_ixJ,
             length(v1b) - len1 - len2 + len_ixJ))
}

func_bigtabulate2    <- function(v1,v2){
    return(bigtabulate(cbind(v1,v2), ccols = c(1,2)))
}

func_tabulate01 <- function(v1,v2){
    return(tabulate(1L + 1L*x + 2L*y))
}

benchmark(replications = 1, order = "elapsed", 
    table = {res <- func_table(x,y)},
    find01  = {res <- func_find01(x,y)},
    find01B  = {res <- func_find01B(x,y)},
    find02  = {res <- func_find01B(xb,yb)},
    logical = {res <- func_logical(x,y)},
    logicalB1 = {res <- func_logical(xb,yb)},
    logicalB2 = {res <- func_logicalB(x,y)},

    logical3B1 = {res <- func_logical3(xb,yb)},
    logical3B2 = {res <- func_logical3B(x,y)},

    tabulate    = {res <- func_tabulate(x,y)},
    bigtabulate = {res <- func_bigtabulate(x,y)},
    bigtabulate2 = {res <- func_bigtabulate2(x1,y1)}
)
1 голос
/ 13 февраля 2012

Вот ответ с Rcpp, сведя в таблицу только те записи, которые не оба 0. Я подозреваю, что должно быть несколько способов улучшить это, поскольку это необычно медленно; это также моя первая попытка использования Rcpp, поэтому могут быть некоторые очевидные недостатки, связанные с перемещением данных. Я написал пример, который является преднамеренно простым ванилью, который должен позволить другим продемонстрировать, как это можно улучшить.

library(Rcpp)
library(inline)
doCrossTab <- cxxfunction(signature(x="integer", y = "integer"), body='
  Rcpp::IntegerVector Vx(x);
  Rcpp::IntegerVector Vy(y);
  Rcpp::IntegerVector V(3);
  for(int i = 0; i < Vx.length(); i++) {
    if( (Vx(i) == 1) & ( Vy(i) == 1) ){ V[0]++; } 
    else if( (Vx(i) == 1) & ( Vy(i) == 0) ){ V[1]++; } 
    else if( (Vx(i) == 0) & ( Vy(i) == 1) ){ V[2]++; } 
 }
  return( wrap(V));
  ', plugin="Rcpp")

Сроки результаты для N = 3E8:

   user  system elapsed 
 10.930   1.620  12.586 

В моем втором ответе это занимает более чем 6X больше, чем func_find01B.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...