【问题标题】:Count number of rows within certain range计算一定范围内的行数
【发布时间】:2021-10-27 16:07:30
【问题描述】:

我有一个包含一些值('value')、下限('min_val')和上限('max_val')的 data.table:

   | value | min_val | max_val |
1: | 94.001 | 94.00 | 94.02 |
2: | 94.002 | 94.00 | 94.03 |
3: | 94.003 | 94.01 | 94.04 |
4: | 95 | 94.98 | 95.02 |

我想为整个 dt 中的值计算每一行的 value > min_val & value

   | value | min_val | max_val | count |
1: | 94.001 | 94.00 | 94.02 |  1       |   #(num of value(s) > 94.00 &  < 94.02)
2: | 94.002 | 94.00 | 94.03 |  2       |
3: | 94.003 | 94.01 | 94.04 |  2       |
4: | 95     | 94.98 | 95.02 |  1       |

我试过了 dt[, count := nrow(dt[value &gt; dt[min_val] &amp; value &lt; dt[max_val]])] 但我走错了路。

【问题讨论】:

  • 我认为他的意思是计算同一个表中有多少行具有给定范围内的值。我也觉得第二张表有错误,第一行的count应该是3,第三行的count应该是0。
  • 我现在看到了.....
  • d[ , N := d[d, on = .(value &gt; min_val, value &lt; max_val), .N, by = .EACHI]$N]
  • @Henrik 您应该考虑将其作为解决方案
  • 试试上面给出的代码

标签: r dataframe performance data.table range


【解决方案1】:

OP has disclosed 他的生产数据集包含 8100 万行。不幸的是,r2evans' benchmark 仅使用了问题提供的 4 行样本数据集,而忽略了henrik's suggestion。为了找到像 OP 这样的大型数据集的最佳方法,我发现运行具有变化和实际问题大小的基准测试是值得的,以便测量 运行时间 以及 内存消耗。

运行时间和内存消耗可能取决于

  1. 值的数量,
  2. 间隔数,
  3. 以及落入每个区间的值的数量。

第 1 项和第 2 项链接为值的数量,区间数由数据集中的行数给出。因此,我们可以改变两个大小参数,行数n 和每个区间内的数值m。为了获得可重现和可预测的基准数据,基准数据集由

d <- data.table(value = as.double(seq(n)))[, min_val := value - m][, max_val := value + m][, count := -1L]

在n &lt;- 4和m &lt;- 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 的方法)之间存在权衡,具体取决于问题的大小。您的里程可能会有所不同。

【讨论】:

  • 非常有趣的分析,Uwe!感谢您完成所有这些工作。
  • 全面的基准测试,酷!为您的出色努力点赞!
  • 谢谢,Uwe。不知道bench::press在基准测试中是否每次都编译cpp函数?
【解决方案2】:

我们可以这样试试outer

> setDT(df)[, count := colSums(outer(value, min_val, ">") & outer(value, max_val, "<"))][]
    value min_val max_val count
1: 94.001   94.00   94.02     3
2: 94.002   94.00   94.03     3
3: 94.003   94.01   94.04     0
4: 95.000   94.98   95.02     1

数据

> dput(df)
structure(list(value = c(94.001, 94.002, 94.003, 95), min_val = c(94,
94, 94.01, 94.98), max_val = c(94.02, 94.03, 94.04, 95.02)), class = "data.frame", row.names = c(NA,
-4L))

【讨论】:

  • 感谢外部提示,rep(Y, rep.int(length(X), length(Y))) 中仍然出现错误:'times' 参数无效
【解决方案3】:

编辑,有 2300 万行,您需要稍微不同的策略。原始答案保留在此下方。

对于更大的数据集,outer(单独)不是一种安全的方法。两种替代方法,这两种方法都会较慢(但如果您的内存受限,这是您必须做出的权衡):

  1. 逐行(min_val 和 max_val);这是两者中较简单的一个,同时给出了预期的结果。

    dat[, count := mapply(function(mi,ma) sum(mi < value & value < ma), min_val, max_val)]
    #    value min_val max_val count
    #    <num>   <num>   <num> <int>
    # 1: 94.01   94.00   94.02     1
    # 2: 94.02   94.00   94.03     2
    # 3: 94.03   94.01   94.04     2
    # 4: 95.00   94.98   95.02     1
    
  2. 更快(并借用 ThomasIsCoding 的方法),分组,但稍微复杂一些。一次有效地在n 行上使用outer。

    dat[, count := rowSums(outer(min_val, dat$value, `<`) &
                             outer(max_val, dat$value, `>`)),
        by = .(g = seq_len(nrow(dat)) %/% 100)]
    

    在此示例中,我们一次处理 100 行 *_val 变量,以及我们在外部保存为 ovalue 的整个 value 列。 (您可能需要四处寻找最佳值而不是 100:当您太高时您会知道的。)


提供的原始解决方案:

dat[, count := rowSums(outer(seq_len(.N), value, function(i, v) min_val[i] < v & v < max_val[i]))]
#    value min_val max_val count
#    <num>   <num>   <num> <num>
# 1: 94.01   94.00   94.02     1
# 2: 94.02   94.00   94.03     2
# 3: 94.03   94.01   94.04     2
# 4: 95.00   94.98   95.02     1

快速演练:

  • outer(.,.,.) 做一个外部产品,我们查看每个行索引 (seq_len(.N)) 与每个 value;我们需要与行索引进行比较的原因是我们需要min_val 和max_val,而这对于双参数函数来说并不容易;

  • 我们给outer 的函数被调用一次,有两个向量:

    cbind(i, v)
    #       i      v
    #  [1,] 1 94.001
    #  [2,] 2 94.001
    #  [3,] 3 94.001
    #  [4,] 4 94.001
    #  [5,] 1 94.002
    #  [6,] 2 94.002
    #  [7,] 3 94.002
    #  [8,] 4 94.002
    #  [9,] 1 94.003
    # [10,] 2 94.003
    # [11,] 3 94.003
    # [12,] 4 94.003
    # [13,] 1 95.000
    # [14,] 2 95.000
    # [15,] 3 95.000
    # [16,] 4 95.000
    

    这对我们来说很好,因为min_val[i] 产生 16 个数字,与 max_val 相同,而 v 已经是我们要比较的数字。

  • outer(.) 返回一个矩阵:

    # dat[, outer(seq_len(.N), value, function(i, v) min_val[i] < v & v < max_val[i])]
    # #       [,1]  [,2]  [,3]  [,4]
    # # [1,]  TRUE FALSE FALSE FALSE
    # # [2,]  TRUE  TRUE FALSE FALSE
    # # [3,] FALSE  TRUE  TRUE FALSE
    # # [4,] FALSE FALSE FALSE  TRUE
    

    第 1 行中的每一列代表一个 行,它与第 1 行的 min_val 和 max_val 约束相匹配。为此,我们只需要rowSums 矩阵即可获得我们的 1、2、2 和 1。

注意:根据您的 cmets 和预期输出,我相信您的 value 应该真的是 94.01 而不是 94.001 (等等)。


数据

dat <- setDT(structure(list(value = c(94.01, 94.02, 94.03, 95), min_val = c(94, 94, 94.01, 94.98), max_val = c(94.02, 94.03, 94.04, 95.02)), class = c("data.table", "data.frame"), row.names = c(NA, -4L)))

基准

(老实说,我原以为 ThomasIsCoding 的 double-outer 会受到 一些 的惩罚,但显然不是。我的猜测是我使用 i 的间接性与 double 一样几乎不明显-outer.)

bench::mark(
  r2evans = dat[, count := rowSums(outer(seq_len(.N), value, function(i, v) {min_val[i] < v & v < max_val[i];}))],
  ThomasIsCoding = dat[, count := colSums(outer(value, min_val, ">") & outer(value, max_val, "<"))],
  AlexandreLeonard = dat[, 
    count := lapply(seq.int(1, nrow(dt)), function(i) {
      sum(dt[, value] > dt[i, min_val] & dt[, value] < dt[i, max_val])
    })]
)
# # A tibble: 3 x 13
#   expression            min   median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc total_time result    memory    time   gc     
#   <bch:expr>       <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl> <int> <dbl>   <bch:tm> <list>    <list>    <list> <list> 
# 1 r2evans           319.3us  347.7us     2449.      33KB     4.24  1154     2      471ms <data.ta~ <Rprofme~ <bch:~ <tibbl~
# 2 ThomasIsCoding    301.5us 344.05us     2454.    32.6KB     2.05  1196     1      487ms <data.ta~ <Rprofme~ <bch:~ <tibbl~
# 3 AlexandreLeonard   3.77ms   4.44ms      205.   408.8KB     4.31    95     2      464ms <data.ta~ <Rprofme~ <bch:~ <tibbl~

【讨论】:

  • 该解决方案适用于小型数据集,但在我的 8100 万行 dt 上运行时,由于 rep(Y, rep.int(length(X), length(Y))) 中的错误而失败:无效'次'论点
  • 有趣的基准测试结果,点赞!
  • @iuliux,我不知道如何临时复制。也许您可以编辑您的问题并添加来自dput(head(x)) 的输出?
  • 在我的数据集的一个子集上运行 2300 万行并得到一个不同的错误:错误:无法分配大小为 4434924.0 Gb 的向量
  • @r2evans,您是否介意检查并仔细检查 my benchmark results 的大型问题?结果表明您的mapply() 方法对于大型问题的表现非常好(并且仅被henrik's approach 击败)。谢谢。
【解决方案4】:

我们可以将add_count 与between 一起使用:

library(dplyr)
df %>% 
    add_count(between(value, min_val, max_val)) %>% 
    select(1:4)
    value min_val max_val count
1: 94.001   94.00   94.02     3
2: 94.002   94.00   94.03     3
3: 94.003   94.01   94.04     0
4: 95.000   94.98   95.02     1

【讨论】:

    【解决方案5】:

    考虑到实际数据集的大小,这是另一个使用 Rcpp 的选项:

    library(Rcpp)
    cppFunction("IntegerVector betnRcpp(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;
    }")
    #same df as ThomasIsCoding answer
    setDT(df)[, count := betnRcpp(value, min_val, max_val)]
    

    【讨论】:

    • 试过了,它跑了一个周末,看不到尽头:)
    • 如果您有 8000 万行,则需要执行 6.4 X 10^15 比较。如果每次比较需要 1 毫秒,那么即使使用 64 个内核,您也需要 10^11 秒来进行配对比较
    【解决方案6】:

    借用@chinsoon12的答案,我们可以先对值和范围进行排序,所以我们可以在cpp代码中添加一些技巧来跳过超出范围值的比较:

    cppFunction("
    IntegerVector f2(NumericVector v, NumericVector minv, NumericVector maxv) {
        int n = v.size();
        IntegerVector res(n);
        int i2 = 0;
        
        for (int r=0; r<n; r++) {
            for (int i=i2; i<n; i++) {
                if (v[i] > minv[r]) {
                    if (v[i] < maxv[r]) {
                        res[r]++;
                    } else {
                      break;
                    }
                } else {
                    i2 = i;
                }
            }
        }
        return res;}")
    

    克里特假数据:

    n <- 8e7
    set.seed(1)
    x <- sample.int(n, n, replace = T)
    a <- seq(0, n - g, by = g)
    p1 <- data.table(min_val = a, max_val = a + g)
    d <- rbindlist(lapply(1:g, function(x) p1))
    d[, value := x]
    

    基准测试:

    # to use new rcpp function we need to sort values in new vector:
    v2 <- sort(d$value)
    d[, id := .I] # add id column if we want to sort the data back
    setkey(d, min_val) # sort data.table by min_value
    
    system.time(
        d[, count2 := d[d, on = .(value > min_val, value < max_val), .N, by = .EACHI]$N]
    ) # 54 seconds henrik
    system.time(
        d[, count4 := f2(v2, min_val, max_val)]
    ) # 1.4 seconds
    all.equal(d$count2, d$count4)
    # TRUE
    

    即使我们将数据排序的时间包括在内,这也应该快得多。当然,实时取决于您的确切数据。我建议首先测试您的数据样本,而不是首先测试所有 80e6 行...

    【讨论】:

      【解决方案7】:

      这不是很好,但做的工作:

      dt[, 
        count := lapply(seq.int(1, nrow(dt)), function(i) {
          sum(dt[, value] > dt[i, min_val] & dt[, value] < dt[i, max_val])
        })
      ]
      
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2013-05-29
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-09-19
        • 2021-11-03
        • 1970-01-01
        相关资源
        最近更新 更多