按组选择第一行


85

从这样的数据框中

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 /字符串对的第一行创建一个新的。如果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查询?



1
@Matthew,我的问题更大。
dmvianna 2014年

2
您的问题是1岁,另一个问题是4岁,对吗?这个问题有很多重复
Matthew

@马修对不起,我一定误读了日期。
dmvianna 2014年

Answers:


119

您可以使用它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

获胜者:system.time(dat3 [!duplicated(dat3 $ id),])用户系统使用了0.07 0.00 0.07
dmvianna 2012年

2
@dmvianna:我没有安装它,也不想打扰它。:)
约书亚·乌尔里希

我们确定我的data.table代码尽可能高效吗?我对自己能否从该工具中获得最佳性能不满。
joran 2012年

2
另外,我认为,如果要对data.table进行基准测试,则键入键时应在基本调用中包含按ID排序。
mnel 2012年

1
@JoshuaUlrich还有一个问题:为什么需要第一句话,即假设数据已经排序。!duplicated(x)找出每个组中的第一个,即使它没有排序,iiuc。
马特·道尔

36

我赞成dplyr方法。

group_by(id) 其次是

  • filter(row_number()==1) 要么
  • slice(1) 要么
  • slice_head(1) #(dplyr => 1.0)
  • top_n(n = -1)
    • top_n()内部使用等级函数。负数从排名底端选择。

在某些情况下,可能有必要在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

2
值得一提sliceslice(x)是的快捷方式filter(row_number() %in% x)
格雷戈尔·托马斯

十分优雅。你知道为什么我有我转换data.tabledata.frame这个工作?
James Hirschorn

@JamesHirschorn我不是所有差异方面的专家。但是data.table从继承,data.frame因此在许多情况下,您可以在上使用dplyr命令 data.table。如果test是,则上面的示例也适用data.table。参见例如stackoverflow.com/questions/13618488/…了解更深入的说明
Kresten

这是一种整洁的方式,正如您所看到的,data.frame在这里实际上是一个小标题。我个人建议您也总是使用小技巧,因为ggplot2的构建方式与此类似。
加里尼

17

关于什么

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

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

编辑

还有一种独特的方法,data.tables该方法将通过键返回第一行

jdtu <- function() unique(DT)

我认为,如果您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        

独特的方法在这里最快。


4
您甚至不必设置密钥。unique(DT,by="id")直接工作
马修

FYI作为data.table版本> = 1.9.8,默认by为参数uniqueby = seq_along(x)(所有列),而不是以前的默认by = key(x)
IceCreamToucan

12

一个简单的ddply选择:

ddply(test,.(id),function(x) head(x,1))

如果速度是一个问题,可以采用类似的方法data.table

testd <- data.table(test)
setkey(testd,id)
testd[,.SD[1],by = key(testd)]

否则可能会更快:

testd[testd[, .I[1], by = key(testd]$V1]

令人惊讶的是,sqldf的执行速度更快:使用data.table的1.77 0.13 1.92与10.53 0.00 10.79
dmvianna 2012年

3
@dmvianna我不一定要排除data.table。我不是该工具的专家,所以我的data.table代码可能不是实现此目的的最有效方法。
joran 2012年

我过早地投票了。当我在一个大的data.table上运行它时,它的速度非常慢,而且无法正常工作:之后的行数是相同的。
James Hirschorn

@JamesHirachorn我很久以前写了这篇文章,该软件包已经发生了很大的变化,我几乎没有使用data.table。如果您找到使用该软件包的正确方法,请随时提出修改建议,以使其变得更好。
joran

8

现在,为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答案). Also I'm not sure what *"if data is numeric"* has anything to do with whether or not one would use first(value)`vs head(value)(或just value[1]
Gregor汤玛斯

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

请注意,所有这三个都依赖于SQL的SQLite扩展,在该扩展中,使用minmax保证会导致从同一行中选择其他列。(在其他无法保证的基于SQL的数据库中。)


谢谢!这比IMO接受的答案好得多,因为它可以普遍化为使用多个聚合函数在聚合步骤中获取第一个/最后一个元素(即,获取此变量的第一个,对该变量求和,等等)。
Bridgeburners

4

甲基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, ]以选择第一行和所有列。


加文,这太慢了:用户系统过去了91.84 6.02 101.10
dmvianna 2012年

任何涉及数据帧的东西都可以。它们的效用是有代价的。因此,例如data.table。
加文·辛普森

在我的辩护和R的辩护中,您没有提到有关效率的问题。通常,易用性一个功能。见证ply的普及,这也是“缓慢的”,至少直到下一个具有data.table支持的版本为止。
加文·辛普森

1
我同意。我不是故意要侮辱你的 但是,我确实发现@ Joshua-Ulrich的方法快速又容易。:7)
dmvianna 2012年

无需道歉,我也不认为这是侮辱。只是指出它的提供没有任何效率要求。请记住,此堆栈溢出问答不仅是为了您的利益,也是其他遇到类似问题的用户的利益。
加文·辛普森
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.