Применение агрегатной функции к нескольким различным срезам

У меня есть массив данных, который содержит некоторую информацию о людях и проектах как таковых:

person_id | project_id | action | time
--------------------------------------
        1 |          1 |      w |    1
        1 |          2 |      w |    2
        1 |          3 |      w |    2
        1 |          3 |      r |    3
        1 |          3 |      w |    4
        1 |          4 |      w |    4
        2 |          2 |      r |    2
        2 |          2 |      w |    3

Я хотел бы дополнить эти данные еще парой полей с именами «first_time» и «first_time_project», которые в совокупности идентифицируют первый раз, когда какое-либо действие было замечено этим человеком, и первый раз, когда разработчик увидел какое-либо действие над проектом. В итоге данные должны выглядеть так:

person_id | project_id | action | time | first_time | first_time_project
------------------------------------------------------------------------
        1 |          1 |      w |    1 |          1 |                  1
        1 |          2 |      w |    2 |          1 |                  2
        1 |          3 |      w |    2 |          1 |                  2
        1 |          3 |      r |    3 |          1 |                  2
        1 |          3 |      w |    4 |          1 |                  2
        1 |          4 |      w |    4 |          1 |                  4
        2 |          2 |      r |    2 |          2 |                  2
        2 |          2 |      w |    3 |          2 |                  2

Мой наивный способ сделать это написать пару циклов:

for (pid in unique(data$person_id)) {
    data[data$pid==pid, "first_time"] = min(data[data$pid==pid, "time"])
    for (projid in unique(data[data$pid==pid, "project_id"])) {
        data[data$pid==pid & data$project_id==projid, "first_time_project"] = min(data[data$pid==pid & data$project_id==projid, "time"]
    }
}

Теперь не нужно быть гением, чтобы понять, что это будет очень медленно с двойными вложенными циклами. Однако я не могу найти способ справиться с этим в R. Я как бы эмулирую группу по опции для SQL. Я знаю, что это может помочь, но я не могу понять, как сделать несколько срезов.

Любые подсказки о том, как превратить мой код из ледяного медленного во что-то немного более быстрое? Я был бы счастлив с улиткой прямо сейчас.


person Pridkett    schedule 15.02.2011    source источник
comment
Не могли бы вы более четко сформулировать вопросы? Насколько я понимаю, first_time — это минимальное время, когда человек совершил какое-либо действие, верно? Если это так, то ваша таблица результатов не имеет смысла. Человек 2 ничего не сделал за время 1.   -  person Maiasaura    schedule 15.02.2011
comment
Вы правы, у меня была опечатка во вставленных данных. Я исправил это и немного прояснил, как значения должны быть агрегированы. Спасибо за улов!   -  person Pridkett    schedule 15.02.2011


Ответы (5)


Попробуйте ave :

transform(data, 
   first_time = ave(time, person_id, FUN = min),
   first_time_project = ave(time, person_id, project_id, drop = TRUE, FUN = min)
)
person G. Grothendieck    schedule 15.02.2011
comment
Это именно то, что я ищу. Время выполнения сократилось с 1123 до 0,427 секунды. Спасибо за ускорение 2630x. Я все еще оцениваю эффективность предложений plyr, но ваше, безусловно, кажется ОЧЕНЬ многообещающим. - person Pridkett; 15.02.2011

Комбинация plyr и transform() Хэдли является мощной. Если я правильно понял ваш вопрос, то:

foo <- ddply(foo, .(person_id), transform, first_time=min(time))
foo <- ddply(foo, .(person_id, project_id), transform, 
  first_time_project=min(time))
person kohske    schedule 15.02.2011
comment
Очень полезно и чисто. В будущем мне придется еще немного изучить plyr. Этот код занял 65 секунд, что составляет 17-кратное ускорение. Отлично, но не так хорошо, как принятое решение от Г. Гротендика. С другой стороны, вы познакомили меня с plyr. - person Pridkett; 15.02.2011

Если вам нужна скорость, то data.table — то, что вам нужно.

library(data.table)
DT <- data.table(foo)
DT[, first_time := min(time), by = person_id]
DT[, first_time_project := min(time), by = list(person_id, project_id)]
person mnel    schedule 11.09.2012
comment
Можно ли выполнить вложенную агрегацию напрямую, не требуя промежуточной переменной? - person smci; 15.04.2015

Быстрое и грязное решение без петель

library(plyr)


# function to get first time by any person/project
fp <- function(dat) 
{
dat$first_time=min(dat$time)
ftp <- function(d) { d$first_time_project=min(d$time); return (d) }
dat=ddply(dat, .(project_id), ftp)
return (dat)
}


#this single call should give you the result you want
result=ddply(data, .(person_id), fp) 
person Maiasaura    schedule 15.02.2011
comment
plyr кажется немного волшебным. Ваше решение завершилось за 54 секунды, что в 21 раз больше. Вы также немного научили меня тому, как добавлять произвольные вызовы функций в plyr. Аккуратный. Спасибо! - person Pridkett; 15.02.2011

Быстрый способ, о котором я могу думать:

foo <- data.frame(
       person_id=rep(1:5,each=6),
       project_id=sample(1:5,30,T),
       time=sample(1:30))

first_time <- aggregate(foo$time, list(foo$person_id), min)

foo$first_time <- first_time[ match(foo$person_id,first_time[,1]),2]

bar <- subset(foo, time==first_time)

foo$first_time_project <- bar$project_id[match(foo$person_id, bar$person_id)]
person Sacha Epskamp    schedule 15.02.2011
comment
Это близко, но это не совсем так, как я надеялся. Он точно вычисляет first_time, но не вычисляет first_time_project так, как хотелось бы. Вместо этого полезно установить для first_time_project значение project_id, которое имеет наименьшее время для каждого person_id. Я искал, чтобы first_time_project был минимумом для каждой комбинации или person_id, project_id. - person Pridkett; 15.02.2011