Fast minimum distance (interval) between elements of 2 logical vectors (take 2)

performance, r, vector

Solution

flodel <- function(x, y) {
  xw <- which(x)
  yw <- which(y)
  i <- findInterval(xw, yw, all.inside = TRUE)
  pmin(abs(xw - yw[i]), abs(xw - yw[i+1L]), na.rm = TRUE)
}

GG1 <- function(x, y) {
  require(zoo)
  yy <- ifelse(y, TRUE, NA) * seq_along(y)
  fwd <- na.locf(yy, fromLast = FALSE)[x]
  bck <- na.locf(yy, fromLast = TRUE)[x]
  wx <- which(x)
  pmin(wx - fwd, bck - wx, na.rm = TRUE)
}

GG2 <- function(x, y) {
  require(data.table)
  dtx <- data.table(x = which(x))
  dty <- data.table(y = which(y), key = "y")
  dty[dtx, abs(x - y), roll = "nearest"] 
}

Sample data:

x <- y <- rep(FALSE, 25)
x[c(3, 8, 10, 15, 17, 25)] <- TRUE
y[c(1, 5, 9, 10, 14, 17, 18, 20)] <- TRUE

X <- rep(x, 100)
Y <- rep(y, 100)

Unit test:

identical(flodel(X, Y), GG1(X, Y))
# [1] TRUE

Benchmarks:

library(microbenchmark)
microbenchmark(flodel(X,Y), GG1(X,Y), GG2(X,Y))
# Unit: microseconds
#          expr       min         lq     median        uq        max neval
#  flodel(X, Y)   115.546   131.8085   168.2705   189.069   1980.316   100
#     GG1(X, Y)  2568.045  2828.4155  3009.2920  3376.742  63870.137   100
#     GG2(X, Y) 22210.708 22977.7340 24695.7225 28249.410 172074.881   100

[Edit by Matt Dowle] 24695 microseconds = 0.024 seconds. Inferences made on microbenchmarks with tiny data rarely hold on meaningful data sizes.

[Edit by flodel] My vectors had length 2500 which was rather meaningful given Tyler's statement (10k), but fine, let's try with vectors of length 2.5e7. I hope you will forgive me for using `system.time` given the circumstances:

X <- rep(x, 1e6)
Y <- rep(y, 1e6)
system.time(flodel(X,Y))
#    user  system elapsed 
#   0.694   0.205   0.899 
system.time(GG1(X,Y))
#    user  system elapsed 
#  31.250  16.496 112.967 
system.time(GG2(X,Y))
# Error in `[.data.table`(dty, dtx, abs(x - y), roll = "nearest") : 
#   negative length vectors are not allowed

[Edit from Arun] - Benchmark for 2.5e7 using 1.8.11: [Edit 2 from Arun] - Updating timings after Matt's recent faster binary search/merge

require(data.table)
arun <- function(x, y) {
    dtx <- data.table(x=which(x))
    setattr(dtx, 'sorted', 'x')
    dty <- data.table(y=which(y))
    setattr(dty, 'sorted', 'y')
    dty[, y1 := y]
    dty[dtx, roll="nearest"][, abs(y-y1)]
}

# minimum of three consecutive runs
system.time(ans1 <- arun(X,Y))
#   user  system elapsed 
#  1.036   0.138   1.192 

# minimum of three consecutive runs
system.time(ans2 <- flodel(X,Y))
#   user  system elapsed 
#  0.983   0.197   1.221 

identical(ans1, ans2) # [1] TRUE

Problem

I asked a related question here but realized I was burning too much time calculating this complex measure (And the goal is to use with a randomization test so speed is an issue). So I've decided to throw out the weighting and just use the minimum distance between two measures. So here I have 2 vectors (in a data frame for demo purposes but in reality they are two vectors. ``` x y 1 FALSE TRUE 2 FALSE FALSE 3 TRUE FALSE 4 FALSE FALSE 5 FALSE TRUE 6 FALSE FALSE 7 FALSE FALSE 8 TRUE FALSE 9 FALSE TRUE 10 TRUE TRUE 11 FALSE FALSE 12 FALSE FALSE 13 FALSE FALSE 14 FALSE TRUE 15 TRUE FALSE 16 FALSE FALSE 17 TRUE TRUE 18 FALSE TRUE 19 FALSE FALSE 20 FALSE TRUE 21 FALSE FALSE 22 FALSE FALSE 23 FALSE FALSE 24 FALSE FALSE 25 TRUE FALSE ``` Here I have some code worked out to find the minimal distance but I need more speed (removal of unnecessary calls and better vectorization). Maybe I can't go any faster in base R. ``` ## MWE EXAMPLE: THE DATA x <- y <- rep(FALSE, 25) x[c(3, 8, 10, 15, 17, 25)] <- TRUE y[c(1, 5, 9, 10, 14, 17, 18, 20)] <- TRUE ## Code to Find Distances xw <- which(x) yw <- which(y) min_dist <- function(xw, yw) { unlist(lapply(xw, function(x) { min(abs(x - yw)) })) } min_dist(xw, yw) ``` Is there any way to improve performance in base R? Using `dplyr` or `data.table`? My vectors are much longer (10,000 + elements). Edit per flodel's benching. flodel there's an issue I had anticipated in my MWE and I'm not sure how to fix it either. The problem arises if any x position is less than the minimum y position. ``` x <- y <- rep(FALSE, 25) x[c(3, 8, 9, 15, 17, 25)] <- TRUE y[c(5, 9, 10, 13, 15, 17, 19)] <- TRUE xw <- which(x) yw <- which(y) flodel <- function(xw, yw) { i <- findInterval(xw, yw) pmin(xw - yw[i], yw[i+1L] - xw, na.rm = TRUE) } flodel(xw, yw) ## [1] -2 -1 -6 -2 -2 20 ## Warning message: ## In xw - yw[i] : ## longer object length is not a multiple of shorter object length ```

Original source