【问题标题】:More elegant way to return a sequence of numbers based on booleans?基于布尔值返回数字序列的更优雅的方式?
【发布时间】:2026-02-21 07:10:01
【问题描述】:

这是我作为 data.frame 一部分的布尔值示例:

atest <- c(FALSE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, FALSE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, FALSE)

我想从每个 FALSE 返回一个从 1 开始并增加 1 直到下一个 FALSE 的数字序列。

得到的所需向量是:

[1]  1  2  3  4  5  6  7  8  9 10  1  2  3  4  5  6  7  8  9 10  1

这是完成此操作的代码,但我确信在 R 中有更简单或更优雅的方法来执行此操作。我一直在尝试学习如何在 R 中更有效地编写代码,而不是简单地完成工作.

result <- c()
x <- 1
for(i in 1:length(atest)){
    if(atest[i] == FALSE){
        result[i] <- 1
        x <- 1
    } 
    if(atest[i] != FALSE){
        x <- x+1
         result[i] <- x
    }
}

【问题讨论】:

  • 在 for 循环中重新分配(“增长”)一个对象在 R 中是一个很大的禁忌。这是你能做的最慢的事情。
  • 我知道我尝试过使用 sapply,但只是想了解基本逻辑。您的解决方案正是我想要的。

标签: r


【解决方案1】:

这是一种方法,使用方便(但不是广为人知/使用的)基本函数:

> sequence(tabulate(cumsum(!atest)))
 [1]  1  2  3  4  5  6  7  8  9 10  1  2  3  4  5  6  7  8  9 10  1

分解:

> # return/repeat integer for each FALSE
> cumsum(!atest)
 [1] 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3
> # count the number of occurrences of each integer
> tabulate(cumsum(!atest))
[1] 10 10  1
> # create concatenated seq_len for each integer
> sequence(tabulate(cumsum(!atest)))
 [1]  1  2  3  4  5  6  7  8  9 10  1  2  3  4  5  6  7  8  9 10  1

【讨论】:

  • 我已经 +1 了,但我会再做一次,因为解释真的很有帮助!
  • @Joshua Ulrich +1 为这个伟大的解决方案;但如果第一个元素不是 FALSE: sequence(tabulate(cumsum(!atest[-1])))
  • @sgibb:在我回答之前我没有尝试过 OP 的代码,但是如果第一个元素不是 FALSE,我看到它从 2 开始第一个序列。这似乎与他们的文字不一致,“我想从每个 FALSE 返回一个从 1 开始并增加 1 直到下一个 FALSE 的数字序列。”
  • 这太棒了。我的数据总是以 FALSE 开头。我从来没有使用过表格或序列,只有 seq。非常感谢!
【解决方案2】:

这是使用其他熟悉功能的另一种方法:

seq_along(atest) - cummax(seq_along(atest) * !atest) + 1L

因为它都是矢量化的,它明显比@Joshua 的解决方案快(如果速度有任何问题的话):

f0 <- function(x) sequence(tabulate(cumsum(!x)))
f1 <- function(x) {i <- seq_along(x); i - cummax(i * !x) + 1L}
x  <- rep(atest, 10000)

library(microbenchmark)
microbenchmark(f0(x), f1(x))
# Unit: milliseconds
#   expr       min        lq    median        uq      max neval
#  f0(x) 19.386581 21.853194 24.511783 26.703705 57.20482   100
#  f1(x)  3.518581  3.976605  5.962534  7.763618 35.95388   100

identical(f0(x), f1(x))
# [1] TRUE

【讨论】:

    【解决方案3】:

    Rcpp 可以很好地解决此类问题。借用@flodel 的代码作为基准测试框架,

    boolseq.cpp
    -----------
    
    #include <Rcpp.h>
    using namespace Rcpp;
    
    // [[Rcpp::export]]
    IntegerVector boolSeq(LogicalVector x) {
      int n = x.length();
      IntegerVector output = no_init(n);
      int counter = 1;
      for (int i=0; i < n; ++i) {
        if (!x[i]) {
          counter = 1;
        }
        output[i] = counter;
        ++counter;
      }
      return output;
    }
    
    /*** R
    x <- c(FALSE, sample( c(FALSE, TRUE), 1E5, TRUE ))
    
    f0 <- function(x) sequence(tabulate(cumsum(!x)))
    f1 <- function(x) {i <- seq_along(x); i - cummax(i * !x) + 1L}
    
    library(microbenchmark)
    microbenchmark(f0(x), f1(x), boolSeq(x), times=100)
    
    stopifnot(identical(f0(x), f1(x)))
    stopifnot(identical(f1(x), boolSeq(x)))
    */
    

    sourceCpping 它给了我:

    Unit: microseconds
           expr       min        lq     median         uq       max neval
          f0(x) 18174.348 22163.383 24109.5820 29668.1150 78144.411   100
          f1(x)  1498.871  1603.552  2251.3610  2392.1670  2682.078   100
     boolSeq(x)   388.288   426.034   518.2875   571.4235   699.710   100
    

    不太优雅,但与您使用 R 代码编写的内容非常接近。

    【讨论】: