As a supplement to Scott's answer, his solution can be written more concisely as
dt[.(1:5), on = .(Dist < V1), allow = TRUE, nomatch = 0][
, .(mean_Dist_by_threshold = mean(V1)), by = .(threshold = Dist)]
Here, .(1:5) creates thresholds on the fly and the data.table expressions are chained.
Alternatively, the aggregation can be done during the join using by = .EACHI:
dt[.(1:5), on = .(Dist < V1), nomatch = 0,
.(mean_Dist_by_threshold = mean(V1)), by = .EACHI][
, setnames(.SD, "Dist", "threshold")]
The call to setnames() is just for convenience to return the same result as Scott's answer.
Benchmark code
library(data.table)
# create data
nr <- 5e2L
set.seed(123L) # to make the data reproducible
dt <-
data.table(
id = factor(rep(1:10, nr / 10)),
V1 = rnorm(nr, 5, 5),
Dist = sample(1:5, nr, replace = T)
)
str(dt)
microbenchmark::microbenchmark(
scott = {
thresholds <- data.table(dist_threshold=1:5)
passes_threshold <-
dt[thresholds, on = .(Dist < dist_threshold), # non-equi join
allow.cartesian = TRUE, # Fixes error, see details in ?data.table
nomatch = 0 # Do not include thresholds which no row satisfies (i.e. Dist < 1)
]
passes_threshold[, .(mean_Dist_by_threshold = mean(V1)), by = .(threshold = Dist)]
},
uwe1 = {
dt[.(1:5), on = .(Dist < V1), allow = TRUE, nomatch = 0][
, .(mean_Dist_by_threshold = mean(V1)), by = .(threshold = Dist)]
},
uwe2 = {
dt[.(1:5), on = .(Dist < V1), nomatch = 0,
.(mean_Dist_by_threshold = mean(V1)), by = .EACHI][
, setnames(.SD, "Dist", "threshold")]
},
times = 100L
)
Benchmark results
With 500 rows, there are only slight differences between the 3 variants, with the chaining slightly ahead of Scott's and by = .EACHI behind.
Unit: milliseconds
expr min lq mean median uq max neval cld
scott 1.460058 1.506854 1.618048 1.526019 1.726257 4.768493 100 a
uwe1 1.302760 1.327686 1.487237 1.338926 1.372498 12.733933 100 a
uwe2 1.827756 1.864777 1.944920 1.888349 2.020097 2.233269 100 b
With 50000 rows, chaining is still slightly ahead of Scott's but by = .EACHI has outperformed the others.
Unit: milliseconds
expr min lq mean median uq max neval cld
scott 3.692545 3.811466 4.016152 3.826423 3.853489 10.336598 100 b
uwe1 3.560786 3.632999 3.936583 3.642526 3.657992 13.579008 100 b
uwe2 2.503508 2.545722 2.577735 2.566869 2.602586 2.798692 100 a
With 5 M rows, this becomes much more evident:
Unit: milliseconds
expr min lq mean median uq max neval cld
scott 641.9945 675.3749 743.0761 708.7552 793.6170 878.4787 3 b
uwe1 587.1724 587.5557 589.1360 587.9391 590.1178 592.2965 3 b
uwe2 130.9358 134.6688 157.1860 138.4019 170.3110 202.2202 3 a
One explanation of the speed difference might be the shear size of the intermediate result passes_threshold of more than 10 M rows (this is why allow.cartesian = TRUE is required).