OP has disclosed 他的生产数据集包含 8100 万行。不幸的是,r2evans' benchmark 仅使用了问题提供的 4 行样本数据集,而忽略了henrik's suggestion。为了找到像 OP 这样的大型数据集的最佳方法,我发现运行具有变化和实际问题大小的基准测试是值得的,以便测量 运行时间 以及 内存消耗。
运行时间和内存消耗可能取决于
- 值的数量,
- 间隔数,
- 以及落入每个区间的值的数量。
第 1 项和第 2 项链接为值的数量,区间数由数据集中的行数给出。因此,我们可以改变两个大小参数,行数n 和每个区间内的数值m。为了获得可重现和可预测的基准数据,基准数据集由
d <- data.table(value = as.double(seq(n)))[, min_val := value - m][, max_val := value + m][, count := -1L]
在n <- 4和m <- 1的情况下d变为
value min_val max_val count
<num> <num> <num> <int>
1: 1 0 2 -1
2: 2 1 3 -1
3: 3 2 4 -1
4: 4 3 5 -1
为了为每个基准测试运行创建相同的条件,count 列预先分配了一些虚拟数据。
基准包括
编辑:第三组基准运行比较
很遗憾,TarJae's answer 对我不起作用。
library(data.table)
library(bench)
library(ggplot2)
options(datatable.print.class = TRUE)
bm1 <- press(
n = 2^c(2, 10, 13),
m = 2^c(0, 9, 12),
{
d <- data.table(value = as.double(seq(n)))[, min_val := value - m][, max_val := value + m][, count := -1L]
mark(
henrik = {
d[ , count := d[d, on = .(value > min_val, value < max_val), .N, by = .EACHI]$N]
},
r2evans0 = {
d[, count := rowSums(outer(seq_len(.N), value, function(i, v) {min_val[i] < v & v < max_val[i];}))]
},
r2evans1 = {
d[, count := mapply(function(mi,ma) sum(mi < value & value < ma), min_val, max_val)]
},
r2evans2 = {
d[, count := rowSums(outer(min_val, d$value, `<`) &
outer(max_val, d$value, `>`)),
by = .(g = seq_len(nrow(d)) %/% 100)]
},
Thomas = {
d[, count := colSums(outer(value, min_val, ">") & outer(value, max_val, "<"))]
},
Alexandre = {
d[, count := lapply(
# seq.int(1, nrow(d)),
seq_len(nrow(d)),
function(i) sum(d[, value] > d[i, min_val] & d[, value] < d[i, max_val])
)]
},
min_iterations = 3
)
}
)
autoplot(bm1)
请注意对数时间刻度。
图表表明
- Alexandre 的方法比任何其他解决方案都要慢一个数量级(并且可能会在以后的运行中省略),
- 随着行数的增加
n henrik 的方法与r2evans1 并驾齐驱(值得进一步研究),
- 每个区间
m 中的值数量似乎对运行时间没有影响或影响很小。
可以通过更改构面并在一个构面中绘制不同m 的中值时间来验证后者:
ggplot(bm1) +
aes(x = median, y = expression, color = as.factor(m)) +
geom_point() +
facet_wrap(vars(n))
在下面的下一个图表中,绘制 mem_alloc 而不是 median 次表明
-
m 对内存分配没有影响(有一个例外),
- 对于大的
n,henrik 的方法需要的内存比任何其他方法都少:
请注意对数刻度。
第二组基准测试
基于之前的结果,下一组基准运行仅改变大小参数n,而m 保持不变。 Alexandre 的方法因为太慢而被省略。
n 从 2^10 (1024) 变为 2^14 (16384) 和 m = 1.0。不幸的是,由于我的电脑内存不足,n = 2^15 的运行被中止。
autoplot(bm2)
henrik 的方法在 2^14 (16384) 行案例的速度方面处于领先地位。
为了确定这是否表明趋势,运行时间与问题大小n 被绘制为
ggplot(bm2) +
aes(x = n, y = median, color = expression, group = attr(expression, "description"),
label = ifelse(n == max(n), attr(expression, "description"), "")) +
geom_point() +
geom_smooth(method = "lm", se = FALSE) +
scale_x_continuous(trans = scales::log2_trans(), expand = expansion(mult = c(0.05, 0.1))) +
ggrepel::geom_label_repel(direction = "y", nudge_x = 1000)
henrik 的方法似乎有很高的开销,但随着问题规模的增加,获得了速度优势。
同样在内存分配方面,henrik 的方法在非等自连接中聚合似乎比其他方法需要的内存要少得多。更重要的是,内存分配随问题大小的增加不那么陡峭,这表明当可用计算机内存是一个限制因素时,这种方法可以处理更大的问题。
编辑:第三组基准测试
这组基准运行比较了henrik's非等自连接中的聚合与chinsoon12's新的Rcpp解决方案。
由于这两种方法的内存占用要小得多,问题大小可以增加到2^18 (262144) 行,然后在我的 Windows PC 上达到 16 GB 内存限制。
library(Rcpp)
bm4 <- press(
n = 2^(10:18),
{
m <- 1.
d <- data.table(value = as.double(seq(n)))[, min_val := value - m][, max_val := value + m][, count := -1L]
mark(
henrik = {
d[ , count := d[d, on = .(value > min_val, value < max_val), .N, by = .EACHI]$N]
},
chinsoon = {
cppFunction("IntegerVector inrange(NumericVector v, NumericVector minv, NumericVector maxv) {
int n = v.size();
IntegerVector res(n);
for (int r=0; r<n; r++) {
for (int i=0; i<n; i++) {
if (v[i] > minv[r] && v[i] < maxv[r]) {
res[r]++;
}
}
}
return res;
}")
d[, count := inrange(value, min_val, max_val)]
},
min_iterations = 3
)
}
)
接下来的两个图表分别显示了中位运行时间和内存分配与问题大小的关系。 (请注意对数刻度):
n = 2^18 (262144) 的结果:
setDT(bm4)[n == 2^18, .(expression = unique(attr(expression, "description")),
n, median, mem_alloc)]
expression n median mem_alloc
<char> <num> <bench_time> <bench_bytes>
1: henrik 262144 17.47s 12.14MB
2: chinsoon 262144 1.03m 2.06MB
显然,对于高达2^16 (65536) 的问题,chinsoon 的方法更快,而 henrik 的方法对于更大的问题规模更快(并且似乎具有更线性的时间行为)。对于n = 2^18 的问题规模,henrik 的方法几乎是 chinsoon 的 4 倍。
另一方面,henrik 的方法分配的内存比 chinsoon 的要多得多。对于问题大小n = 2^18,henrik 的方法分配的内存是 chinsoon 的 6 倍。显然,随着问题规模的增加,这个比率是恒定的。
因此,速度(henrik 的方法)和内存需求(chinsoon 的方法)之间存在权衡,具体取决于问题的大小。您的里程可能会有所不同。