根据列条目(或排名)对数据框进行子集化。

13

我有一个非常简单的数据框:

id group idu  value
1  1     1_1  34
2  1     2_1  23
3  1     3_1  67
4  2     4_2  6
5  2     5_2  24
6  2     6_2  45
1  3     1_3  34
2  3     2_3  67
3  3     3_3  76

我想要从哪里检索每个组的前几个条目的子集,类似于:

id group idu value
1  1     1_1 34
4  2     4_2 6
1  3     1_3 34

id不唯一,因此方法不应该依赖它。

我能避免循环来实现这个吗?

data <- data.frame(
  id = c(1L, 2L, 3L, 4L, 5L, 6L, 1L, 2L, 3L),
  group = rep(1:3, each = 3L),
  idu = factor(c("1_1", "2_1", "3_1", "4_2", "5_2", "6_2", "1_3", "2_3", "3_3")),
  value = c(34L, 23L, 67L, 6L, 24L, 45L, 34L, 67L, 76L)
)
4个回答

10

使用 Gavin 的百万行数据框:

DF3 <- data.frame(id = sample(1000, 1000000, replace = TRUE),
                  group = factor(rep(1:1000, each = 1000)),
                  value = runif(1000000))
DF3 <- within(DF3, idu <- factor(paste(id, group, sep = "_")))

我认为最快的方法是重新排列数据框,然后使用 duplicated:

system.time({
  DF4 <- DF3[order(DF3$group), ]
  out2 <- DF4[!duplicated(DF4$group), ]
})
# user  system elapsed 
# 0.335   0.107   0.441

在我的电脑上,这相当于Gavin的最快的lapply+split方法需要7秒。

通常,在使用数据框时,最快的方法通常是生成所有索引,然后进行一次子集操作。


这是一个不错的方法,但是为了补充一下,真实数据可能会重复组代码,这需要额外的步骤:向整个数据集添加一个真实的单一组ID,可能基于时间戳列。 - Paulo E. Cardoso
为什么 !duplicated 返回重复组的第一个值? - zach
如果您阅读duplicated的帮助文档,您可以看到它实际上是“重复”的一个相当具体的定义 - “具有较小下标的元素的重复项”。 因此,当遇到组ID的第一次时,R仅查看其处理过的先前记录,而不是任何前面的副本。 所以它返回FALSE,这是Hadley反转的。 - Matt Parker

5

根据OP的评论更新

如果在百万行以上进行此操作,所有提供的选项都会很慢。以下是对一个虚拟数据集(100,000行)进行比较所需的时间:

set.seed(12)
DF3 <- data.frame(id = sample(1000, 100000, replace = TRUE),
                  group = factor(rep(1:100, each = 1000)),
                  value = runif(100000))
DF3 <- within(DF3, idu <- factor(paste(id, group, sep = "_")))

> system.time(out1 <- do.call(rbind, lapply(split(DF3, DF3["group"]), `[`, 1, )))
   user  system elapsed 
 19.594   0.053  19.984 
> system.time(out3 <- aggregate(DF3[,-2], DF3["group"], function (x) x[1]))
   user  system elapsed 
 12.419   0.141  12.788 

我放弃了处理一百万行的数据。信不信由你,更快的方法是:

out2 <- matrix(unlist(lapply(split(DF3[, -4], DF3["group"]), `[`, 1,)),
               byrow = TRUE, nrow = (lev <- length(levels(DF3$group))))
colnames(out2) <- names(DF3)[-4]
rownames(out2) <- seq_len(lev)
out2 <- as.data.frame(out2)
out2$group <- factor(out2$group)
out2$idu <- factor(paste(out2$id, out2$group, sep = "_"),
                   levels = levels(DF3$idu))

输出结果(实际上)是相同的:

> all.equal(out1, out2)
[1] TRUE
> all.equal(out1, out3[, c(2,1,3,4)])
[1] "Attributes: < Component 2: Modes: character, numeric >"              
[2] "Attributes: < Component 2: target is character, current is numeric >"

out1(或out2)和out3aggregate()版本)之间的区别仅在于组件的行名称。)

计时为:

   user  system elapsed 
  0.163   0.001   0.168

在处理10万行问题和百万行问题时:

set.seed(12)
DF3 <- data.frame(id = sample(1000, 1000000, replace = TRUE),
                  group = factor(rep(1:1000, each = 1000)),
                  value = runif(1000000))
DF3 <- within(DF3, idu <- factor(paste(id, group, sep = "_")))

在计时方面

   user  system elapsed 
 11.916   0.000  11.925

使用矩阵版本(生成out2)在处理100,000行问题时比其他版本更快,这表明使用矩阵非常快速,而我的do.call()版本中的瓶颈是将结果rbind()在一起。

百万行问题的计时是通过以下方式完成的:

system.time({out4 <- matrix(unlist(lapply(split(DF3[, -4], DF3["group"]),
                                          `[`, 1,)),
                            byrow = TRUE,
                            nrow = (lev <- length(levels(DF3$group))))
             colnames(out4) <- names(DF3)[-4]
             rownames(out4) <- seq_len(lev)
             out4 <- as.data.frame(out4)
             out4$group <- factor(out4$group)
             out4$idu <- factor(paste(out4$id, out4$group, sep = "_"),
                                levels = levels(DF3$idu))})

翻译

如果你的数据在DF中,那么:

do.call(rbind, lapply(with(DF, split(DF, group)), head, 1))

会做你想要的:

> do.call(rbind, lapply(with(DF, split(DF, group)), head, 1))
  idu group
1   1     1
2   4     2
3   7     3

如果新数据在DF2中,则我们会得到:

> do.call(rbind, lapply(with(DF2, split(DF2, group)), head, 1))
  id group idu value
1  1     1 1_1    34
2  4     2 4_2     6
3  1     3 1_3    34

但是为了速度,我们可能想要使用子集而不是使用head(),并且通过不使用with()可以获得一些收益,例如:

do.call(rbind, lapply(split(DF2, DF2$group), `[`, 1, ))

> system.time(replicate(1000, do.call(rbind, lapply(split(DF2, DF2$group), `[`, 1, ))))
   user  system elapsed 
  3.847   0.040   4.044
> system.time(replicate(1000, do.call(rbind, lapply(split(DF2, DF2$group), head, 1))))
   user  system elapsed 
  4.058   0.038   4.111
> system.time(replicate(1000, aggregate(DF2[,-2], DF2["group"], function (x) x[1])))
   user  system elapsed 
  3.902   0.042   4.106

似乎可以工作,Gavin。我编辑了这个问题的内容,但它可能不会受到影响。我必须用一个200万行的数据框来测试它的性能。 - Paulo E. Cardoso
@Paulo,我更新了回答,并附上了在这个数据集合上多次运行的比较时间。 - Gavin Simpson
@Paulo Cardosa,我对一个大问题进行了一些计时,所有选项都很慢,所以我提供了一个使用矩阵的版本,速度更快。包括一百万行问题的计时。 - Gavin Simpson
非常有用的信息,Gavin。我会尝试使用真实数据来查看当DF具有更多列时它的行为如何。所有这些都非常关键,因为我需要处理一个2000万行的对象,任何节省时间都将对最终计算产生巨大影响。 - Paulo E. Cardoso
另一个要求是保留仅匹配nrows选择限制条件(符合DF2$group条目的条目)。代码能否适应此要求? - Paulo E. Cardoso

1
我认为这会起作用:
aggregate(data["idu"], data["group"], function (x) x[1])

针对您更新的问题,我建议使用plyr包中的ddply函数:
ddply(data, .(group), function (x) x[1,])

1

使用plyr的一种解决方案,假设您的数据存储在名为zzz的对象中:

ddply(zzz, "group", function(x) x[1 ,])

另一种选项是计算行之间的差异,应该会更快,但前提是对象事先被排序。这也假设您没有一个组值为0:

zzz <- zzz[order(zzz$group) ,]

zzz[ diff(c(0,zzz$group)) != 0, ]

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