эффективный способ выбора строк с минимальным интервалом времени между датами при группировании - PullRequest
3 голосов
/ 21 февраля 2020

Я хочу выбрать строки данных с такими датами, чтобы минимальная разница во времени составляла 3 месяца. Вот пример:

    patient numsermed       date
 1:       1   numser1 2020-01-08
 2:       2   numser2 2015-01-02
 3:       2   numser2 2019-12-12
 4:       2   numser2 2020-01-05
 5:       2   numser2 2020-01-08
 6:       2   numser2 2020-01-20
 7:       2   numser2 2020-03-15
 8:       2   numser2 2020-03-18
 9:       2   numser3 2020-03-13
10:       2   numser3 2020-03-18
11:       3   numser3 2020-01-22
12:       4   numser4 2018-01-02

Я хочу, чтобы patient и numsermed сохранили date с разницей не менее 3 месяцев. Я не могу просто использовать последовательные различия. Ожидаемый результат:

   patient numsermed       date
1:       1   numser1 2020-01-08
2:       2   numser2 2015-01-02
3:       2   numser2 2019-12-12
4:       2   numser2 2020-03-15
5:       2   numser3 2020-03-13
6:       3   numser3 2020-01-22
7:       4   numser4 2018-01-02

Здесь, для numsermed2 и пациента 2, после 2019-12-12, следующая дата, через 3 месяца как минимум, 2020-03-15, которую я сохраняю. Таким образом я удаляю 2020-01-05, 2020-01-08, 2020-01-20.

Затем я удаляю 2020-03-18, то есть через 3 дня после 2020-03-15. Вот мое решение с data.table:

library(data.table)
library(lubridate)

setkeyv(test,c("numsermed","patient","date"))
test[,indx := .I - .I[1]+1,by = .(numsermed,patient)]

max(test[,.N,by = .(numsermed,patient)]$N)
Nmax <- max(test[,.N,by = .(numsermed,patient)]$N)
test[,supp := 0]

for(i in 1:Nmax){
  test[N>1 ,supp := ifelse(i < indx & date < date[i] + 90,1,0),
       by = .(numsermed,patient)]
  test <- test2[supp != 1  ]
  test[,indx := .I - .I[1]+1,by = .(numsermed,patient)]
}

Идея состоит в том, чтобы для каждой строки проверить условие и затем выполнить подмножество. Кажется, это работает, но для таблицы с миллионами строк это довольно медленно (несколько часов). Я уверен, что есть эффективный способ с полуэквивалентным или скользящим объединением в data.table, но мне не удалось его написать. Может ли кто-нибудь придумать более эффективное решение? dplyr решения, конечно, тоже приветствуются.

Данные:

library(data.table)
library(lubridate)  test<-setDT(list(patient=c(1:3,2),numsermed=c(paste0("numser",1:3),"numser2"),date=as_date(c("2020-01-08","2020-01-20","2020-01-22","2019-12-12"))))
    test<-rbind(test,data.table(patient=4,numsermed="numser4",date=as_date("2018-01-02")))
    test<-rbind(test,data.table(patient=2,numsermed="numser2",date=as_date("2015-01-02")))
    test<-rbind(test,data.table(patient=2,numsermed="numser2",date=as_date("2020-03-15")))
    test<-rbind(test,data.table(patient=2,numsermed="numser2",date=as_date("2020-01-05")))
    test<-rbind(test,data.table(patient=2,numsermed="numser2",date=as_date("2020-01-08")))
    test<-rbind(test,data.table(patient=2,numsermed="numser3",date=as_date("2020-03-13")))
    test<-rbind(test,data.table(patient=2,numsermed="numser3",date=as_date("2020-03-18")))
    test<-rbind(test,data.table(patient=2,numsermed="numser2",date=as_date("2020-03-18")))

Редактировать

Я предлагаю сравнить предложенное решение, @Ben ' Решение s, @ chinsoon12 и @astrofunkswag.

Вот тестовые данные:

library(data.table)
library(lubridate)
library(magrittr)

set.seed(1234)
origin <- "1970-01-01"
dt <- data.table(numsermed = sample(paste0("numsermed",1:30),10000,replace = T))
dt[,patient := sample(1:10000,.N,replace = T),by = numsermed]
dt[,date := sample((dmy("01.01.2019") %>% as.numeric()):(dmy("01.01.2020") %>% as.numeric()),.N),by = .(patient)]

и вот 4 функции, включая мою:

ben = function(dt){
  dt[, c("idx", "date2") := list(.I, date - 90L)]
  dt_final <- unique(dt[dt, on = c(patient = "patient", numsermed = "numsermed", date = "date2"), 
                            roll = -Inf][order(i.date)], by = "idx")
  setorderv(dt_final, c("patient", "numsermed", "i.date"))
  return(dt_final[,.(patient,numsermed,date = i.date)])
}


chinson = function(dt){
  dt[, d := as.integer(date)]
  setkey(dt,date)
  return( dt[dt[, g := findInterval(d, seq(d[1L], d[.N]+90L, by=90L)), .(patient, numsermed)][,
        .I[1L], .(patient, numsermed, g)]$V1][,.(patient,numsermed,date)])
}

sum_reset_at <- function(thresh) {
  function(x) {
    accumulate(x, ~if_else(.x>=thresh, .y, .x+.y))
  }  
}

mon_diff <- function(d1, d2){
  12 * as.numeric((as.yearmon(d1) - as.yearmon(d2)))
}
library(tidyverse); library(zoo)

astrofun = function(dt){
 return(
    dt %>% 
     group_by(patient, numsermed) %>% 
     mutate(diff1 = mon_diff(date, lag(date)),
            diff1 = if_else(is.na(diff1), 300, diff1)) %>% 
     mutate(diff2 = sum_reset_at(3)(diff1)) %>% 
     filter(diff2 >= 3) %>% 
     select(-contains('diff'))
 ) 
}

denis = function(dt){
  df <- copy(dt)
  setkeyv(dt,c("numsermed","patient","date"))
  df[,indx := .I - .I[1]+1,by = .(numsermed,patient)]

  df[,N := .N,by = .(numsermed,patient)]
  Nmax <- max(df[,N])
  df[,supp := 0]

  for(i in 1:Nmax){
    df[N>1 ,supp := ifelse(i < indx & date < date[i] + 90,1,0),
         by = .(numsermed,patient)]
    df <- df[supp != 1  ]
    df[,indx := .I - .I[1]+1,by = .(numsermed,patient)]
  }
  return(df[,.(patient,numsermed,date)])
}

Во-первых, ни один из них не дает одинакового результата! denis(dt) вывод 9833 строк, ben(dt) 9928, chinson(dt) 9929 и решение @astrofunkswag astrofun(dt) вывод 9990 строк. Я не уверен, почему это не дает тот же результат, ни то, что решение является хорошим (я бы сказал, что мое просто претенциозно, но я даже не уверен).

Затем сравнительный анализ для сравнения эффективности ,

library(microbenchmark)
microbenchmark(ben(dt),
               chinson(dt),
               astrofun(dt),
               denis(dt),times = 10)


Unit: milliseconds
         expr       min        lq       mean    median        uq       max neval
      ben(dt)   17.3841   19.8321   20.88349   20.9609   21.8815   23.5125    10
  chinson(dt)  230.8868  232.6298  275.16637  236.8482  239.0144  544.2292    10
 astrofun(dt) 4460.2159 4565.9120 4795.98600 4631.3251 5007.8055 5687.7717    10
    denis(dt)   68.0480   68.4170   88.88490   80.9636   90.0514  142.9553    10

@ Решение Бена с подвижным соединением, конечно, самое быстрое. Мое не так уж плохо, и решение @astrofunkswag очень медленное из-за накопленной суммы, я думаю.

Ответы [ 3 ]

2 голосов
/ 21 февраля 2020

С data.table вы можете попробовать следующее. Это потребует создания второго свидания за 90 дней до этого, а затем повторного объединения.

library(data.table)

setDT(test[, c("idx", "date2") := list(.I, date - 90L)]) 
test_final <- unique(test[test, on = c(patient = "patient", numsermed = "numsermed", date = "date2"), 
                          roll = -Inf][order(i.date)], by = "idx")
setorderv(test_final, c("patient", "numsermed", "i.date"))
test_final

Вывод

(i.date имеет желаемую конечную дату)

   patient numsermed       date idx      date2     i.date i.idx
1:       1   numser1 2019-10-10   1 2019-10-10 2020-01-08     1
2:       2   numser2 2014-10-04   6 2014-10-04 2015-01-02     6
3:       2   numser2 2019-09-13   4 2019-09-13 2019-12-12     4
4:       2   numser2 2019-12-16   8 2019-10-07 2020-03-15     7
5:       2   numser3 2019-12-14  10 2019-12-14 2020-03-13    10
6:       3   numser3 2019-10-24   3 2019-10-24 2020-01-22     3
7:       4   numser4 2017-10-04   5 2017-10-04 2018-01-02     5
1 голос
/ 22 февраля 2020

Еще один вариант использования findInterval для группировки:

library(data.table)
DT[, d := as.integer(date)]
DT[DT[, g := findInterval(d, seq(d[1L], d[.N]+90L, by=90L)), .(patient, numsermed)][,
    .I[1L], .(patient, numsermed, g)]$V1]

вывод:

   patient numsermed       date     d  g
1:       1   numser1 2020-01-08 18269  1
2:       2   numser2 2015-01-02 16437  1
3:       2   numser2 2019-12-12 18242 21
4:       2   numser2 2020-03-15 18336 22
5:       2   numser3 2020-03-13 18334  1
6:       3   numser3 2020-01-22 18283  1
7:       4   numser4 2018-01-02 17533  1

Если у вас много групп пациентов и чисел, решение Бена с использованием скользящего соединения будет быстрее , И еще один способ кодирования скользящего соединения с помощью цепочки:

DT[, .(patient, numsermed, date=date+90L)][
    DT, on=.NATURAL, roll=-Inf, .(patient, numsermed, x.date, i.date)][, 
        .(date=i.date[1L]), .(patient, numsermed, x.date)][, 
            x.date := NULL][]

Или, более кратко:

DT[, c("rn", "qtrago") := .(.I, date - 90L)]
DT[DT[DT, on=.(patient, numsermed, date=qtrago), roll=-Inf, unique(rn)]]

data:

library(data.table)
DT <- fread("patient numsermed       date
1   numser1 2020-01-08
2   numser2 2015-01-02
2   numser2 2019-12-12
2   numser2 2020-01-05
2   numser2 2020-01-08
2   numser2 2020-01-20
2   numser2 2020-03-15
2   numser2 2020-03-18
2   numser3 2020-03-13
2   numser3 2020-03-18
3   numser3 2020-01-22
4   numser4 2018-01-02")
DT[, date := as.IDate(date, format="%Y-%m-%d")]
1 голос
/ 21 февраля 2020

Вот решение с dplyr и purrr. Я использую 2 вспомогательные функции, одну для расчета разницы в месяцах и одну для расчета кумулятивной суммы, которая сбрасывается при достижении порога, кредит этой записи .

Я рассчитываю разницу в месяце с запаздывающее значение даты, но вы хотите включить первое, которое будет NA. Одна странная вещь заключается в том, что для включения NA самым простым для меня было преобразование NA в некоторое значение 3 или выше. Я произвольно сделал это 300. Вы, вероятно, можете изменить функцию sum_reset_at, чтобы обрабатывать NA так, как вы хотите. Возможно, вы также захотите сжать код каким-то образом, так как я делаю несколько вызовов mutate, а затем отменяю выбор этих столбцов, но я делал все это отдельными строками, чтобы было более понятно, что происходит. Я думаю, что это функциональное решение для программирования будет быстрее, но я не тестировал его на большом наборе данных по сравнению с вашим текущим решением.

test <- test %>% arrange(patient, numsermed, date)


library(tidyverse); library(zoo)

mon_diff <- function(d1, d2){
  12 * as.numeric((as.yearmon(d1) - as.yearmon(d2)))
}

sum_reset_at <- function(thresh) {
  function(x) {
    accumulate(x, ~if_else(.x>=thresh, .y, .x+.y))
  }  
}

test %>% 
  group_by(patient, numsermed) %>% 
  mutate(diff1 = mon_diff(date, lag(date)),
         diff1 = if_else(is.na(diff1), 300, diff1)) %>% 
  mutate(diff2 = sum_reset_at(3)(diff1)) %>% 
  filter(diff2 >= 3) %>% 
  select(-contains('diff'))


test
    <dbl> <chr>     <date>    
1       1 numser1   2020-01-08
2       2 numser2   2015-01-02
3       2 numser2   2019-12-12
4       2 numser2   2020-03-15
5       2 numser3   2020-03-13
6       3 numser3   2020-01-22
7       4 numser4   2018-01-02
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...