Швидкі способи R отримати перший ряд кадру даних, згрупований за ідентифікатором [закритий]


14

Іноді мені потрібно отримати лише перший рядок набору даних, згрупований за ідентифікатором, як при пошуку віку та статі, коли на кожного людини є кілька спостережень. Який швидкий (або найшвидший) спосіб зробити це в R? Я використовував агрегат () нижче і підозрюю, що є кращі способи. Перш ніж надсилати це питання, я трохи шукав у google, знайшов і спробував ddply, і був здивований, що це надзвичайно повільно, і дав мені помилки пам'яті на моєму наборі даних (400 000 рядків x 16 cols, 7000 унікальних ідентифікаторів), тоді як сукупна () версія був досить швидким.

(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пакет.


Дякую за всі ваші відповіді. Розв’язання даних.Steve було найшвидшим на коефіцієнт ~ 5 у моєму наборі даних над сукупним () рішенням @Gavin (що, в свою чергу, було швидше, ніж мій агрегатний () код), і коефіцієнт ~ 7,5 над рішенням by () @Matt. Я не встиг переробити ідею, оскільки не зміг швидко працювати. Я здогадуюсь, рішення, яке дав @Chase, буде найшвидшим, і це було насправді те, що я шукав, але коли я почав писати цей коментар, код не працював (я бачу, це виправлено зараз!).
заблоковано

Насправді @Chase був швидшим на коефіцієнт ~ 9 над data.table, тому я змінив прийняту відповідь. Ще раз дякую всім - дізналися купу нових інструментів.
заблоковано

вибачте, я виправив свій код. Одне застереження або хитрість тут полягає в об'єднанні значення, яке не є одним із ваших ідентифікаторів, diff()щоб ви могли забрати перший ідентифікатор у dx.
Чейз

Відповіді:


11

Чи дійсно ваш стовпець ідентифікатора є фактором? Якщо вона насправді числова, я думаю, ви можете використовувати 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 мільйонів поїздок на рік та дані за кілька років, мій цикл "за" робив 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 таблиць даних з "альтернативою Метта Паркера рішенням Чейза" (який, здавалося, був найшвидшим досі):

> 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 р. Так. Не соромтесь редагувати цю відповідь безпосередньо, або, можливо, цей Q потребує вікі спільноти чи щось таке.
Метт Даул

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) Розробка форми Зака2:

> 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) рішення даних Стіва:

> 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 - зроблено, спасибі, але я не випадково відібрав вибірки ID, тому результат був порівнянний з іншими рішеннями.
Відновіть Моніку - Г. Сімпсон

І версія @Matt Parker у коментарях до відповіді @ Chase
Відновіть Моніку - Г. Сімпсон

2
Дякую за те, що ви зробили таймінги, Гевін - це дуже корисно для таких питань.
Метт Паркер

10

Ви можете спробувати використовувати пакет 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

Спробуйте змінити форму2

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.