Выбрать первую строку по группе


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 02

2
Вашему вопросу 1 год, а другому вопросу 4 года, не так ли? Есть так много дубликатов этого вопроса
Мэтью

@ Мэтью Извини, я, должно быть, неправильно прочитал даты.
dmvianna 04

Ответы:


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),]) user system elapsed 0,07 0,00 0,07
dmvianna

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

Уверены ли мы, что мой код data.table максимально эффективен? Я не уверен в своей способности добиться максимальной производительности от этого инструмента.
joran

2
Кроме того, я считаю, что если вы собираетесь протестировать таблицу data.table, при вводе ключей вы должны включить упорядочение по идентификатору в базовые вызовы.
mnel 08

1
@JoshuaUlrich Еще один вопрос: зачем нужно первое предложение, т.е. предположение, что данные уже отсортированы. !duplicated(x)находит первую в каждой группе, даже если она не отсортирована, iiuc.
Мэтт Доул

38

Я предпочитаю подход 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).
Gregor Thomas

Очень элегантный. Вы знаете, почему мне нужно преобразовать my data.tableв a, data.frameчтобы это работало?
Джеймс Хиршорн

@JamesHirschorn Я не эксперт во всех различиях. Но data.tableунаследовано от, data.frameпоэтому во многих случаях вы можете использовать команды dplyr для файла data.table. Пример выше, например, также работает, если testэто файл 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 07

3
@dmvianna Я бы не стал считать data.table обязательно. Я не эксперт в этом инструменте, поэтому мой код data.table может быть не самым эффективным способом решить эту проблему.
joran

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

@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, не требующие написания оператора для каждого отдельного столбца, который нужно включить (см., Например, ответ атомщика ниже) . 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])
Грегор 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, ]для выбора первой строки и всех столбцов.


Это было очень медленно, Гэвин:
срок действия

Все, что связано с фреймами данных, будет. Их полезность имеет свою цену. Отсюда, например, data.table.
Гэвин Симпсон

В мою защиту и R, вы ничего не упомянули об эффективности в вопросе. Часто удобство использования - это особенность. Обратите внимание на популярность ply, которая тоже «медленная», по крайней мере, до следующей версии, которая будет поддерживать data.table.
Гэвин Симпсон

1
Я согласен. Я не хотел тебя оскорблять. Однако я обнаружил, что метод @Joshua-Ulrich был одновременно быстрым и простым. : 7)
dmvianna 08

Не нужно извиняться, и я не воспринял это как оскорбление. Просто указывал, что это было предложено без каких-либо претензий на эффективность. Помните, что эти вопросы и ответы о переполнении стека предназначены не только для вас, но и для других пользователей, которые сталкиваются с вашим вопросом, поскольку у них есть аналогичная проблема.
Гэвин Симпсон
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.