Chọn hàng đầu tiên theo nhóm


85

Từ khung dữ liệu như thế này

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

Tôi muốn tạo một cái mới với hàng đầu tiên của mỗi cặp id / chuỗi. Nếu sqldf chấp nhận mã R bên trong nó, truy vấn có thể trông như sau:

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

Có giải pháp nào ngắn để tạo một cột mới như

test$row <- rownames(test)

và chạy cùng một truy vấn sqldf với min (row)?



1
@Matthew, câu hỏi của tôi cũ hơn.
dmvianna

2
Câu hỏi của bạn là 1 tuổi, và câu hỏi còn lại là 4 tuổi phải không? Có rất nhiều bản sao của câu hỏi này
Ma-thi-ơ

@Matthew Xin lỗi, tôi chắc đã đọc nhầm ngày tháng.
dmvianna

Câu trả lời:


119

Bạn có thể sử dụng duplicatedđể làm điều này rất nhanh chóng.

test[!duplicated(test$id),]

Điểm chuẩn, dành cho những kẻ kỳ quái về tốc độ:

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

Hãy thử lại lần nữa, nhưng chỉ với những ứng cử viên từ lần đầu tiên và với nhiều dữ liệu hơn và nhiều bản sao hơn.

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

Người chiến thắng: system.time (dat3 [! Đôi (dat3 $ id),]) Hệ thống sử dụng trôi qua 0.07 0.00 0.07
dmvianna

2
@dmvianna: Tôi chưa cài đặt nó và không muốn bận tâm đến nó. :)
Joshua Ulrich

Chúng tôi có chắc chắn rằng mã data.table của tôi hiệu quả nhất có thể không? Tôi không tự tin vào khả năng của mình để đạt được hiệu suất tốt nhất từ ​​công cụ đó.
joran

2
Ngoài ra, tôi nghĩ rằng, nếu bạn định chuẩn dữ liệu.table, việc khóa bạn nên bao gồm thứ tự theo id trong các lệnh gọi cơ sở.
mnel

1
@JoshuaUlrich Một câu hỏi nữa: tại sao lại cần câu đầu tiên, tức là giả định rằng dữ liệu đã được sắp xếp. !duplicated(x)tìm nhóm đầu tiên của mỗi nhóm ngay cả khi nó không được sắp xếp, iiuc.
Matt Dowle

36

Tôi ủng hộ cách tiếp cận dplyr.

group_by(id) tiếp theo là một trong hai

  • filter(row_number()==1) hoặc là
  • slice(1) hoặc là
  • slice_head(1) # (dplyr => 1.0)
  • top_n(n = -1)
    • top_n()nội bộ sử dụng chức năng xếp hạng. Các lựa chọn phủ định từ cuối bảng xếp hạng.

Trong một số trường hợp, việc sắp xếp id sau group_by có thể cần thiết.

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)

Cả ba phương pháp đều trả về cùng một kết quả

# 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
Cũng đáng để đưa ra lời cảm ơn slice. slice(x)là một phím tắt cho filter(row_number() %in% x).
Gregor Thomas

Rất thanh lịch. Bạn có biết tại sao tôi phải chuyển đổi của tôi data.tablethành a data.frameđể cái này hoạt động không?
James Hirschorn

@JamesHirschorn Tôi không phải chuyên gia về tất cả sự khác biệt. Nhưng data.tablekế thừa từ data.frameđó trong nhiều trường hợp, bạn có thể sử dụng lệnh dplyr trên a data.table. Ví dụ trên vd cũng hoạt động nếu testlà a data.table. Xem ví dụ: stackoverflow.com/questions/13618488/… để có giải thích sâu hơn
Kresten

Đây là một cách gọn gàng để làm điều đó và như bạn thấy data.frame thực sự là một mẩu nhỏ ở đây. Cá nhân tôi khuyên bạn nên làm việc luôn luôn với các viên sỏi vì ggplot2 được xây dựng theo cách tương tự.
Garini

17

Thế còn

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

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

Biên tập

Ngoài ra còn có một phương pháp duy nhất data.tablessẽ trả về hàng đầu tiên theo khóa

jdtu <- function() unique(DT)

Tôi nghĩ rằng, nếu bạn đang đặt hàng testbên ngoài điểm chuẩn, thì bạn cũng có thể xóa setkeydata.tablechuyển đổi khỏi điểm chuẩn (vì setkey về cơ bản sắp xếp theo id, giống như vậy 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         

và với nhiều dữ liệu hơn

** Chỉnh sửa bằng phương pháp duy nhất **

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        

Phương pháp duy nhất là nhanh nhất ở đây.


4
Bạn thậm chí không phải đặt chìa khóa. unique(DT,by="id")hoạt động trực tiếp
Matthew

FYI như các data.tablephiên bản> = 1.9.8, mặc định bylập luận cho uniqueby = seq_along(x)(tất cả các cột), thay vì mặc định trướcby = key(x)
IceCreamToucan

12

Một ddplylựa chọn đơn giản :

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

Nếu tốc độ là một vấn đề, một cách tiếp cận tương tự có thể được thực hiện với data.table:

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

hoặc điều này có thể nhanh hơn đáng kể:

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

Đáng ngạc nhiên, sqldf làm nó nhanh hơn: 1,77 0,13 1,92 vs 10,53 0,00 10,79 với data.table
dmvianna

3
@dmvianna Tôi không nhất thiết phải đếm data.table. Tôi không phải là chuyên gia về công cụ đó, vì vậy mã data.table của tôi có thể không phải là cách hiệu quả nhất để thực hiện điều đó.
joran

Tôi đã ủng hộ điều này sớm. Khi tôi chạy nó trên một data.table lớn, nó rất chậm và nó không hoạt động: số lượng hàng giống nhau sau đó.
James Hirschorn

@JamesHirachorn Tôi đã viết bài này cách đây khá lâu, gói đã thay đổi rất nhiều và tôi hầu như không sử dụng data.table chút nào. Nếu bạn tìm thấy cách phù hợp để thực hiện việc này với gói đó, vui lòng đề xuất chỉnh sửa để làm cho gói đó tốt hơn.
joran

8

bây giờ, cho dplyr, thêm một bộ đếm riêng biệt.

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

Bạn tạo nhóm, họ tóm tắt trong nhóm.

Nếu dữ liệu là số, bạn có thể sử dụng:
first(value)[there also last(value)] thay chohead(value, 1)

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

Đầy:

> 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

Câu trả lời này khá cũ - có nhiều cách tốt hơn để làm điều này dplyrmà không yêu cầu viết câu lệnh cho mỗi cột đơn lẻ được đưa vào (ví dụ: xem câu trả lời của atomman bên dưới) . Also I'm not sure what *"if data is numeric"* has anything to do with whether or not one would use đầu tiên (giá trị) `so với head(value)(hoặc chỉ value[1])
Gregor Thomas

7

(1) SQLite có một rowidcột giả được tích hợp sẵn để nó hoạt động:

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

cho:

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

(2) sqldfBản thân nó cũng có một row.names=đối số:

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

cho:

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

(3) Phương án thay thế thứ ba kết hợp các yếu tố của hai điều trên có thể còn tốt hơn:

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

cho:

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

Lưu ý rằng cả ba điều này đều dựa trên một phần mở rộng SQLite cho SQL trong đó việc sử dụng minhoặc maxđược đảm bảo sẽ dẫn đến các cột khác được chọn từ cùng một hàng. (Trong các cơ sở dữ liệu dựa trên SQL khác có thể không được đảm bảo.)


Cảm ơn! Điều này tốt hơn nhiều so với câu trả lời được chấp nhận IMO vì nó có thể khái quát để lấy phần tử đầu tiên / cuối cùng trong một bước tổng hợp bằng cách sử dụng nhiều hàm tổng hợp (tức là lấy phần tử đầu tiên của biến này, tính tổng biến đó, v.v.).
Bridgeburners

4

Một tùy chọn cơ bản R là split()- lapply()- do.call()thành ngữ:

> 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

Một lựa chọn trực tiếp hơn là để lapply()các [chức năng:

> 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

Dấu phẩy 1, )ở cuối lapply()cuộc gọi là điều cần thiết vì điều này tương đương với việc gọi [1, ]để chọn hàng đầu tiên và tất cả các cột.


Đây là rất chậm, Gavin: Hệ thống sử dụng trôi qua 91,84 6,02 101,10
dmvianna

Bất cứ điều gì liên quan đến khung dữ liệu sẽ được. Tiện ích của họ đi kèm với một mức giá. Do đó data.table chẳng hạn.
Gavin Simpson

trong cách bảo vệ của tôi và R, bạn đã không đề cập bất cứ điều gì về hiệu quả trong câu hỏi. Thường dễ sử dụng một tính năng. Chứng kiến ​​sự phổ biến của ply, nó cũng "chậm", ít nhất là cho đến phiên bản tiếp theo có hỗ trợ data.table.
Gavin Simpson

1
Tôi đồng ý. Tôi không có ý xúc phạm bạn. Tuy nhiên, tôi thấy rằng phương pháp của @ Joshua-Ulrich vừa nhanh vừa dễ. : 7)
dmvianna

Không cần phải xin lỗi và tôi không coi đó là một sự xúc phạm. Chỉ chỉ ra rằng nó đã được cung cấp mà không có bất kỳ tuyên bố nào về hiệu quả. Hãy nhớ câu hỏi và đáp về Stack Overflow này không chỉ vì lợi ích của bạn mà còn cho những người dùng khác gặp câu hỏi của bạn khi họ gặp vấn đề tương tự.
Gavin Simpson
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.