R: эффективное вычисление сумм значений-подмножеств, содержание которых определяется отношением между двумя переменными - PullRequest
0 голосов
/ 24 мая 2018

У меня есть две таблицы, A и B.Для каждой строки таблицы A я хочу получить сводную статистику для B$value, где значение B$location находится в пределах 100 от A$location.Я выполнил это, используя цикл for, приведенный ниже, но это медленное решение, которое хорошо работает, когда таблицы маленькие, но я хотел бы увеличить масштаб до таблицы A, которая состоит из тысяч строк и таблицы Bэто почти миллионы строк.Есть идеи как этого добиться?Заранее спасибо!

Цикл for:

for (i in 1:nrow(A)) {    
   subset(B, abs(A$location[i] - B$location) <= 100) -> temp
   A$n[i] <- nrow(temp)
   A$sum[i] <- sum(temp$value)
   A$avg[i] <- mean(temp$value)
}    

Пример:
A loc 150 250 400
B loc value 25 7 77 19 170 10 320 15

станет:
A loc n sum avg 150 2 29 14.5 250 2 25 12.5 400 1 15 15

Ответы [ 6 ]

0 голосов
/ 25 мая 2018

Подобно ответу Мэтта Саммерсгилла, вы можете сделать неэкви-соединение для обновления A:

A[, up := loc + 100]
A[, dn := loc - 100]
A[, c("n", "s", "m") := 
  B[copy(.SD), on=.(loc >= dn, loc <= up), .(.N, sum(value), mean(value)), by=.EACHI][, .(N, V2, V3)]
]

Или в одной цепочечной команде:

A[, up := loc + 100][, dn := loc - 100][, c("n", "s", "m") := 
  B[copy(.SD), on=.(loc >= dn, loc <= up), 
    .(.N, sum(value), mean(value)), by=.EACHI][, 
    .(N, V2, V3)]
]

Это должно бытьдовольно эффективный, я думаю.

Как это работает

Внутри j из x[i, j], .SD относится к подмножеству данных из x (в этом случае все это A).

x[i, on=, j, by=.EACHI] является объединением, использующим каждую строку i (в данном случае copy(.SD) == A) для поиска соответствующих строкx (в данном случае B) с использованием условий в on=.Для каждой строки i вычисляется j (что означает by=.EACHI).

Когда j не имеет имен, они назначаются автоматически.V1, V2 и т. Д..N по умолчанию получает имя N.

0 голосов
/ 24 мая 2018

Это возможно с помощью функции foverlaps в пределах data.table, и следующий метод фактически выполняет молитву по завершению вашего фактического варианта использования - A, что составляет тысячи строк, и таблицу B, котораяэто почти миллионы строк - в разумные сроки.


На примере вашей игрушки:

library(data.table)

A <- fread("
           loc
           150
           250
           400")

B <- fread("
           loc    value
           25     7
           77     19
           170    10
           320    15")

## Create a 'dummy' value to create an interval w/same start and end in A
A[,loc_Dummy := loc]

## Create values bounding the match range for loc in B
B[,loc_Plus100 := loc + 100]
B[,loc_Minus100 := loc - 100]

## Set up for the overlap join
setkey(A,loc,loc_Dummy)
setkey(B,loc_Minus100, loc_Plus100)

## Create a table of only matches instead of doing a full cartesian join of all cases
Matches <- foverlaps(A[,.(loc, loc_Dummy)],
                     B[,.(loc_Minus100,loc_Plus100,value)])

## Create a summary table
Matches[,.(n = .N, sum = sum(value), avg = mean(value)), by = .(loc)]

#    loc n sum  avg
# 1: 150 2  29 14.5
# 2: 250 2  25 12.5
# 3: 400 1  15 15.0

Увеличение - yikes!

Однако - это на самом деле чрезвычайно вычислительно интенсивная проблема.Масштабирование до фактических размеров дел показывает здесь проблему - используя 10 000 строк для таблицы A и 1 000 000 строк для таблицы B, этот метод завершается за 91 секунд на сервере, на котором я работаю, но использует более 112 ГБ памяти !

A <- data.table(loc = sample.int(1000, size = 1e4, replace = TRUE))


B <- data.table(loc = sample.int(1000, size = 1e6, replace = TRUE),
                value = sample.int(100, size = 1e6, replace = TRUE))

system.time({
  A[,loc_Dummy := loc]
  B[,loc_Plus100 := loc + 100]
  B[,loc_Minus100 := loc - 100]

  setkey(A,loc,loc_Dummy)
  setkey(B,loc_Minus100, loc_Plus100)

  Matches <- foverlaps(A[,.(loc, loc_Dummy)],
                       B[,.(loc_Minus100,loc_Plus100,value)])

  Summary  <- Matches[,.(n = .N, sum = sum(value), avg = mean(value)), by = .(loc)]

})

## Warning: Memory usage peaks at ~112 GB!

# user  system elapsed 
# 56.407  46.258  91.135

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

Если у вас нет сотен гигабайт памяти в вашем распоряжении, вам, вероятно, придется немного более умно подходить к этому и перебирать куски за раз.

Из того, что я могу сказать, ваша проблема на самом деле похожа на ту, которая была поставлена ​​(и решена) Лоренцо Бусетто и подробно описана в сообщении в блоге: Ускорение пространственного анализа за счет интеграции sf иdata.table: контрольный пример .


Разделение на части

Требование более ~ 100 гигабайт памяти не является действительно возможным решением, особенно если вы хотите масштабировать Aили B на порядок выше в какой-то момент.

Однако метод разбиения на куски (вдохновленный постом Лоренцо, связанным выше), который разбивает проблему на 100 частей на самом деле только увеличиваетсяво время выполнения тривиальная величина составляет 116 секунд , но сокращает пиковое использование памяти до менее 3 ГБ !Если бы я планировал сделать это в производстве, я бы сделал что-то вроде следующего.

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

A <- data.table(loc = sample.int(1000, size = 1e4, replace = TRUE))

B <- data.table(loc = sample.int(1000, size = 1e6, replace = TRUE),
                value = sample.int(100, size = 1e6, replace = TRUE))

system.time({

  A[,loc_Dummy := loc]
  B[,loc_Plus100 := loc + 100]
  B[,loc_Minus100 := loc - 100]

  setkey(A,loc)
  setkey(B,loc)

  ChunkCount <- 100
  ChunkSize <- A[,.N/ChunkCount]

  ResultList <- vector("list", ChunkCount) 

  for (j in seq_len(ChunkCount)){

    A_loc_Min <- A[((j-1)*ChunkSize + 1):(min(nrow(A),(j)*ChunkSize)), min(loc)]
    A_loc_Max <- A[((j-1)*ChunkSize + 1):(min(nrow(A),(j)*ChunkSize)), max(loc)]

    A_Sub <- A[loc >= A_loc_Min & loc < A_loc_Max]
    B_Sub <- B[loc_Plus100 >= A_loc_Min & loc_Minus100 < A_loc_Max]

    setkey(A_Sub,loc,loc_Dummy)
    setkey(B_Sub,loc_Minus100, loc_Plus100)

    Matches <- foverlaps(A_Sub[,.(loc, loc_Dummy)],
                         B_Sub[,.(loc_Minus100,loc_Plus100,value)])

    ResultList[[j]]  <- Matches[,.(n = .N, sum = sum(value), avg = mean(value)), by = .(loc)]

  }

  Summary  <- rbindlist(ResultList)

})

#    user  system elapsed 
# 109.125  16.864 116.129 

Проверка

Обновление: предложения @Alexis и @ Frank в комментариях приводят к одному и тому же набору результатов, у меня получается немного другой, но только по количеству.Если кто-то еще сможет подтвердить, что правильный ответ действительно предоставлен @ Alexis / @ Frank, то я был бы рад отозвать свой ответ, поскольку оба метода выполняются быстрее, чем предложенный мной.

set.seed(1234)

A <- data.table(loc = sample.int(1000, size = 1e3, replace = TRUE))

B <- data.table(loc = sample.int(1000, size = 1e4, replace = TRUE),
                value = sample.int(10, size = 1e4, replace = TRUE))



## Matt 
Matt_A <- copy(A)
Matt_B <- copy(B)

Matt_A[,loc_Dummy := loc]
Matt_B[,loc_Plus100 := loc + 100]
Matt_B[,loc_Minus100 := loc - 100]

setkey(Matt_A,loc,loc_Dummy)
setkey(Matt_B,loc_Minus100, loc_Plus100)

Matches <- foverlaps(Matt_A[,.(loc, loc_Dummy)],
                     Matt_B[,.(loc_Minus100,loc_Plus100,value)])

Summary_Matt  <- Matches[,.(n = .N, sum = sum(value), avg = mean(value)), keyby = .(loc)]


## Alexis

Rcpp::sourceCpp("RowRanges.cpp")

A_loc <- sort(A$loc, decreasing = FALSE)
B <- B[order(B$loc),]
Alexis <- foo(unique(A_loc), B$loc, B$value)

Summary_Alexis <- as.data.table(Alexis)
colnames(Summary_Alexis) <- c("n","sum","avg")

Summary_Alexis[,loc := unique(A_loc)]
setcolorder(Summary_Alexis, c("loc","n","sum","avg"))

## Frank

Frank <- A[, up := loc + 100][
  , dn := loc - 100][
    , c("n", "s", "m") := B[copy(.SD), on=.(loc >= dn, loc <= up), .(.N, sum(value), mean(value)), by=.EACHI][
      , .(N, V2, V3)]][]

Summary_Frank <- unique(Frank[,.(loc,n, sum = s, avg = m)][order(loc)])

## Comparing

all.equal(Summary_Frank,Summary_Alexis)
# [1] TRUE

all.equal(Summary_Frank,Summary_Matt)
# [1] "Column 'n': Mean relative difference: 1.425292"
0 голосов
/ 24 мая 2018

Мое решение на чистом R (ниже) все еще значительно медленнее, в моей системе потребовалось 32 секунды, чтобы закончить большой пример Мэтта Саммерсгилла, но по сравнению с другими решениями оно все еще разумно.

Логика моего решенияявляется то, что, поскольку входы отсортированы, поскольку вы рассматриваете новые элементы A_loc, диапазон значений в B_loc либо останется прежним, если новый элемент A_loc идентичен предыдущему, либо он сместится вправо на B_loc, возможно, сокращается или расширяется.Обратите внимание, что если бы вы работали с double входами, вам пришлось бы быть немного более осторожным со сравнениями.

Эта версия C ++, естественно, быстрее.Если вы можете Rcpp::sourceCpp этот код:

#include <Rcpp.h>
using namespace Rcpp;

// [[Rcpp::export]]
DataFrame foo(IntegerVector A_loc, IntegerVector B_loc, IntegerVector B_val) {
    IntegerVector n(A_loc.length());
    IntegerVector sum(A_loc.length());
    NumericVector avg(A_loc.length());

    int lower = 0;
    int upper = 0;
    int count = 0;
    int current_sum = 0;
    for (int i = 0; i < A_loc.length(); i++) {
        checkUserInterrupt();

        while (lower < B_loc.length()) {
            if (B_loc[lower] >= A_loc[i] - 100) {
                break;
            }

            if (count > 0) {
                count--;
                current_sum -= B_val[lower];
            }

            lower++;
        }

        if (upper < lower) {
            upper = lower;
        }

        while (upper < B_loc.length()) {
            if (B_loc[upper] > A_loc[i] + 100) {
                break;
            }

            count++;
            current_sum += B_val[upper++];
        }

        n[i] = count;
        sum[i] = current_sum;
        avg[i] = static_cast<double>(current_sum) / count;
    }

    DataFrame df = DataFrame::create(
        Named("loc") = A_loc,
        Named("n") = n,
        Named("sum") = sum,
        Named("avg") = avg
    );

    return df;
}

, тогда это:

A <- data.frame(loc = sample.int(1000, size = 1e4, replace = TRUE))


B <- data.frame(loc = sample.int(1000, size = 1e6, replace = TRUE),
                value = sample.int(100, size = 1e6, replace = TRUE))

test <- function() {
    # remove unique if you want to consider duplicated values
    A_loc <- sort(unique(A$loc), decreasing = FALSE)
    B <- B[order(B$loc),]
    out <- foo(A_loc, B$loc, B$value)
}

microbenchmark::microbenchmark(test())

показывает следующие временные параметры:

Unit: milliseconds
   expr      min      lq     mean   median       uq      max neval
 test() 44.74469 45.8118 51.35361 47.34657 48.99376 95.00938   100

Если вы не можете использовать Rcpp, а затем рассмотрим версию R ниже или решение Фрэнка с data.table, я думаю, что сортировка входных данных также может помочь в этом случае?


for В R обычно избегают циклов, но я неНе думайте, что они всегда медленные, вы просто должны быть осторожны, чтобы не копировать данные слишком много.Кроме того, поскольку R v3.5.0 при записи чего-то вроде for i in 1:10 больше не выделяет сначала весь вектор, он поддерживает компактное представление.

A_loc <- sort(unique(A$loc), decreasing = FALSE)
B <- B[order(B$loc),]

out <- data.frame(loc = A_loc,
                  n = 0L,
                  sum = 0L,
                  avg = 0)

lower <- 1L
upper <- 1L
count <- 0L
sum <- 0L
upper_limit <- nrow(B)
for (i in seq_along(A_loc)) {
  current_loc <- A_loc[i]

  while (lower <= upper_limit) {
    if (B$loc[lower] >= current_loc - 100L) {
      break
    }

    if (count > 0L) {
      count <- count - 1L
      sum <- sum - B$value[lower]
    }

    lower <- lower + 1L
  }

  if (upper < lower) {
    upper <- lower
  }

  while (upper <= upper_limit) {
    if (B$loc[upper] > current_loc + 100L) {
      break
    }

    count <- count + 1L
    sum <- sum + B$value[upper]
    upper <- upper + 1L
  }

  out$n[i] <- count
  out$sum[i] <- sum
  out$avg[i] <- sum / count
}
0 голосов
/ 24 мая 2018

Вот решение tidyverse

library(tidyverse)

A = read.table(text = "
loc
150
250
400
", header=T)

B = read.table(text = "
loc    value
25     7
77     19
170    10
320    15
", header=T)

A %>%
  mutate(B = list(B)) %>%              # create all combinations of rows of A and B
  unnest() %>%
  filter(abs(loc - loc1) <= 100) %>%   # keep rows that satisfy your condition
  group_by(loc) %>%                    # for each loc values
  summarise(sum = sum(value),          # calculate sum
            avg = mean(value))         # calculate mean

# # A tibble: 3 x 3
#     loc   sum   avg
#    <int> <int> <dbl>
# 1   150    29  14.5
# 2   250    25  12.5
# 3   400    15  15  

Возможно, не лучшее решение, если у вас большие таблицы A и B, так как вам нужно создать все комбинации строк и затем отфильтровать.

0 голосов
/ 24 мая 2018

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

A <- within(A,{
 B.filter <- outer(B$loc, A$loc, function(x, y) abs(x - y) <= 100) 

 n <- colSums(B.filter)
 sum <- colSums(B$value * B.filter)
 avg <- sum / n
 rm(B.filter)
})

Если местоположения в A и / или B повторяются, вы можете уменьшить размер матрицы фильтра, используя только уникальные значения:

A <- within(A,{
 B.filter <- outer(unique(B$loc), unique(A$loc), function(x, y) abs(x - y) <= 100) 
 colnames(B.filter) <- unique(A$loc)
 rownames(B.filter) <- unique(B$loc)

 n <- colSums(B.filter[,as.character(A$loc)])
 sum <- colSums(B$value * B.filter[as.character(B$loc),])
 avg <- sum / n
 rm(B.filter)
})
0 голосов
/ 24 мая 2018

Обычно я не предлагаю решения, основанные на установке пакетов, но я думаю, что этот поможет вам.Он установит пакет, который позволит вам кодировать в SQL внутри R.

# Load the package
install.packages("sqldf")
library(sqldf)

# Create tables
A <- data.frame("loc"=c(150,250,400))
B <- data.frame("loc"=c(25,77,170,320),"value"=c(7,19,10,15))


# Join tables
df0 <- sqldf('select a.loc
                    ,count(b.value) as n_value
                    ,sum(b.value) as sum_value
                    ,avg(b.value) as avg_value
              from A as a
              left join B as b
              on abs(a.loc - b.loc) <= 100
              group by a.loc')

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