Виділіть перший рядок за групою


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 / 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 (рядок)?



1
@Matthew, моє запитання старіше.
dmvianna

2
Твоєму питанню 1 рік, а іншому питанню 4 роки, ні? Дуже багато дублікатів цього запитання
Матвій

@Matthew Вибачте, я, мабуть, неправильно прочитав дати.
dmvianna

Відповіді:


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),]) user system минуло 0,07 0,00 0,07
dmvianna

2
@dmvianna: У мене його не встановлено, і я не хотів з цим заморочуватися. :)
Джошуа Ульріх

Ми впевнені, що мій код data.table максимально ефективний? Я не впевнений у своїй здатності досягти найкращої продуктивності за допомогою цього інструменту.
Джоран

2
Крім того, я вважаю, якщо ви збираєтеся провести тестування data.table, введення ключів має включати порядок за ідентифікатором серед базових викликів.
mnel

1
@JoshuaUlrich Ще одне питання: навіщо потрібне перше речення, тобто припущення, що дані вже відсортовані. !duplicated(x)знаходить першу з кожної групи, навіть якщо вона не відсортована, iiuc.
Matt Dowle

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.

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).
Грегор Томас

Дуже елегантний. Чи знаєте ви, чому я повинен перетворити свій data.tableна a, data.frameщоб це працювало?
Джеймс Гіршхорн,

@JamesHirschorn Я не фахівець з усіх відмінностей. Але data.tableуспадковується від data.frameso, тому в багатьох випадках ви можете використовувати команди dplyr на a data.table. Наведений вище приклад, наприклад, також працює, якщо testє a data.table. Див , наприклад stackoverflow.com/questions/13618488 / ... для більш глибокого explanantion
Крестен

Це дуже непоганий спосіб зробити це, і, як ви бачите, data.frame насправді тут є підказкою. Я особисто раджу вам завжди працювати з таблицями, тому що ggplot2 побудований подібним чином.
Гаріні

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 в основному сортує за ідентифікатором, так само, як 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аргумент 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 може бути не найефективнішим способом зробити це.
Джоран

Я передчасно проголосував за це. Коли я запускав його на великому data.table, це було смішно повільно, і це не працювало: кількість рядків була однаковою після.
Джеймс Гіршхорн,

@JamesHirachorn Я писав це давно, пакет сильно змінився, і я майже не використовую data.table взагалі. Якщо ви знайдете правильний спосіб зробити це за допомогою цього пакету, сміливо пропонуйте редагування, щоб покращити його.
Джоран

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 спочатку (значення) `vs head(value)(або просто value[1])
Грегор Томас

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, ]для вибору першого рядка та всіх стовпців.


Це було дуже повільно, Гевіне: система користувача закінчилась 91,84 6,02 101,10
dmvianna

Буде що завгодно, що включає кадри даних. Їх корисність має свою ціну. Звідси data.table, наприклад.
Гевін Сімпсон,

на мій захист і R, ви нічого не згадали про ефективність у питанні. Часто простота використання є особливістю. Засвідчіть популярність ply, яка теж "повільна", принаймні до наступної версії, яка має підтримку data.table.
Гевін Сімпсон,

1
Я згоден. Я не хотів вас ображати. Я знайшов, проте, що @ метод Джошуа-Ульріха був як швидко і легко. : 7)
dmvianna

Не потрібно вибачатися, і я не сприйняв це як образу. Тільки вказував, що це було запропоновано без будь-яких претензій на ефективність. Пам’ятайте, що запитання та відповіді щодо переповнення стеку не лише для вашої вигоди, але й для інших користувачів, які стикаються з вашим запитанням, оскільки вони мають подібну проблему.
Gavin Simpson
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.