按组选择第一行。

120

从类似这样的数据框开始

test <- data.frame('id'= rep(1:5,2), 'string'= LETTERS[1:10])
test <- test[order(test$id), ]
rownames(test) <- 1:10

> test
    id string
 1   1      A
 2   1      F
 3   2      B
 4   2      G
 5   3      C
 6   3      H
 7   4      D
 8   4      I
 9   5      E
 10  5      J

我希望创建一个由每个id / string对的第一行组成的新数据集。如果sqldf允许其中嵌入R代码,查询可能看起来像这样:

res <- sqldf("select id, min(rownames(test)), string 
              from test 
              group by id, string")

> res
    id string
 1   1      A
 3   2      B
 5   3      C
 7   4      D
 9   5      E

除了创建一个新的列,是否有其他解决方案?

test$row <- rownames(test)

并使用min(row)运行相同的sqldf查询?


可能是[按每组选择一行折叠数据框]的重复问题(https://dev59.com/a3E85IYBdhLWcg3wwWUb)。 - Matthew
1
@Matthew,我的问题比较旧。 - dmvianna
2
你的问题已经有1年了,而另一个问题已经有4年了,对吗?这个问题已经有很多重复的了。 - Matthew
@Matthew 对不起,我可能误读了日期。 - dmvianna
8个回答

142
你可以使用duplicated来快速完成这个任务。
test[!duplicated(test$id),]

性能测试,适合追求速度的用户:

ju <- function() test[!duplicated(test$id),]
gs1 <- function() do.call(rbind, lapply(split(test, test$id), head, 1))
gs2 <- function() do.call(rbind, lapply(split(test, test$id), `[`, 1, ))
jply <- function() ddply(test,.(id),function(x) head(x,1))
jdt <- function() {
  testd <- as.data.table(test)
  setkey(testd,id)
  # Initial solution (slow)
  # testd[,lapply(.SD,function(x) head(x,1)),by = key(testd)]
  # Faster options :
  testd[!duplicated(id)]               # (1)
  # testd[, .SD[1L], by=key(testd)]    # (2)
  # testd[J(unique(id)),mult="first"]  # (3)
  # testd[ testd[,.I[1L],by=id] ]      # (4) needs v1.8.3. Allows 2nd, 3rd etc
}

library(plyr)
library(data.table)
library(rbenchmark)

# sample data
set.seed(21)
test <- data.frame(id=sample(1e3, 1e5, TRUE), string=sample(LETTERS, 1e5, TRUE))
test <- test[order(test$id), ]

benchmark(ju(), gs1(), gs2(), jply(), jdt(),
    replications=5, order="relative")[,1:6]
#     test replications elapsed relative user.self sys.self
# 1   ju()            5    0.03    1.000      0.03     0.00
# 5  jdt()            5    0.03    1.000      0.03     0.00
# 3  gs2()            5    3.49  116.333      2.87     0.58
# 2  gs1()            5    3.58  119.333      3.00     0.58
# 4 jply()            5    3.69  123.000      3.11     0.51

让我们再试一次,只选取第一轮比赛的竞争者,增加更多数据和更多重复实验。

set.seed(21)
test <- data.frame(id=sample(1e4, 1e6, TRUE), string=sample(LETTERS, 1e6, TRUE))
test <- test[order(test$id), ]
benchmark(ju(), jdt(), order="relative")[,1:6]
#    test replications elapsed relative user.self sys.self
# 1  ju()          100    5.48    1.000      4.44     1.00
# 2 jdt()          100    6.92    1.263      5.70     1.15

2
@dmvianna:我没有安装它,也不想烦。 :) - Joshua Ulrich
@joran 哎呀,S.O. 动作真快。这很低效。没有关键字只需 DT[,.SD[1L],by=id]。或者使用 setkey 和相同的方式,或者 DT[J(unique(id)),mult="first"]。但是对于这种情况,DT[!duplicated(id)] 可能是最快的,就像 ju() 一样。但是按组 lapply S3 genric (head) 将会很慢。 - Matt Dowle
2
另外,我认为,如果你要对 data.table 进行基准测试并进行键控,那么你应该在基本调用中包括按 id 排序。 - mnel
1
@JoshuaUlrich 再问一个问题:为什么需要第一句话,即假设数据已经排序。如果没有排序,“!duplicated(x)”也可以找到每个组的第一个元素,如果我理解正确的话。 - Matt Dowle
@MatthewDowle:你说得非常正确,这并不是必需的。干得好! - Joshua Ulrich
显示剩余7条评论

87

我更喜欢dplyr的方法。

group_by(id)后跟以下任一选项:

  • filter(row_number()==1) 或者
  • slice(1) 或者
  • slice_head(1) #(dplyr => 1.0)
  • top_n(n = -1)
    • top_n() 内部使用rank函数。负数选择从rank底部开始。

在某些情况下,在group_by之后排列id可能是必要的。

library(dplyr)

# using filter(), top_n() or slice()

m1 <-
test %>% 
  group_by(id) %>% 
  filter(row_number()==1)

m2 <-
test %>% 
  group_by(id) %>% 
  slice(1)

m3 <-
test %>% 
  group_by(id) %>% 
  top_n(n = -1)

三种方法返回相同的结果。

# A tibble: 5 x 2
# Groups:   id [5]
     id string
  <int> <fct> 
1     1 A     
2     2 B     
3     3 C     
4     4 D     
5     5 E

3
值得一提的是 slice 命令。 slice(x)filter(row_number() %in% x) 的快捷方式。 - Gregor Thomas
非常优雅。你知道为什么我必须将我的 data.table 转换为 data.frame 才能使其工作吗? - James Hirschorn
@JamesHirschorn 我不是所有差异的专家。但是 data.table 继承自 data.frame,因此在许多情况下,您可以在 data.table 上使用 dplyr 命令。例如上面的示例,如果 testdata.table,则也可以工作。请参见 https://dev59.com/5mYr5IYBdhLWcg3wg6b9 以获取更深入的解释。 - Kresten
这是一个整洁的方式来完成它,正如您所看到的,data.frame实际上在这里是一个tibble。我个人建议您始终使用tibbles,因为ggplot2也是以类似的方式构建的。 - Garini

19

怎么样呢?

DT <- data.table(test)
setkey(DT, id)

DT[J(unique(id)), mult = "first"]

编辑

此外,data.tables 还有一种独特的方法,可以通过键返回第一行


jdtu <- function() unique(DT)
我认为,如果你在基准测试之外进行测试的订购,那么你可以从基准测试中删除setkeydata.table转换(因为setkey基本上按id排序,与order相同)。

我认为,如果您在基准测试之外使用test进行订购,那么您也可以从基准测试中删除setkeydata.table转换(因为setkey基本上按照id排序,与order相同)。
set.seed(21)
test <- data.frame(id=sample(1e3, 1e5, TRUE), string=sample(LETTERS, 1e5, TRUE))
test <- test[order(test$id), ]
DT <- data.table(DT, key = 'id')
ju <- function() test[!duplicated(test$id),]

jdt <- function() DT[J(unique(id)),mult = 'first']


 library(rbenchmark)
benchmark(ju(), jdt(), replications = 5)
##    test replications elapsed relative user.self sys.self 
## 2 jdt()            5    0.01        1      0.02        0        
## 1  ju()            5    0.05        5      0.05        0         

并且有更多的数据

**使用独特的方法进行编辑**

set.seed(21)
test <- data.frame(id=sample(1e4, 1e6, TRUE), string=sample(LETTERS, 1e6, TRUE))
test <- test[order(test$id), ]
DT <- data.table(test, key = 'id')
       test replications elapsed relative user.self sys.self 
2  jdt()            5    0.09     2.25      0.09     0.00    
3 jdtu()            5    0.04     1.00      0.05     0.00      
1   ju()            5    0.22     5.50      0.19     0.03        

这里的独特方法是最快的。


5
不需要设置键值。unique(DT,by="id") 直接奏效。 - Matthew
请注意,从 data.table 版本 >= 1.9.8 开始,unique 函数的默认 by 参数为 by = seq_along(x)(所有列),而不是以前的默认值 by = key(x) - IceCreamToucan

16
如果速度是一个问题,可以采用类似的方法来处理data.table
testd <- data.table(test)
testd[, .SD[1], by = id]

或者这可能会快得多:
testd[testd[, .I[1], by = id]$V1]

一个简单的ddply选项:
ddply(test,.(id),function(x) head(x,1))

令人惊讶的是,使用sqldf更快:1.77 0.13 1.92与使用data.table的10.53 0.00 10.79相比。 - dmvianna
3
我不会完全排除使用data.table。虽然我不是这个工具的专家,所以我的data.table代码可能不是最有效的处理方式。 - joran
我过早地点赞了这个。当我在一个大的数据表上运行它时,速度非常慢,而且它也没有起作用:行数仍然是一样的。 - James Hirschorn
@JamesHirachorn 我写这个已经很久了,这个包已经有很大的变化,而且我几乎不再使用data.table。如果你找到了使用该包完成此操作的正确方法,请随时建议编辑以使其更好。 - joran

9

现在,对于 dplyr,添加一个不同的计数器。

df %>%
    group_by(aa, bb) %>%
    summarise(first=head(value,1), count=n_distinct(value))

你创建分组,然后在分组内进行汇总。

如果数据是数值型的,可以使用:
first(value) [还有last(value)] 代替 head(value, 1)

参见:http://cran.rstudio.com/web/packages/dplyr/vignettes/introduction.html

> df
Source: local data frame [16 x 3]

   aa bb value
1   1  1   GUT
2   1  1   PER
3   1  2   SUT
4   1  2   GUT
5   1  3   SUT
6   1  3   GUT
7   1  3   PER
8   2  1   221
9   2  1   224
10  2  1   239
11  2  2   217
12  2  2   221
13  2  2   224
14  3  1   GUT
15  3  1   HUL
16  3  1   GUT

> library(dplyr)
> df %>%
>   group_by(aa, bb) %>%
>   summarise(first=head(value,1), count=n_distinct(value))

Source: local data frame [6 x 4]
Groups: aa

  aa bb first count
1  1  1   GUT     2
2  1  2   SUT     2
3  1  3   SUT     3
4  2  1   221     3
5  2  2   217     3
6  3  1   GUT     2

这个答案已经过时了 - 有更好的方法可以使用 dplyr 来完成,而不需要为每个要包含的列编写语句(例如,参见下面的 atomman 的答案)。此外,我不确定“如果数据是数字”与是否使用 first(value) vs head(value)(或只是 value[1])有任何关系。 - Gregor Thomas

7

(1) SQLite内置了rowid伪列,因此可以这样使用:

sqldf("select min(rowid) rowid, id, string 
               from test 
               group by id")

提供:

  rowid id string
1     1  1      A
2     3  2      B
3     5  3      C
4     7  4      D
5     9  5      E

(2) 同样,sqldf 本身具有 row.names= 参数:

sqldf("select min(cast(row_names as real)) row_names, id, string 
              from test 
              group by id", row.names = TRUE)

提供:

  id string
1  1      A
3  2      B
5  3      C
7  4      D
9  5      E

(3)第三种混合了以上两种元素的方案可能会更好:
sqldf("select min(rowid) row_names, id, string 
               from test 
               group by id", row.names = TRUE)

提供:

  id string
1  1      A
3  2      B
5  3      C
7  4      D
9  5      E

请注意,这三种方法都依赖于SQLite扩展SQL,其中使用minmax保证会选择同一行的其他列。(在其他基于SQL的数据库中可能无法保证。)

谢谢!在我看来,这比被接受的答案好多了,因为它可以推广到使用多个聚合函数进行聚合步骤中获取第一个/最后一个元素(即获取该变量的第一个元素、求和该变量等)。 - Bridgeburners

6
一个基本的 R 选项是 split()-lapply()-do.call()习惯用法:
> do.call(rbind, lapply(split(test, test$id), head, 1))
  id string
1  1      A
2  2      B
3  3      C
4  4      D
5  5      E

更直接的选项是使用lapply()函数应用[函数:
> do.call(rbind, lapply(split(test, test$id), `[`, 1, ))
  id string
1  1      A
2  2      B
3  3      C
4  4      D
5  5      E

逗号和空格1, )lapply()语句的末尾是必要的,因为这等价于调用[1, ]来选择第一行和所有列。

这非常慢,Gavin:用户系统经过了91.84秒,实际经过了101.10秒。 - dmvianna
任何涉及数据框的操作都是必要的。然而,它们的实用性是有代价的。因此,例如 data.table。 - Gavin Simpson
在我的和R的辩护中,你在问题中没有提到任何关于效率的事情。通常易用性本身就是一种特性。可以看出ply的流行程度,即使它“缓慢”,至少在下一个版本中有data.table支持之前是这样的。 - Gavin Simpson
1
我同意。我并不是想侮辱你。但我确实发现@Joshua-Ulrich的方法又快又简单。:7) - dmvianna
不需要道歉,我并没有把它当成一种侮辱。只是想指出,没有提供任何效率声明就进行了提议。请记住,这个问答环节不仅是为了你的好处,还是为了其他遇到类似问题的用户。 - Gavin Simpson

0
一个非常快速的选项是collapse::ffirst:
library(collapse)
ffirst(test, g = test$id)

#   id string
# 1  1      A
# 2  2      B
# 3  3      C
# 4  4      D
# 5  5      E

一个更近期的`dplyr`答案是使用`slice_head`的内联分组和`by`参数。
library(dplyr)
slice_head(test, n = 1, by = id)

data.table 相比,在一个包含 1,000,000 行和 10,000 组的数据集上,collapse 的速度几乎是两倍。
Unit: milliseconds
     expr     min       lq     mean   median       uq      max neval
 collapse  8.8234 10.31675 13.27663 11.85590 14.59135  35.9251   100
       DT 17.0479 19.35955 24.61700 21.34465 24.61960 172.5803   100
      DT2 10.5810 13.03335 23.65378 21.70410 26.26575 195.0825   100

代码

set.seed(21)
library(collapse)
library(data.table)
library(dplyr)
test <- data.frame(id=sample(1e4, 1e6, TRUE), string=sample(LETTERS, 1e6, TRUE))
test <- test[order(test$id), ]
DT <- data.table(test, key = 'id')

library(microbenchmark)
microbenchmark(
  collapse = ffirst(test, g = test$id),
  DT = DT[J(unique(DT, by = "id")), mult = "first"],
  DT2 = DT[DT[, .I[1], by = id]$V1]
)

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