在特定数值的连续运行中创建计数器

18

我有一个每小时的值。我想计算从上次不为零以来连续多少小时该值为零。这对于电子表格或循环而言很容易,但我希望能够用一行快速的向量化代码来完成任务。

x <- c(1, 0, 1, 0, 0, 0, 1, 1, 0, 0)
df <- data.frame(x, zcount = NA)

df$zcount[1] <- ifelse(df$x[1] == 0, 1, 0)
for(i in 2:nrow(df)) 
  df$zcount[i] <- ifelse(df$x[i] == 0, df$zcount[i - 1] + 1, 0)

期望的输出:

R> df
   x zcount
1  1      0
2  0      1
3  1      0
4  0      1
5  0      2
6  0      3
7  1      0
8  1      0
9  0      1
10 0      2
6个回答

25

如果你想查找与运行长度有关的所有内容,可以查看William Dunlap在R-help上的帖子。 他从此帖子中的f7是相关的。

f7 <- function(x){ tmp<-cumsum(x);tmp-cummax((!x)*tmp)}

在当前的情况下,执行f7(!x)。就性能而言,存在

> x <- sample(0:1, 1000000, TRUE)
> system.time(res7 <- f7(!x))
   user  system elapsed 
  0.076   0.000   0.077 
> system.time(res0 <- cumul_zeros(x))
   user  system elapsed 
  0.345   0.003   0.349 
> identical(res7, res0)
[1] TRUE

23
这里有一种基于Joshua的rle方法的方法:(根据Marek的建议,已编辑为使用seq_len和lapply)
> (!x) * unlist(lapply(rle(x)$lengths, seq_len))
 [1] 0 1 0 1 2 3 0 0 1 2

更新。仅供娱乐,这里有另外一种方法可以做到同样的效果,速度要快5倍左右:

cumul_zeros <- function(x)  {
  x <- !x
  rl <- rle(x)
  len <- rl$lengths
  v <- rl$values
  cumLen <- cumsum(len)
  z <- x
  # replace the 0 at the end of each zero-block in z by the 
  # negative of the length of the preceding 1-block....
  iDrops <- c(0, diff(v)) < 0
  z[ cumLen[ iDrops ] ] <- -len[ c(iDrops[-1],FALSE) ]
  # ... to ensure that the cumsum below does the right thing.
  # We zap the cumsum with x so only the cumsums for the 1-blocks survive:
  x*cumsum(z)
}

试一个例子:

> cumul_zeros(c(1,1,1,0,0,0,0,0,1,1,1,0,0,1,1))
 [1] 0 0 0 1 2 3 4 5 0 0 0 1 2 0 0

现在比较一个长度为一百万的向量的时间:
> x <- sample(0:1, 1000000,T)
> system.time( z <- cumul_zeros(x))
   user  system elapsed 
   0.15    0.00    0.14 
> system.time( z <- (!x) * unlist( lapply( rle(x)$lengths, seq_len)))
   user  system elapsed 
   0.75    0.00    0.75 

故事的寓意是:一行代码更加友好易懂,但并不总是最快的!

2
+1 个精彩的一行代码。小型代码分析:(!x) * unlist(lapply(rle(x)$lengths, seq_len))lapply 更安全、更快,seq_lenseq 的简化版本),大约快了 2 倍。 - Marek
谢谢@Marek。有几件事情对我来说是新的:seq_len更快,很好知道;为什么lapply更安全?此外,rle并不特别快;我有一种烦人的感觉,可以使用纯算术运算而无需分解数组和重新组装等方式更快地完成这项工作(例如,涉及cumsum的某些内容)。 - Prasad Chalasani
1
lapply总是返回列表,而sapply有时不会,例如尝试使用 x <- c(0,0,1,1,0,0,1,1)进行代码测试。除此之外,在这里使用基于lapply的函数已经足够了,所以为什么要使用其他函数呢? - Marek
2
vapplysapply 的更安全的版本,因为你可以告诉它输出类型应该是什么。 - hadley

6
< p > rle 将“计算从上次不为零以来连续为零的小时数”,但不是您所期望的输出格式。

请注意,对应值为零的元素长度:

rle(x)
# Run Length Encoding
#   lengths: int [1:6] 1 1 1 3 2 2
#   values : num [1:6] 1 0 1 0 1 0

1
方便,但我无法从rle中获得所需的内容,除非采取相当不端的行为。 - J. Win.

5

一个简单的base R方法:

ave(!x, cumsum(x), FUN = cumsum)

#[1] 0 1 0 1 2 3 0 0 1 2

3

一行代码,不是非常优雅:

x <- c(1, 0, 1, 0, 0, 0, 1, 1, 0, 0) 

 unlist(lapply(split(x, c(0, cumsum(abs(diff(!x == 0))))), function(x) (x[1] == 0) * seq(length(x))))

1
使用purr::accumulate()非常简单,因此这个tidyverse解决方案可能会在这里增加一些价值。我必须承认它肯定不是最快的,因为它调用了相同的函数length(x)多次。
library(purrr)

accumulate(x==0, ~ifelse(.y!=0, .x+1, 0))

 [1] 0 1 0 1 2 3 0 0 1 2

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接