Подсчитать строки (условно) в течение указанного периода времени по группам в R

Я работаю с данными, сгенерированными пользователями, и хочу подсчитать количество строк / действий, то есть звонков каждого пользователя, сделанных в течение определенного периода времени. Вот макет фрейма данных, похожий на тот, с которым я работаю:

library(ids)#for generating the UserID variable
library(wakefield)#for generating the Status variable
library(dplyr)

set.seed(123)
UserID<-random_id(n=10, bytes = 5)
DateTime<-seq.POSIXt(from = as.POSIXct("2020-08-01 01:00:00", tz = Sys.timezone()), length.out = 70, by = "15 mins")
df<-cbind(UserID,DateTime)
df<-as.data.frame(df)
df$Status<-r_sample_factor(x = c("Answered", "Abandoned", "Engaged"), n=70)
df$DateTime<-seq.POSIXt(from = as.POSIXct("2020-08-01 01:00:00", tz = Sys.timezone()), 
                        length.out = 70, by = "15 mins")#re-doing this again as it annoyingly converts to numeric each time 

df<-df%>%arrange(UserID,DateTime)
head(df)


      #UserID            DateTime    Status
#1 0a5f3a2a8b 2020-08-01 02:00:00   Engaged
#2 0a5f3a2a8b 2020-08-01 04:30:00   Engaged
#3 0a5f3a2a8b 2020-08-01 07:00:00   Engaged
#4 0a5f3a2a8b 2020-08-01 09:30:00   Engaged
#5 0a5f3a2a8b 2020-08-01 12:00:00   Engaged
#6 0a5f3a2a8b 2020-08-01 14:30:00 Abandoned

Я хочу подсчитать количество вызовов UserID в течение 5-часового периода с двумя другими условиями:

  1. Если в течение 5-часового периода с момента последнего звонка, совершенного пользователем, не было другого звонка, то это будет единичная попытка.
  2. Если у пользователя есть N вызовов в течение 5-часового периода, пока на них не ответят, это считается успешной попыткой. В противном случае он будет признан неудачным.

Вот чего я пытаюсь достичь:

UserId          OrigTime       LastTime          Calls  Status       Successful
0a5f3a2a8b  2020-08-01 02:00:00 2020-08-01 07:00:00 3   Engaged          No
16db61d2bc  2020-08-01 03:15:00 2020-08-01 03:15:00 1   Answered         Yes
6355f7700d  2020-08-01 01:00:00 2020-08-01 06:00:00 3   Answered         Yes
9b9fab9789  2020-08-01 04:15:00 2020-08-01 09:15:00 3   Answered         Yes
...

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

Любые указатели в правильном направлении были бы замечательно. Я полагаю, что есть какое-то data.table или dplyr решение, но я раньше не занимался подобным образом, поэтому не знаю, с чего начать. Заранее большое спасибо :)

ИЗМЕНИТЬ

@Waldi предоставил решение, которое обеспечило почти то, что мне было нужно. Вот решение, которое пока работает лучше всего (с небольшими изменениями из ответа @Waldi): -

CondCount <- function(data,maxdelay){
  result <- list()
  row <- 0
  calls <- 0
  OrigTime <- NA
  n <- nrow(data)
  
  for (i in 1:n) {
    if (is.na(OrigTime)) {
      OrigTime <- data$DateTime[[i]]
      calls <- 0
    }
    calls = calls + 1
    if (data$Status[[i]] == "Answered" | difftime(data$DateTime[[i]],OrigTime,units='hours') > maxdelay | i==n) {
      row <- row + 1
      result[[row]] <- data.frame(OrigTime = OrigTime, LastTime = data$DateTime[[i]], calls = calls, Status = factor(data$Status[[i]],levels=c("Answered" ,"Abandoned" ,"Engaged","Unknown")), Successful = ifelse(data$Status[[i]]=="Answered",'Y','N')  )
      OrigTime <- NA
    }
  }
  dplyr::bind_rows(result)
}

df %>% arrange(UserID,DateTime) %>%
       split(.$UserID) %>%
       map(function(data) {CondCount(data,1) }) %>%
       bind_rows(.id="UserID") 


См. 2 шага, которые я написал до редактирования. На этот раз период составляет 1 час, а не 5 часов.

С решением @Waldi вот когда оно работает на моем реальном df (прошу прощения за цветовую кодировку, которую я использовал, если случайно есть какие-либо дальтоники SO-пользователи): -

Правильный результат

введите описание изображения здесь

и с решением @Waldi он даст вам следующее: -

введите описание изображения здесь

что правильно! Это то, к чему я стремлюсь. Однако я хочу проиллюстрировать два примера того, что происходит, когда я запускаю этот код, что дает нежелательный результат:

Неверный результат 1

введите описание изображения здесь

что дает вам это: -

введите описание изображения здесь

Это неверно. Это должно быть две строки, по одной для каждой попытки (каждая с окончательным статусом «Отказано»), а не одна строка, поскольку разница во времени между двумя последними строками превышает 60 минут.

Неправильный результат 2

введите описание изображения здесь

что дает вам это: -

введите описание изображения здесь

Это неверно. Это должно быть две строки, по одной для каждой попытки (первая строка со статусом "Вовлечена", вторая строка со статусом "ответили").

Я должен отдать должное @Waldi, потому что решение отлично работает для отвеченных звонков. Однако он не принимает во внимание другие типы статуса, например, "Отказано" и "Вовлечено". Возможно, для этих двух статусов недостаточно условий. Как всегда, мы будем благодарны за любую помощь!


person Robin Turkington    schedule 03.08.2020    source источник
comment
не могли бы вы dput два последних примера с ошибками: я думаю, что исправил код, но хотел бы его протестировать. Спасибо.   -  person Waldi    schedule 25.09.2020


Ответы (1)


Вы можете использовать purrr для разделения данных по пользователям и использовать простую функцию цикла for для реализации логики, которую вы ищете:

library(purrr)

CondCount <- function(data,maxdelay){
  result <- list()
  row <- 0
  calls <- 0
  OrigTime <- NA
  n <- nrow(data)
  
  for (i in 1:n) {
    if (is.na(OrigTime)) {
      OrigTime <- data$DateTime[[i]]
      calls <- 0
    }
    calls = calls + 1
    if (difftime(data$DateTime[[i]],OrigTime,units='hours') > maxdelay) {
      row <- row + 1
      result[[row]] <- data.frame(OrigTime = OrigTime, LastTime = data$DateTime[[i-1]], calls = calls, Status = factor(data$Status[[i-1]],levels=c("Answered" ,"Abandoned" ,"Engaged")), Successful = ifelse(data$Status[[i]]=="Answered",'Y','N')  )
      OrigTime <- data$DateTime[[i]]
    } 
    if ((data$Status[[i]] !="Engaged") | i == n) {
      row <- row + 1
      result[[row]] <- data.frame(OrigTime = OrigTime, LastTime = data$DateTime[[i]], calls = calls, Status = factor(data$Status[[i]],levels=c("Answered" ,"Abandoned" ,"Engaged")), Successful = ifelse(data$Status[[i]]=="Answered",'Y','N')  )
      OrigTime <- NA
    }
  } 
  dplyr::bind_rows(result)
}



df %>% arrange(UserID,DateTime) %>%
  split(.$UserID) %>%
  map(function(data) {CondCount(data,5) }) %>%
  bind_rows(.id="UserID")

       UserID            OrigTime            LastTime calls    Status Successful
1  022098d3cf 2020-08-01 03:15:00 2020-08-01 03:15:00     1  Answered          Y
2  022098d3cf 2020-08-01 05:45:00 2020-08-01 05:45:00     1  Answered          Y
3  022098d3cf 2020-08-01 08:15:00 2020-08-01 08:15:00     1 Abandoned          N
4  022098d3cf 2020-08-01 10:45:00 2020-08-01 10:45:00     1  Answered          Y
5  022098d3cf 2020-08-01 13:15:00 2020-08-01 13:15:00     1 Abandoned          N
6  022098d3cf 2020-08-01 15:45:00 2020-08-01 15:45:00     1 Abandoned          N
7  022098d3cf 2020-08-01 18:15:00 2020-08-01 18:15:00     1 Abandoned          N
8  18f13c3972 2020-08-01 01:15:00 2020-08-01 03:45:00     2 Abandoned          N
9  18f13c3972 2020-08-01 06:15:00 2020-08-01 06:15:00     1  Answered          Y
10 18f13c3972 2020-08-01 08:45:00 2020-08-01 13:45:00     3  Answered          Y

Если цикл должен быть очень быстрым, его можно легко преобразовать в Rcpp. .

NB: по какой-то причине set.seed (123) кажется недостаточным для получения воспроизводимых результатов.

person Waldi    schedule 06.08.2020
comment
Большое спасибо за ваше решение, это сработало! Я бы потратил слишком много времени на то, чтобы придумать собственное решение, которое работало бы так же хорошо, как это :) Я зачислю вам 50 очков репутации за награду, если это позволит мне через 21 час или около того! - person Robin Turkington; 06.08.2020
comment
Думаю, мне нужно немного поправить код. Я изменил порог с 5 часов на 1 час. Однако я заметил, что в некоторых записях между звонками, сделанными каждым UserID, была пара дней. Я думаю, он продолжал искать Status, который был Answered, вместо того, чтобы создавать строку для попытки, если не было другого вызова, сделанного в течение 1 часа после предыдущего вызова. Вот что у меня получилось: UserID OrigTime LastTime calls Status Successful 084272a5e6 2018-01-17 22:57:48 2018-01-19 20:28:32 2 Answered Y - person Robin Turkington; 02.09.2020
comment
Оказалось, что первый звонок из двух в моем примере выше был вовлечен. Итак, я должен был получить одну строку, в которой статус был задействован и успешно == N, и следующую строку, где на вызов был дан ответ и успешно == Y. Я думаю, что мне где-то нужен оператор else в коде, но что это похоже, мне нужна помощь с @Waldi - person Robin Turkington; 02.09.2020
comment
@Robin, я посмотрю на это сегодня вечером (CET) - person Waldi; 02.09.2020
comment
Спасибо, с нетерпением жду того, что вы придумали :) - person Robin Turkington; 02.09.2020
comment
Смотрите мою правку, некоторые условия не были проверены достаточно ... Сообщите мне, если теперь все в порядке - person Waldi; 03.09.2020
comment
Спасибо за правку, думаю, почти готово. Есть только одна проблема. Он не сохраняет переменную UserID и вместо этого заменяет каждый UserID на 1,2,3 ... Я создам правку с моим исходным вопросом, чтобы показать вам результат, - person Robin Turkington; 03.09.2020
comment
номер строки - 1,2,3,4, .. но столбец UserID должен сохранять исходный идентификатор, как в примере выше. - person Waldi; 03.09.2020
comment
не могли бы вы dput(head(realdata$UserID,10))? - person Waldi; 03.09.2020
comment
класс - это характер - person Robin Turkington; 03.09.2020
comment
спасибо и не могли бы вы dput(head(realdata$UserID,10)) - person Waldi; 03.09.2020
comment
Позвольте нам продолжить это обсуждение в чате. - person Robin Turkington; 03.09.2020