เลือกแถวแรกตามกลุ่ม


87

จากดาต้าเฟรมเช่นนี้

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)

และเรียกใช้แบบสอบถาม sqldf เดียวกันกับ min (row)?



1
@ แมทธิวคำถามของฉันแก่กว่า
dmvianna

2
คำถามของคุณมีอายุ 1 ปีและอีกคำถามคือ 4 ปีไม่ใช่เหรอ? คำถามนี้มีคำซ้ำกันมากมาย
มัทธิว

@ แมทธิวขออภัยฉันต้องอ่านวันที่ผิด
dmvianna

คำตอบ:


120

คุณสามารถใช้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

2
@dmvianna: ฉันไม่ได้ติดตั้งและไม่รู้สึกรำคาญกับมัน :)
Joshua Ulrich

เราแน่ใจหรือไม่ว่าโค้ด data.table ของฉันมีประสิทธิภาพมากที่สุด ฉันไม่มั่นใจในความสามารถของตัวเองที่จะดึงประสิทธิภาพที่ดีที่สุดออกมาจากเครื่องมือนั้น
joran

2
นอกจากนี้ฉันคิดว่าหากคุณกำลังจะเปรียบเทียบ data.table การป้อนข้อมูลคุณควรรวมลำดับตาม id ไว้ในการเรียกพื้นฐาน
ช่อง

1
@JoshuaUlrich อีกคำถามหนึ่ง: ทำไมประโยคแรกจึงจำเป็นเช่นสมมติว่าข้อมูลถูกจัดเรียงแล้ว !duplicated(x)ค้นหากลุ่มแรกของแต่ละกลุ่มแม้ว่าจะไม่ได้เรียงลำดับก็ตาม iiuc
Matt Dowle

38

ฉันชอบวิธี dplyr

group_by(id) ตามด้วย

  • filter(row_number()==1) หรือ
  • slice(1) หรือ
  • slice_head(1) # (dplyr => 1.0)
  • top_n(n = -1)
    • top_n()ใช้ฟังก์ชันอันดับภายใน Negative เลือกจากด้านล่างของอันดับ

ในบางกรณีการจัดเรียงรหัสหลังจาก group_by อาจจำเป็น

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
ควรค่าแก่การตะโกนออกไปsliceเช่นกัน เป็นทางลัดสำหรับslice(x) filter(row_number() %in% x)
Gregor Thomas

สง่างามมาก. คุณรู้หรือไม่ว่าทำไมฉันต้องแปลงdata.tableเป็น a data.frameเพื่อให้ได้ผล?
James Hirschorn

@JamesHirschorn ฉันไม่ใช่ผู้เชี่ยวชาญเกี่ยวกับความแตกต่างทั้งหมด แต่data.tableสืบทอดมาจากdata.frameหลาย ๆ กรณีคุณสามารถใช้คำสั่ง dplyr บนไฟล์ data.table. ตัวอย่างข้างต้นเช่นใช้งานได้ถ้าtestเป็นไฟล์data.table. ดูเช่นstackoverflow.com/questions/13618488/…สำหรับคำอธิบายที่ลึกซึ้งยิ่งขึ้น
Kresten

นี่เป็นวิธีที่ไม่เป็นระเบียบเรียบร้อยและอย่างที่คุณเห็น data.frame นั้นเป็นวิธีที่ทำได้จริง ฉันแนะนำให้คุณทำงานกับ tibbles เป็นการส่วนตัวเนื่องจาก ggplot2 สร้างขึ้นในลักษณะที่คล้ายกัน
Garini

17

แล้ว

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

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

แก้ไข

นอกจากนี้ยังมีวิธีการเฉพาะdata.tablesซึ่งจะส่งคืนแถวแรกด้วยคีย์

jdtu <- function() unique(DT)

ฉันคิดว่าหากคุณกำลังสั่งซื้อtestนอกเกณฑ์มาตรฐานคุณสามารถลบsetkeyและdata.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")ทำงานโดยตรง
Matthew

FYI เป็นdata.tableเวอร์ชัน> = 1.9.8 byอาร์กิวเมนต์เริ่มต้นสำหรับuniqueคือby = 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 เร็วกว่า: 1.77 0.13 1.92 เทียบกับ 10.53 0.00 10.79 ด้วย data.table
dmvianna

3
@dmvianna ฉันไม่จำเป็นต้องนับ data.table ฉันไม่ใช่ผู้เชี่ยวชาญเกี่ยวกับเครื่องมือนั้นดังนั้นโค้ด data.table ของฉันอาจไม่ใช่วิธีที่มีประสิทธิภาพที่สุดในการดำเนินการดังกล่าว
joran

ฉันโหวตเรื่องนี้ก่อนกำหนด เมื่อฉันเรียกใช้ข้อมูลขนาดใหญ่ตารางมันช้าอย่างน่าขันและไม่ได้ผล: จำนวนแถวเท่ากันหลังจากนั้น
James Hirschorn

@JamesHirachorn ฉันเขียนไว้เมื่อนานมาแล้วแพ็คเกจมีการเปลี่ยนแปลงมากและฉันแทบจะไม่ได้ใช้ data โต๊ะเลย หากคุณพบวิธีที่ถูกต้องในการดำเนินการกับแพ็กเกจนั้นอย่าลังเลที่จะแนะนำการแก้ไขเพื่อให้ดีขึ้น
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)(or just 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 ซึ่งการใช้minหรือmaxได้รับการรับรองว่าจะส่งผลให้คอลัมน์อื่น ๆ ถูกเลือกจากแถวเดียวกัน (ในฐานข้อมูลที่ใช้ 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, ]เพื่อเลือกแถวแรกและคอลัมน์ทั้งหมด


สิ่งนี้ช้ามาก Gavin: ระบบผู้ใช้ผ่านไป 91.84 6.02 101.10
dmvianna

ทุกสิ่งที่เกี่ยวข้องกับเฟรมข้อมูลจะเป็น ประโยชน์ใช้สอยของพวกเขามาในราคา ดังนั้น data.table ตัวอย่างเช่น
Gavin Simpson

ในการป้องกันของฉันและ R คุณไม่ได้พูดถึงอะไรเกี่ยวกับประสิทธิภาพในคำถาม บ่อยครั้งที่ความสะดวกในการใช้งานเป็นคุณสมบัติ เป็นสักขีพยานถึงความนิยมของ ply ซึ่ง "ช้า" เช่นกันอย่างน้อยก็จนถึงเวอร์ชันถัดไปที่มีการรองรับ data.table
Gavin Simpson

1
ฉันเห็นด้วย. ฉันไม่ได้ตั้งใจที่จะดูถูกคุณ ฉันพบว่าวิธีการของ @ Joshua-Ulrich นั้นทั้งรวดเร็วและง่ายดาย : 7)
dmvianna

ไม่ต้องขอโทษและฉันไม่ได้เอามาดูถูก เป็นเพียงการชี้ให้เห็นว่ามีการเสนอโดยไม่มีการอ้างถึงประสิทธิภาพ โปรดจำไว้ว่าคำถามและคำตอบStack Overflowนี้ไม่ได้มีไว้เพื่อประโยชน์ของคุณเท่านั้น แต่สำหรับผู้ใช้รายอื่นที่พบคำถามของคุณเนื่องจากพวกเขามีปัญหาที่คล้ายกัน
Gavin Simpson
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.