为连续序列创建分组变量并拆分向量

21

我有一个向量,比如 c(1, 3, 4, 5, 9, 10, 17, 29, 30),我想将形成连续序列的“相邻”元素分组在一起,也就是增加1,在不规则向量中,结果为:

L1: 1
L2: 3,4,5
L3: 9,10
L4: 17
L5: 29,30

天真的代码(来自一位前C程序员):

partition.neighbors <- function(v)
{
    result <<- list() #jagged array
    currentList <<- v[1] #current series

    for(i in 2:length(v))
    {
        if(v[i] - v [i-1] == 1)
        {
            currentList <<- c(currentList, v[i])
        }
        else
        {
            result <<- c(result, list(currentList))
            currentList <<- v[i] #next series
        }       
    }

    return(result)  
}

现在我明白了:

a)虽然花括号相似,但是 R 不同于 C;
b)全局变量是纯恶魔;
c)那是一种可怕的低效方法来实现结果。

因此,欢迎任何更好的解决方案。

5个回答

24

运用了一些 R 语言的惯用表达:

> split(v, cumsum(c(1, diff(v) != 1)))
$`1`
[1] 1

$`2`
[1] 3 4 5

$`3`
[1]  9 10

$`4`
[1] 17

$`5`
[1] 29 30

12

daroczig写道:“你可以基于diff编写更加简洁的代码”...

以下是一种方法:

split(v, cumsum(diff(c(-Inf, v)) != 1))

编辑(添加时间):

Tommy 发现通过对类型进行仔细处理可以使其更快;它变得更快的原因是 split 在整数上更快,而在因子上实际上仍然更快。

这里是 Joshua 的解决方案;cumsum 的结果是一个数字,因为它正在与 1 进行 c 操作,所以它是最慢的。

system.time({
a <- cumsum(c(1, diff(v) != 1))
split(v, a)
})
#   user  system elapsed 
#  1.839   0.004   1.848 

仅使用1L将结果转换为整数会大大加快运算速度。

system.time({
a <- cumsum(c(1L, diff(v) != 1))
split(v, a)
})
#   user  system elapsed 
#  0.744   0.000   0.746 

这是Tommy的解决方案,供参考;它也可以按整数进行分割。

> system.time({
a <- cumsum(c(TRUE, diff(v) != 1L))
split(v, a)
})
#   user  system elapsed 
#  0.742   0.000   0.746 

这是我的原始解决方案,它也在一个整数上进行了拆分。

system.time({
a <- cumsum(diff(c(-Inf, v)) != 1)
split(v, a)
})
#   user  system elapsed 
#  0.750   0.000   0.754 

这是Joshua的代码,结果在split之前转换为整数。

system.time({
a <- cumsum(c(1, diff(v) != 1))
a <- as.integer(a)
split(v, a)
})
#   user  system elapsed 
#  0.736   0.002   0.740 

对于在整数向量上进行的split操作,所有版本大致相同;如果该整数向量已经是一个因子,它甚至可以更快,因为从整数到因子的转换实际上需要约一半的时间。在这里,我直接将其转换成了一个因子;总的来说这并不被推荐,因为它取决于因子类的结构。这里只是为了比较而已。

system.time({
a <- cumsum(c(1L, diff(v) != 1))
a <- structure(a, class = "factor", levels = 1L:a[length(a)])
split(v,a)
})
#   user  system elapsed 
#  0.356   0.000   0.357 

是的,这是一种更整洁的方式! :) 我不知道 split,感谢您指出这个有用的函数。 - daroczig
我应该指出,当使用 as.integer 时要小心,因为它返回截断的值,当数字是通过浮点运算创建时,这可能不是您想要的结果。例如,as.integer(0.3*3+0.1) 返回 0 - Aaron left Stack Overflow
你能解释一下 diff() 函数是做什么的以及它是如何工作的吗?官方文档并没有帮助我理解它。 - OnlyDean
它只是计算术语之间的差异。帮助可能会让人感到困惑,因为它比这更一般化,允许不同的滞后,并且可以重复该过程,进行双重差异(差异的差异)等操作。 - Aaron left Stack Overflow

8

乔舒亚和艾伦是正确的。但是,通过仔细使用正确的类型,整数和逻辑运算,他们的代码仍然可以快两倍以上:

split(v, cumsum(c(TRUE, diff(v) != 1L)))

v <- rep(c(1:5, 19), len = 1e6) # Huge vector...
system.time( split(v, cumsum(c(1, diff(v) != 1))) ) # Joshua's code
# user  system elapsed 
#   2.64    0.00    2.64 

system.time( split(v, cumsum(c(TRUE, diff(v) != 1L))) ) # Modified code
# user  system elapsed 
# 1.09    0.00    1.12 

哇!我真没想到它会有这么大的影响。 - Aaron left Stack Overflow
Tommy,我找到了为什么速度更快的原因,并编辑了你的帖子以添加它。我不确定这是否是适当的礼仪; 希望你不介意。(此外,它必须经过同行评审,所以如果您没有立即看到它,那就是原因。) - Aaron left Stack Overflow
显然我的编辑被拒绝了;我已经在我的回答中添加了时间。 - Aaron left Stack Overflow

4
您可以轻松定义切点:
which(diff(v) != 1)

基于此尝试:

v <- c(1,3,4,5,9,10,17,29,30)
cutpoints <- c(0, which(diff(v) != 1), length(v))
ragged.vector <- vector("list", length(cutpoints)-1)
for (i in 2:length(cutpoints)) ragged.vector[[i-1]] <- v[(cutpoints[i-1]+1):cutpoints[i]]

这导致:
> ragged.vector
[[1]]
[1] 1

[[2]]
[1] 3 4 5

[[3]]
[1]  9 10

[[4]]
[1] 17

[[5]]
[1] 29 30

这个算法并不好,但你可以基于 diff 编写更简洁的代码 :) 祝你好运!


4

您可以创建一个data.frame,并使用diffifelsecumsum将元素分配到组中,然后使用tapply进行聚合:

v.df <- data.frame(v = v)
v.df$group <- cumsum(ifelse(c(1, diff(v) - 1), 1, 0))
tapply(v.df$v, v.df$group, function(x) x)

$`1`
[1] 1

$`2`
[1] 3 4 5

$`3`
[1]  9 10

$`4`
[1] 17

$`5`
[1] 29 30

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