Быстрые способы в R получить первую строку фрейма данных, сгруппированного по идентификатору [закрыто]


14

Иногда мне нужно получить только первую строку набора данных, сгруппированную по идентификатору, как при получении возраста и пола, когда существует несколько наблюдений на человека. Какой быстрый (или самый быстрый) способ сделать это в R? Я использовал агрегат () ниже и подозреваю, что есть лучшие способы. Перед публикацией этого вопроса я немного искал в Google, нашел и попробовал ddply, и был удивлен, что он был очень медленным и дал мне ошибки памяти в моем наборе данных (400 000 строк x 16 столбцов, 7 000 уникальных идентификаторов), тогда как версия aggregate () было достаточно быстро.

(dx <- data.frame(ID = factor(c(1,1,2,2,3,3)), AGE = c(30,30,40,40,35,35), FEM = factor(c(1,1,0,0,1,1))))
# ID AGE FEM
#  1  30   1
#  1  30   1
#  2  40   0
#  2  40   0
#  3  35   1
#  3  35   1
ag <- data.frame(ID=levels(dx$ID))
ag <- merge(ag, aggregate(AGE ~ ID, data=dx, function(x) x[1]), "ID")
ag <- merge(ag, aggregate(FEM ~ ID, data=dx, function(x) x[1]), "ID")
ag
# ID AGE FEM
#  1  30   1
#  2  40   0
#  3  35   1
#same result:
library(plyr)
ddply(.data = dx, .var = c("ID"), .fun = function(x) x[1,])

ОБНОВЛЕНИЕ: См. Ответ Чейза и комментарий Мэтта Паркера о том, что я считаю самым элегантным подходом. Посмотрите ответ @Matthew Dowle для быстрого решения, которое использует data.tableпакет.


Спасибо за все ваши ответы. Решение data.table в @Steve оказалось самым быстрым (~ 5) в моем наборе данных по сравнению с решением aggregate () @Gavin (которое, в свою очередь, было быстрее, чем мой код aggregate ()), и в ~ 7,5 раза. через () решение @Matt. У меня не было времени изменить идею, потому что я не мог заставить ее работать быстро. Я предполагаю, что решение, которое дал @Chase, будет самым быстрым, и это было именно то, что я искал, но когда я начал писать этот комментарий, код не работал (я вижу, что он исправлен сейчас!).
закрыто

На самом деле @Chase был быстрее в 9 раз по сравнению с data.table, поэтому я изменил свой принятый ответ. Еще раз спасибо всем - узнал кучу новых инструментов.
закрыто

извини, я исправил свой код Единственное предостережение или хитрость здесь - объединить значение, которое не входит в ваши идентификаторы, diff()чтобы вы могли получить первый идентификатор dx.
Погоня

Ответы:


10

Ваш столбец идентификаторов действительно является фактором? Если это на самом деле числовой, я думаю, вы можете использовать diffфункцию в ваших интересах. Вы также можете привести его к числовому as.numeric().

dx <- data.frame(
    ID = sort(sample(1:7000, 400000, TRUE))
    , AGE = sample(18:65, 400000, TRUE)
    , FEM = sample(0:1, 400000, TRUE)
)

dx[ diff(c(0,dx$ID)) != 0, ]

1
Умная! Вы могли бы также сделать dx[c(TRUE, dx$ID[-1] != dx$ID[-length(dx$ID)], ]для нечисловых данных - я получаю 0,03 для символа, 0,05 для факторов. PS: есть дополнительная функция )в вашей первой system.time()функции, после второго нуля.
Мэтт Паркер

@Matt - хороший звонок и хороший улов. Кажется, я не могу скопировать / вставить код, который стоит перевернуть сегодня.
Погоня

Я работаю по схеме London Cycle Hire, и мне нужно было найти способ найти первые и последние экземпляры проката велосипедов для пользователей. С 1 миллионом пользователей, 10 миллионами поездок в год и данными за несколько лет мой цикл for выполнял 1 пользователя в секунду. Я попробовал решение «по», и через час его не удалось завершить. Сначала я не мог понять, что делает «альтернатива Мэтта Паркера решению Чейза», но в конце концов пенни упала, и она выполняется за считанные секунды. Таким образом, мой опыт свидетельствует о том, что улучшение становится больше с большими наборами данных.
Джордж Симпсон

@ GeorgeSimpson - рад видеть, что на него все еще ссылаются! Нижеприведенное data.tableрешение должно оказаться самым быстрым, поэтому я бы проверил это на вашем месте (вероятно, здесь должен быть принятый ответ).
Погоня

17

Следуя ответу Стива, в data.table есть гораздо более быстрый путь:

> # Preamble
> dx <- data.frame(
+     ID = sort(sample(1:7000, 400000, TRUE))
+     , AGE = sample(18:65, 400000, TRUE)
+     , FEM = sample(0:1, 400000, TRUE)
+ )
> dxt <- data.table(dx, key='ID')

> # fast self join
> system.time(ans2<-dxt[J(unique(ID)),mult="first"])
 user  system elapsed 
0.048   0.016   0.064

> # slower using .SD
> system.time(ans1<-dxt[, .SD[1], by=ID])
  user  system elapsed 
14.209   0.012  14.281 

> mapply(identical,ans1,ans2)  # ans1 is keyed but ans2 isn't, otherwise identical
  ID  AGE  FEM 
TRUE TRUE TRUE 

Если вам просто нужен первый ряд каждой группы, гораздо быстрее присоединиться к этому ряду напрямую. Зачем создавать объект .SD каждый раз, только чтобы использовать его первый ряд?

Сравните 0,064 data.table с «альтернативой Мэтта Паркера решению Чейза» (которая пока казалась самой быстрой):

> system.time(ans3<-dxt[c(TRUE, dxt$ID[-1] != dxt$ID[-length(dxt$ID)]), ])
 user  system elapsed 
0.284   0.028   0.310 
> identical(ans1,ans3)
[1] TRUE 

Так что ~ в 5 раз быстрее, но это крошечная таблица с количеством строк менее 1 миллиона. С увеличением размера увеличивается и разница.


Вау, я никогда не понимал, насколько «умной» [.data.tableможет быть функция ... Думаю, я не осознавал, что вы не создали .SDобъект, если он вам действительно не нужен. Хороший!
Стив Лианоглу

Да, это действительно быстро! Даже если вы включите dxt <- data.table(dx, key='ID')в вызов system.time (), это быстрее, чем решение @ Matt.
закрыто

Полагаю, это устарело, поскольку новые версии data.table SD[1L]были полностью оптимизированы, а ответ @SteveLianoglou будет в два раза быстрее для строк 5e7.
Дэвид Аренбург,

@DavidArenburg Начиная с v1.9.8 ноября 2016 года, да. Не стесняйтесь редактировать этот ответ напрямую, или, возможно, этот вопрос должен быть вики сообщества или что-то в этом роде.
Мэтт Доул

10

Вам не нужно много merge()шагов, просто aggregate()обе переменные, представляющие интерес:

> aggregate(dx[, -1], by = list(ID = dx$ID), head, 1)
  ID AGE FEM
1  1  30   1
2  2  40   0
3  3  35   1

> system.time(replicate(1000, aggregate(dx[, -1], by = list(ID = dx$ID), 
+                                       head, 1)))
   user  system elapsed 
  2.531   0.007   2.547 
> system.time(replicate(1000, {ag <- data.frame(ID=levels(dx$ID))
+ ag <- merge(ag, aggregate(AGE ~ ID, data=dx, function(x) x[1]), "ID")
+ ag <- merge(ag, aggregate(FEM ~ ID, data=dx, function(x) x[1]), "ID")
+ }))
   user  system elapsed 
  9.264   0.009   9.301

Сроки сравнения:

1) Решение Мэтта:

> system.time(replicate(1000, {
+ agg <- by(dx, dx$ID, FUN = function(x) x[1, ])
+ # Which returns a list that you can then convert into a data.frame thusly:
+ do.call(rbind, agg)
+ }))
   user  system elapsed 
  3.759   0.007   3.785

2) Решение Zach's reshape2:

> system.time(replicate(1000, {
+ dx <- melt(dx,id=c('ID','FEM'))
+ dcast(dx,ID+FEM~variable,fun.aggregate=mean)
+ }))
   user  system elapsed 
 12.804   0.032  13.019

3) Решение для data.table Стива:

> system.time(replicate(1000, {
+ dxt <- data.table(dx, key='ID')
+ dxt[, .SD[1,], by=ID]
+ }))
   user  system elapsed 
  5.484   0.020   5.608 
> dxt <- data.table(dx, key='ID') ## one time step
> system.time(replicate(1000, {
+ dxt[, .SD[1,], by=ID] ## try this one line on own
+ }))
   user  system elapsed 
  3.743   0.006   3.784

4) Быстрое решение Чейза с использованием числового, а не факторного ID:

> dx2 <- within(dx, ID <- as.numeric(ID))
> system.time(replicate(1000, {
+ dy <- dx[order(dx$ID),]
+ dy[ diff(c(0,dy$ID)) != 0, ]
+ }))
   user  system elapsed 
  0.663   0.000   0.663

и 5) альтернатива Мэтта Паркера решению Чейза, для характера или фактора ID, которая немного быстрее, чем числовое решение Чейза ID:

> system.time(replicate(1000, {
+ dx[c(TRUE, dx$ID[-1] != dx$ID[-length(dx$ID)]), ]
+ }))
   user  system elapsed 
  0.513   0.000   0.516

О, правильно, спасибо! Забыли об этом синтаксисе для агрегата.
закрыто

Если вы хотите добавить решение Чейза, вот что я получил:dx$ID <- sample(as.numeric(dx$ID)) #assuming IDs arent presorted system.time(replicate(1000, { dy <- dx[order(dx$ID),] dy[ diff(c(0,dy$ID)) != 0, ] })) user system elapsed 0.58 0.00 0.58
lockedoff

@lockedoff - сделано, спасибо, но я не случайно выбрал IDs, поэтому результат был сопоставим с другими решениями.
Восстановить Монику - Г. Симпсон

И время @Matt Parker версия в комментариях к ответу @ Чейз
Восстановить Монику - Г. Симпсон

2
Спасибо за время, Гэвин - это действительно полезно для таких вопросов.
Мэтт Паркер

9

Вы можете попробовать использовать пакет data.table .

Для вашего конкретного случая, плюс в том, что это (безумно) быстро. Когда я впервые познакомился с ним, я работал с объектами data.frame с сотнями тысяч строк. "Нормальный" aggregateили ddplyметоды были приняты ~ 1-2 минуты, чтобы завершить (это было до того, как Хэдли ввел idata.frameмоджо в ddply). Используя data.table, операция была буквально за несколько секунд.

Недостатком является то, что это так быстро, потому что он преобразует ваш data.table (он как data.frame) в «ключевые столбцы» и использует стратегию интеллектуального поиска для поиска подмножеств ваших данных. Это приведет к изменению порядка ваших данных, прежде чем вы соберете статистику по ним.

Учитывая, что вы просто захотите первый ряд каждой группы - возможно, при переупорядочении будет запутано, какой ряд является первым, поэтому он может не подходить для вашей ситуации.

В любом случае, вам придется судить, подходит ли data.tableэто здесь или нет , но вот как вы будете использовать его с данными, которые вы представили:

install.packages('data.table') ## if yo udon't have it already
library(data.table)
dxt <- data.table(dx, key='ID')
dxt[, .SD[1,], by=ID]
     ID AGE FEM
[1,]  1  30   1
[2,]  2  40   0
[3,]  3  35   1

Обновление: Мэтью Доул (главный разработчик пакета data.table) предоставил лучший / более умный / (чрезвычайно) более эффективный способ использования data.table для решения этой проблемы в качестве одного из ответов здесь ... определенно проверьте это ,


4

Попробуй reshape2

library(reshape2)
dx <- melt(dx,id=c('ID','FEM'))
dcast(dx,ID+FEM~variable,fun.aggregate=mean)

3

Вы могли бы попробовать

agg <- by(dx, dx$ID, FUN = function(x) x[1, ])
# Which returns a list that you can then convert into a data.frame thusly:
do.call(rbind, agg)

Я понятия не имею, будет ли это быстрее, чем plyr, хотя.

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