(Пространственный) Эффективный способ поиска всех точек в пределах X метров от точки?

У меня есть большой набор пространственных данных (12 миллионов строк). Геометрия - это точки на карте. Для каждой строки в наборе данных я хотел бы найти все точки, которые находятся в пределах 500 метров от этой точки.

В r, используя sf, я пытался сделать это путем параллельного цикла по каждой строке и запуска st_buffer и st_intersects, затем сохраняя результат в виде списка в формате ключ-значение (ключ является точкой происхождения, значения соседи).

Проблема в том, что набор данных слишком велик. Даже при распараллеливании до 60 ядер операция занимает слишком много времени (> 1 недели и обычно дает сбой).

Каковы альтернативы этому методу грубой силы? Можно ли строить индексы с помощью sf? Возможно, перенесите операцию во внешнюю базу данных?

Реплекс:

library(sf)
library(tidyverse)
library(parallel)
library(foreach)


# example data, convert to decimal:
nc <- st_read(system.file("shape/nc.shp", package="sf")) %>% st_transform(32618)
# expand the data a a bit to make the example more interesting:
nc <- rbind(nc,nc,nc)
nc <- nc %>% mutate(Id = row_number())


## can run in parallel if desired:
# num_cores <- parallel::detectCores()-2
# cl <- makeSOCKcluster(num_cores)
# registerDoSNOW(cl)

# or just run in sequence:
registerDoSEQ()

neighbors <- foreach(ii = 1:nrow(nc)
                      , .verbose = FALSE
                      , .errorhandling = "pass") %dopar% {

                        l = 500 # 500 meters

                        # isolate the row as the origin point:
                        row_interest <- filter(nc, row_number()==ii)

                        # create the buffer:
                        buffer <- row_interest %>% st_buffer(dist = l)

                        # extract the row numbers of the neighbors
                        comps_idx <- suppressMessages(st_intersects(buffer, nc))[[1]]

                        # get all the neighbors:
                        comps <- nc %>% filter(row_number() %in% comps_idx)

                        # remove the geometry:
                        comps <- comps %>% st_set_geometry(NULL)

                        # flow control in case there are no neibors:
                        if(nrow(comps)>0) {
                          comps$Origin_Key <- row_interest$Id
                        } else {
                          comps <- data_frame("lat" = NA_integer_,"lon" = NA_integer_, "bbl" = row_interest$bbl)
                          comps$Origin_Key <- row_interest$Id
                        }


                        return(comps)
                      }

closeAllConnections()

length(neighbors)==nrow(nc)
[1] TRUE

r sf
person Tim_K    schedule 06.02.2018    source источник
comment
не могли бы вы привести минимальный пример, чтобы мы могли что-нибудь попробовать? См. stackoverflow.com / questions / 5963269 /   -  person denis    schedule 06.02.2018
comment
Извините, я думал, что предоставленного мной примера кода должно быть достаточно? Что относительно примера, который я опубликовал, не соответствует стандарту воспроизводимости примера?   -  person Tim_K    schedule 07.02.2018
comment
@Tim_K В конце концов мне стало любопытно, и я реализовал интегрированное возможное решение sf + data.table. Возможно, вас заинтересует обновленный ответ ниже.   -  person lbusett    schedule 20.02.2018
comment
Вам следует подумать о том, чтобы взглянуть на этот пост: gis.stackexchange.com/questions/255671/; У меня была такая же проблема, и я решил ее с помощью аппроксимации и data.table подмножества, которые также можно легко запустить параллельно. Я не уверен, что это самый быстрый способ сделать это, но для 9 * 10 ^ 6 требуется около 80 часов на одно ядро, 40 часов на 2 ядра и так далее.   -  person nilsole    schedule 22.02.2018
comment
nilsole этот пост поможет обдумать проблему. Предлагаемое решение состоит в предварительной фильтрации с использованием подмножества квадратов перед вычислением точки в многоугольнике. Подобно ответу @lbusett ниже, но подмножество выполняется для каждой отдельной точки, а не вырезать всю плоскость в сетку nxn   -  person Tim_K    schedule 24.02.2018


Ответы (3)


При работе с объектами sf явный цикл по функциям для выполнения бинарных операций, таких как пересечения, обычно контрпродуктивен (см. Также Как я могу ускорить пространственные операции в` dplyr :: mutate () `?)

Подход, аналогичный вашему (т.е. буферизация и пересечение), но без явного цикла for работает лучше.

Давайте посмотрим, как он работает на достаточно большом наборе данных из 50000 точек:

library(sf)
library(spdep)
library(sf)

pts <- data.frame(x = runif(50000, 0, 100000),
                  y = runif(50000, 0, 100000))
pts     <- sf::st_as_sf(pts, coords = c("x", "y"), remove = F)
pts_buf <- sf::st_buffer(pts, 5000)
coords  <- sf::st_coordinates(pts)

microbenchmark::microbenchmark(
  sf_int = {int <- sf::st_intersects(pts_buf, pts)},
  spdep  = {x   <- spdep::dnearneigh(coords, 0, 5000)}
  , times = 1)
#> Unit: seconds
#>    expr       min        lq      mean    median        uq       max neval
#>  sf_int  21.56186  21.56186  21.56186  21.56186  21.56186  21.56186     1
#>   spdep 108.89683 108.89683 108.89683 108.89683 108.89683 108.89683     1

Вы можете видеть здесь, что подход st_intersects в 5 раз быстрее, чем метод dnearneigh.

К сожалению, это вряд ли решит вашу проблему. Глядя на время выполнения наборов данных разного размера, мы получаем:

subs <- c(1000, 3000, 5000, 10000, 15000, 30000, 50000)
times <- NULL
for (sub in subs[1:7]) {
  pts_sub <- pts[1:sub,]
  buf_sub <- pts_buf[1:sub,]
  t0 <- Sys.time()
  int <- sf::st_intersects(buf_sub, pts_sub)
  times <- cbind(times, as.numeric(difftime(Sys.time() , t0, units = "secs")))
}

plot(subs, times)

times <- as.numeric(times)
reg <- lm(times~subs+I(subs^2))
summary(reg)
#> 
#> Call:
#> lm(formula = times ~ subs + I(subs^2))
#> 
#> Residuals:
#>        1        2        3        4        5        6        7 
#> -0.16680 -0.02686  0.03808  0.21431  0.10824 -0.23193  0.06496 
#> 
#> Coefficients:
#>               Estimate Std. Error t value Pr(>|t|)    
#> (Intercept)  2.429e-01  1.371e-01   1.772    0.151    
#> subs        -2.388e-05  1.717e-05  -1.391    0.237    
#> I(subs^2)    8.986e-09  3.317e-10  27.087  1.1e-05 ***
#> ---
#> Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#> 
#> Residual standard error: 0.1908 on 4 degrees of freedom
#> Multiple R-squared:  0.9996, Adjusted R-squared:  0.9994 
#> F-statistic:  5110 on 2 and 4 DF,  p-value: 1.531e-07

Здесь мы видим почти идеальную квадратичную зависимость между временем и количеством точек (как и следовало ожидать). На подмножестве из 10 миллионов точек, предполагая, что поведение не изменится, вы получите:

predict(reg, newdata = data.frame(subs = 10E6))
#>        1 
#> 898355.4

, что соответствует примерно 10 дням, если предположить, что тренд будет постоянным при дальнейшем увеличении количества точек (но то же самое произойдет и для _9 _...)

Я предлагаю «разбить» ваши баллы на куски, а затем работать над ними.

Например, вы можете расположить свои точки в начале по оси x, а затем легко и быстро извлечь подмножества буферов и точек, с которыми их можно будет сравнить, используя data.table.

Ясно, что буфер «точек» должен быть больше, чем буфер «буферов», в зависимости от расстояния сравнения. Так, например, если вы создаете подмножество pts_buf с центроидами в [50000 - 55000], соответствующее подмножество pts должно включать точки в диапазоне [49500 - 55500]. Этот подход легко распараллелить, назначив разные подмножества разным ядрам в foreach или аналогичной конструкции.

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

HTH!

ОБНОВЛЕНИЕ

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

points_in_distance_parallel <- function(in_pts,
                                        maxdist,
                                        ncuts = 10) {

  require(doParallel)
  require(foreach)
  require(data.table)
  require(sf)
  # convert points to data.table and create a unique identifier
  pts <-  data.table(in_pts)
  pts <- pts[, or_id := 1:dim(in_pts)[1]]

  # divide the extent in quadrants in ncuts*ncuts quadrants and assign each
  # point to a quadrant, then create the index over "xcut"
  range_x  <- range(pts$x)
  limits_x <-(range_x[1] + (0:ncuts)*(range_x[2] - range_x[1])/ncuts)
  range_y  <- range(pts$y)
  limits_y <- range_y[1] + (0:ncuts)*(range_y[2] - range_y[1])/ncuts
  pts[, `:=`(xcut =  as.integer(cut(x, ncuts, labels = 1:ncuts)),
             ycut = as.integer(cut(y, ncuts, labels = 1:ncuts)))] %>%
    setkey(xcut, ycut)

  results <- list()

  cl <- parallel::makeCluster(parallel::detectCores() - 2, type =
                                ifelse(.Platform$OS.type != "windows", "FORK",
                                       "PSOCK"))
  doParallel::registerDoParallel(cl)
  # start cycling over quadrants
  out <- foreach(cutx = seq_len(ncuts)), .packages = c("sf", "data.table")) %dopar% {

    count <- 0

    # get the points included in a x-slice extended by `dist`, and build
    # an index over y
    min_x_comp    <- ifelse(cutx == 1, limits_x[cutx], (limits_x[cutx] - maxdist))
    max_x_comp    <- ifelse(cutx == ncuts,
                            limits_x[cutx + 1],
                            (limits_x[cutx + 1] + maxdist))
    subpts_x <- pts[x >= min_x_comp & x < max_x_comp] %>%
      setkey(y)

    for (cuty in seq_len(pts$ycut)) {

      count <- count + 1

      # subset over subpts_x to find the final set of points needed for the
      # comparisons
      min_y_comp  <- ifelse(cuty == 1,
                            limits_y[cuty],
                            (limits_y[cuty] - maxdist))
      max_y_comp  <- ifelse(cuty == ncuts,
                            limits_y[cuty + 1],
                            (limits_y[cuty + 1] + maxdist))
      subpts_comp <- subpts_x[y >= min_y_comp & y < max_y_comp]

      # subset over subpts_comp to get the points included in a x/y chunk,
      # which "neighbours" we want to find. Then buffer them.
      subpts_buf <- subpts_comp[ycut == cuty & xcut == cutx] %>%
        sf::st_as_sf() %>%
        st_buffer(maxdist)

      # retransform to sf since data.tables lost the geometric attrributes
      subpts_comp <- sf::st_as_sf(subpts_comp)

      # compute the intersection and save results in a element of "results".
      # For each point, save its "or_id" and the "or_ids" of the points within "dist"

      inters <- sf::st_intersects(subpts_buf, subpts_comp)

      # save results
      results[[count]] <- data.table(
        id = subpts_buf$or_id,
        int_ids = lapply(inters, FUN = function(x) subpts_comp$or_id[x]))

    }
    return(data.table::rbindlist(results))
  }
parallel::stopCluster(cl)
data.table::rbindlist(out)
}

Функция принимает в качестве входных данных объект точек sf, целевое расстояние и количество «разрезов», которые используются для разделения экстента на квадранты. и предоставляет на выходе кадр данных, в котором для каждой исходной точки "идентификаторы" точек в пределах maxdist сообщаются в столбце списка int_ids.

На тестовом наборе данных с различным количеством равномерно распределенных точек и двумя значениями maxdist я получил такие результаты («параллельный» прогон выполняется с использованием 6 ядер):

введите описание изображения здесь

Итак, здесь мы получаем 5-6-кратное увеличение скорости уже при «последовательной» реализации и еще 5-кратное благодаря распараллеливанию по 6 ядрам. Хотя время, показанное здесь, является лишь ориентировочным и связано с конкретным набором тестовых данных, который мы создали (на менее равномерно распределенном наборе данных я бы ожидал более низкого улучшения скорости), я думаю, что это неплохо.

HTH!

PS: более подробный анализ можно найти здесь:

https://lbusettspatialr.blogspot.it/2018/02/speeding-up-spatial-analyses-by.html

person lbusett    schedule 09.02.2018
comment
Для целей документации я подумал, что этот комментарий из вопроса SO в верхней части вашего ответа кажется актуальным: избегайте построчных операций, если шаг включает двоичные логические предикаты (например, st_intersects, st_crosses и т. Д.), Потому что вы теряете эффективность пространственного индексирования увеличение - person Tim_K; 09.02.2018

У меня есть две альтернативы: одна кажется более быстрой, а другая нет. К сожалению, более быстрый метод может не подойти для распараллеливания, поэтому он может и не помочь.

library(sf)
nc <- st_transform(st_read(system.file("shape/nc.shp", package="sf")), 32618)
# create points
pts <- st_centroid(nc)

dis <- 50000
result <- list()

Ваш подход

system.time(
for (i in 1:nrow(pts)) {
    b <- st_buffer(pts[i,], dist = dis)
    result[[i]] <- st_intersects(b, nc)[[1]]
}
)

Более медленная альтернатива

system.time(
for (i in 1:nrow(pts)) {
    b <- as.vector(st_distance(pts[i,], pts))
    result[[i]] <- which(b <= dis)
}
)

Для небольших наборов данных без зацикливания:

x <- st_distance(pts)
res <- apply(x, 1, function(i) which(i < dis)) 

Более быстрая альтернатива (не очевидно, как делать параллельно) и, возможно, несправедливое сравнение, поскольку мы не делаем цикл сами

library(spdep)
pts2 <- st_coordinates(pts)
system.time(x <- dnearneigh(pts2, 0, dis))

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

person Robert Hijmans    schedule 07.02.2018
comment
Основываясь на вашем ответе, я смог найти это сообщение в блоге, в котором обсуждается эта же тема: cran.r-project.org/web/packages/spdep/vignettes/nb_sf.html Тот же метод, что и выше, можно применять, оставаясь в sf , например, x ‹- dnearneigh (st_coordinate (pts), 0, dis) - person Tim_K; 07.02.2018

Отработав ответ Роберта, в этом конкретном примере немного быстрее извлечь координаты с помощью sf :: st_coordinates.

library(sf)
library(spdep)
nc <- st_transform(st_read(system.file("shape/nc.shp", package="sf")), 32618)
# create points
pts <- st_centroid(nc)

dis <- 50000

# quickest solution:
x <- spdep::dnearneigh(sf::st_coordinates(pts), 0, dis)

микробенчмаркинг:

my_method <- function(pts) {
  result <- list()
  for (i in 1:nrow(pts)) {
    b <- st_buffer(pts[i,], dist = dis)
    result[[i]] <- st_intersects(b, nc)[[1]]
  }
  result
}

library(microbenchmark)

microbenchmark(
  my_method(pts),
  dnearneigh(as(pts, 'Spatial'), 0, dis),
  dnearneigh(st_coordinates(pts), 0, dis)
)

Unit: microseconds
                                    expr        min          lq        mean      median          uq        max neval
                          my_method(pts) 422807.146 427434.3450 435974.4320 429862.8705 434968.3975 596832.271   100
  dnearneigh(as(pts, "Spatial"), 0, dis)   3727.221   3939.8540   4155.3094   4112.8200   4221.9525   7592.739   100
 dnearneigh(st_coordinates(pts), 0, dis)    394.323    409.5275    447.1614    430.4285    484.0335    611.970   100

проверка эквивалентности:

x <-  dnearneigh(as(pts, 'Spatial'), 0, dis)
y <- dnearneigh(st_coordinates(pts), 0, dis)

all.equal(x,y, check.attributes = F)
[1] TRUE
person Tim_K    schedule 07.02.2018
comment
as(pts, 'Spatial') преобразовать объект sf в объект Spatial*, как определено в sp. Это не часть spdep. dnearneigh принимает как пространственный объект, так и матрицу координат. Извлечение координат происходит быстрее, но оба подхода быстрые, и вам нужно сделать это только один раз для всего набора данных, поэтому разница не должна быть такой важной. (он должен масштабироваться более или менее линейно --- тогда как вычисления расстояния нет) - person Robert Hijmans; 07.02.2018
comment
Вы совершенно правы. Я изменил язык своего ответа, чтобы решить эту проблему. Мой пример выше очень специфичен для этого варианта использования и не обязательно применим в целом. - person Tim_K; 08.02.2018