Подсчет количества частичных перекрытий для интервалов диапазона дат

#r #performance #for-loop #data.table #apply

#r #Производительность #для цикла #данные.таблица #применить

Вопрос:

Мне нужно запустить цикл над фреймом данных с ~ 150 Тыс. строк. Однако циклу необходимо проверять каждую строку и проверять условие, которое проверяет каждую другую строку в наборе данных. Мой код отлично работает для игрушечного набора данных, он выдает правильное значение, но работает слишком медленно для моего реального набора данных. Я позволил ему работать в течение нескольких часов, и он все еще не закончился. итак, я надеюсь, что у кого-то есть идея получше, как подойти к этому.

 #R version 3.5.1 Windows 64-bit

#Example dataset
my_df <- data.frame("PERSON" = c("A","A","A","B","A","A","B"),
                    "DATE_START" = c("2019-01-15","2019-01-10","2019-01-20","2019-01-19","2018-12-20","2018-03-03","2019-05-01"),
                    "DATE_FINISH" = c("2019-01-30","2019-01-18","2019-02-05","2019-01-23","2019-02-10","2018-04-01","2019-06-06")
                    )
#Each row is a task that the assigned person is working on
my_df
   PERSON   DATE_START DATE_FINISH
1       A   2019-01-15  2019-01-30
2       A   2019-01-10  2019-01-18
3       A   2019-01-20  2019-02-05
4       B   2019-01-19  2019-01-23
5       A   2018-12-20  2019-02-10
6       A   2018-03-03  2018-04-01
7       B   2019-05-01  2019-06-06
  

Что я хочу знать ДЛЯ строки 1, сколько других задач у пользователя A перекрывается между датами его начала и окончания? (включая строку, в которой он включен)

Итак, ответ, который я ищу, это

    PERSON   DATE_START DATE_FINISH  NUMBER_OF_TASKS
1       A   2019-01-15  2019-01-30  4
2       A   2019-01-10  2019-01-18  3
3       A   2019-01-20  2019-02-05  3
4       B   2019-01-19  2019-01-23  1
5       A   2018-12-20  2019-02-10  4
6       A   2018-03-03  2018-04-01  1
7       B   2019-05-01  2019-06-06  1
  

Таким образом, это в основном говорит о том, что для строки 1 у человека A было 4 открытых задания

Я попробовал создать элемент списка для каждой строки, которая включает диапазон дат в виде числовых значений, а затем, чтобы проверить, есть ли перекрытие, я использовал оператор %in% для сравнения не перечисленных диапазонов

Я сделал нечто подобное, используя функцию lapply (здесь не показана), но та же проблема, выполнение которой занимает целую вечность.

 ##This is what I currently have

temp_list <- list()
num_open_tasks <- c()
open_work_cc <- c()

##Create a list of length = nrow(my_df)
##Each element in the list is a range of dates coerced to numeric
for(i in 1:nrow(my_df)){
  temp_list[[i]] <- as.numeric(my_df$DATE_START[i]) : 
                    as.numeric(my_df$DATE_FINISH[i])
}


for(i in 1:nrow(my_df)){
  for(j in 1:nrow(my_df)){

##If elements from the temp_list overlap by 5 days, the overlap = 5
##I'm just checking if the overlap is greater than 0 (is there any overlap at all)
##And if the tasks belongs to the same person or not    
open_work_cc[j] <- ifelse(sum(unlist(temp_list[[i]]) %in% 
                              unlist(temp_list[[j]])) > 0 amp;
                              my_df$PERSON[i] == my_df$PERSON[j] 
                              ,1,0
                           )
open_work_cc_total <- sum(open_work_cc)

  }
  num_open_tasks[i] <- open_work_cc_total

}
my_df <- cbind(my_df, num_open_tasks)
  

Этот метод возвращает нужный мне столбец, заполненный правильными значениями.
Но я полагаю, что существует более элегантный и значительно более быстрый метод, использующий некоторую форму разделения / применения / объединения. Приветствуется любая помощь, спасибо

Комментарии:

1. Если вы напишете точно такую же логику на языке Julia — у вас не будет этой проблемы со скоростью.

2. … и если вы попробуете as.numeric(my_df$DATE_START[i]) как есть, у вас возникнет много логических проблем (т.е. множество NA ).

3. Обучение Julia находится на пути к успеху! Спасибо!

Ответ №1:

foverlaps data.table вероятно, это будет самый быстрый подход в R. Я думаю, что следующий код делает то, что вы хотите:

 library(data.table)
setDT(my_df)

my_df[, DATE_START_N:=as.numeric(as.Date(DATE_START))]
my_df[, DATE_FINISH_N:=as.numeric(as.Date(DATE_FINISH))]

setkey(my_df, PERSON, DATE_START_N,DATE_FINISH_N)

my_df[,NUMBER_OF_TASKS:=foverlaps(my_df,my_df,which=TRUE)[,.N,by=xid]$N]
my_df
  

Для большей ясности: foverlaps(my_df,my_df,which=TRUE) выполняется самосоединение для диапазона дат внутри PERSON (объединение определяется setkey ). Обратите внимание, что аргументом по умолчанию для type объединения интервалов для foverlaps является "any" , который является частичным совпадением по интервалам: т. Е. То, что вы хотите здесь.

Указание which=TRUE просто предоставит индексы совпадений в x и y (а не фактические объединенные данные, которые здесь не нужны). Вызов foverlaps возвращает объект класса data.table , который затем немедленно агрегируется для получения количества строк в каждой группе, определенной xid (которые являются просто строками my_df ), с использованием вызова функции bracket [,.N,by=xid] . Эти подсчеты извлекаются в вектор с $N и присваиваются новому столбцу NUMBER_OF_TASKS в my_df .

Комментарии:

1. data.table::foverlaps это была моя первая мысль, и, похоже, это правильный способ атаковать ее здесь (особенно с as.Date работающими на нас :-).

2. Это сработало как по волшебству! Для выполнения всего этого потребовалось около 2 секунд. Большое вам спасибо, показывает мне, что мне нужно изучить намного больше данных.таблица

3. К сожалению, я недостаточно разбираюсь в магии data.table, но это спасло мне жизнь, спасибо! Мне не нужно было группировать их по персоналиям, и поэтому я просто опустил бит person, а мне это было нужно с точностью до минуты (и мои данные в формате ymd_hms): « setkey(my_df, DATE_START, DATE_FINISH) « Для меня это сработало как чудо, суммируя количество одновременных входов в базу данных 🙂