R数据框按连续值分组

5
我在R中没有找到这个常见分组问题的解决方案:
这是我的原始数据集。
ID  State
1   A
2   A
3   B
4   B
5   B
6   A
7   A
8   A
9   C
10  C

这应该是我的分组结果数据集。
State   min(ID) max(ID)
A       1       2
B       3       5
A       6       8
C       9       10

所以,思路是首先通过ID列(或时间戳列)对数据集进行排序。然后将所有没有间隔的连接状态分组在一起,并返回最小和最大ID值。这与rle方法有关,但这种方法不允许计算组的最小、最大值。
有什么想法吗?

2
相关帖子:https://dev59.com/FZjga4cB1Zd3GeqPDwDS - zx8754
4个回答

7

你可以尝试以下方法:

library(dplyr)
df %>%
  mutate(rleid = cumsum(State != lag(State, default = ""))) %>%
  group_by(rleid) %>%
  summarise(State = first(State), min = min(ID), max = max(ID)) %>%
  select(-rleid)

根据评论中@alistaire提到的方式,你实际上可以在group_by()内使用相同的语法进行突变操作,将前两个步骤合并。借用data.table::rleid()并使用summarise_all()来简化:

df %>% 
  group_by(State, rleid = data.table::rleid(State)) %>% 
  summarise_all(funs(min, max)) %>% 
  select(-rleid)

这将得到:

## A tibble: 4 × 3
#   State   min   max
#  <fctr> <int> <int>
#1      A     1     2
#2      B     3     5
#3      A     6     8
#4      C     9    10

你实际上可以在 group_by 中使用相同的语法进行 mutate,将前两个步骤结合起来。借用 data.table::rleid 并使用 summarise_all 进行简化:df %>% group_by(State, rleid = data.table::rleid(State)) %>% summarise_all(funs(min, max)) %>% select(-rleid) - alistaire
@alistaire 没想过在那里使用 summarise_all()。不错的建议。我已经更新了答案。 - Steven Beaupré

5
这里有一种方法,使用基础R中的rle函数来处理您提供的数据集。
# get the run length encoding
temp <- rle(df$State)

# construct the data.frame
newDF <- data.frame(State=temp$values,
                    min.ID=c(1, head(cumsum(temp$lengths) + 1, -1)),
                    max.ID=cumsum(temp$lengths))

该函数返回

newDF
  State min.ID max.ID
1     A      1      2
2     B      3      5
3     A      6      8
4     C      9     10

请注意,rle需要的是字符向量而不是因子向量,因此我在下面使用了as.is参数。
正如@cryo111在下面的评论中指出的那样,数据集可能是无序的时间戳,这些时间戳与rle计算的长度不对应。为使该方法有效,您需要先使用类似as.POSIXct的函数将时间戳转换为日期时间格式,使用df <- df[order(df$ID),]进行排序,然后采用上述方法进行轻微改动。
# get the run length encoding
temp <- rle(df$State)

# construct the data.frame
newDF <- data.frame(State=temp$values,
                    min.ID=df$ID[c(1, head(cumsum(temp$lengths) + 1, -1))],
                    max.ID=df$ID[cumsum(temp$lengths)])

数据

df <- read.table(header=TRUE, as.is=TRUE, text="ID  State
1   A
2   A
3   B
4   B
5   B
6   A
7   A
8   A
9   C
10  C")

如果ID是OP所提到的时间戳列,这个方法是否有效? - cryo111
在您的解决方案中,min.IDmax.ID 是通过 rle 长度计算出来的。如果 ID 列现在包含(无序的)时间戳,那怎么办?我猜 OP 现在想要各组的最小和最大时间戳。 - cryo111
我现在明白您的意思了。我已经进行了编辑,应该能够解决这个问题。 - lmo

4
一个关于 data.table 的想法:
require(data.table)

dt <- fread("ID  State
1   A
            2   A
            3   B
            4   B
            5   B
            6   A
            7   A
            8   A
            9   C
            10  C")

dt[,rle := rleid(State)]
dt2<-dt[,list(min=min(ID),max=max(ID)),by=c("rle","State")]

这将会得到:

   rle State min max
1:   1     A   1   2
2:   2     B   3   5
3:   3     A   6   8
4:   4     C   9  10

这个思路是通过rleid方法识别序列,然后通过元组rleState获取ID的最小值和最大值。

您可以使用以下代码删除rle列:

dt2[,rle:=NULL]

链接:

 dt2<-dt[,list(min=min(ID),max=max(ID)),by=c("rle","State")][,rle:=NULL]

您可以直接在by中使用rleid来进一步缩短上述代码:

dt2 <- dt[, .(min=min(ID),max=max(ID)), by=.(State, rleid(State))][, rleid:=NULL]

谢谢您的解释,我之前不知道rleid函数。 - HansHupe
1
@HansHupe 这是data.table包的一部分,它可以轻松地处理很多类似这样的事情。 - Tensibai
1
dt[, .(min = min(ID), max = max(ID)), by = .(State, rl = rleid(State))][, rl := NULL][] 这甚至更短。 - Jaap
@pro 我不想进入高级DT语法,也不确定我能否完全解释清楚。因此请随意编辑添加,或作为另一个答案添加;) - Tensibai
1
已添加,与现有答案相比不足以证明需要单独的回答。 - Jaap

2

以下是使用基本 R 中的 rleaggregate 的另一种尝试:

rl <- rle(df$State)
newdf <- data.frame(ID=df$ID, State=rep(1:length(rl$lengths),rl$lengths))
newdf <- aggregate(ID~State, newdf, FUN = function(x) c(minID=min(x), maxID=max(x)))
newdf$State <- rl$values

  # State ID.minID ID.maxID
# 1     A        1        2
# 2     B        3        5
# 3     A        6        8
# 4     C        9       10

数据

df <- structure(list(ID = 1:10, State = c("A", "A", "B", "B", "B", 
"A", "A", "A", "C", "C")), .Names = c("ID", "State"), class = "data.frame", 
row.names = c(NA, 
    -10L))

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