【问题标题】:efficient way of selecting rows with a minimum time spacing between dates while grouping在分组时选择日期之间时间间隔最小的行的有效方法
【发布时间】:2020-02-21 17:40:56
【问题描述】:

我想选择具有日期的数据行,使得日期的最小时差为 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

我想通过patientnumsermed 保留至少相差3 个月的date。我不能简单地使用连续的差异。预期结果是:

   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-052020-01-082020-01-20

然后我删除2020-03-18,这是在2020-03-15 3 天之后。 这是我使用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 的解决方案,@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

@Ben 的滚动连接解决方​​案当然是最快的。我的还不错,而且@astrofunkswag 的解决方案超级慢,因为我猜是累积总和。

【问题讨论】:

    标签: r dataframe dplyr data.table


    【解决方案1】:

    使用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
    

    【讨论】:

    • 谢谢,这正是我想要写的。虽然它没有提供与我相同的输出(请参阅我的编辑),我不知道为什么。我猜我的脚本会出错/删除太多
    • @denis - 感谢更新和基准测试。如果我的方法使用date - 89L 而不是date - 90L,我认为@chinsoon12 和我会得到相同的结果。我检查了一下,然后两者都有 9929 obs。不同之处在于一名患者的间隔正好是 90 天(#5092 使用相同的种子)。
    • 是的,我认为你是对的。我不明白我的解决方案出了什么问题。
    • 在你的函数的第二行你想要df而不是dt吗? setkeyv(df,c("numsermed","patient","date"))
    【解决方案2】:

    这是dplyrpurrr 的解决方案。我使用了 2 个辅助函数,一个用于计算月差,一个用于计算达到阈值时重置的累积总和,归功于 this post

    我用滞后日期值计算月差,但您想包括第一个日期值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
    

    【讨论】:

    • 在更大的数据集上进行测试时,您的解决方案出奇地保守,它似乎没有删除足够多的行(请参阅我的编辑)。我还没有时间深入研究原因。不过谢谢你的提议
    【解决方案3】:

    使用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
    

    如果您有许多组耐心和数字,Ben 使用滚动连接的解决方案会更快。还有另一种通过链接对滚动连接进行编码的方法:

    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)]]
    

    数据:

    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")]
    

    【讨论】:

    • 很好,谢谢。不知道findInterval这个功能,很有用。
    猜你喜欢
    • 2019-05-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-08-02
    • 2011-01-16
    • 2012-05-04
    • 2022-01-18
    相关资源
    最近更新 更多