【问题标题】:Date Interval removal日期间隔删除
【发布时间】:2016-06-09 13:38:49
【问题描述】:

我的数据看起来像

ID  CLM_ID  Date1   Date2
1   718182  1/1/2014    1/17/2014
1   718184  1/2/2014    1/8/2014
1   885236  1/15/2014   1/17/2014
1   885362  3/20/2014   3/21/2014
2   589963  3/18/2015   3/22/2015
2   589999  2/27/2015   5/9/2015
2   594226  4/11/2015   4/17/2015
2   689959  5/10/2015   6/10/2015
3   656696  5/1/2016    5/5/2016
3   669625  5/6/2016    5/22/2016
4   777777  2/21/2015   3/4/2015
4   778952  2/1/2015    2/28/2015
4   778965  3/1/2015    3/22/2015

我正在解决两个不同的问题。第一个在上一篇关于如何汇总日期的帖子中得到了回答(Date roll-up in R),第二个现在是我有间隔在间隔内,我正试图摆脱它们。所以最终的结果应该是这样的

ID  CLM_ID  Date1   Date2
1   718182  1/1/2014    1/17/2014
1   885362  3/20/2014   3/21/2014
2   589999  2/27/2015   5/9/2015
3   656696  5/1/2016    5/22/2016
4   778952  2/1/2015    3/22/2015

现在我知道我必须首先通过日期汇总创建扩展间隔,但是我如何摆脱这些子间隔(我正在弥补间隔内的间隔的术语)?我也在寻找一种高效的解决方案,因为我实际上有 75,000 条记录要处理(即我试图避免迭代解决方案)。

【问题讨论】:

    标签: r date


    【解决方案1】:

    使用non-equicurrent development version of data.table, v1.9.7 连接,

    require(data.table) # v1.9.7+
    dt[dt, .(CLM_IDs = CLM_IDs[.N==1L]), on=.(ID, Date1<=Date1, Date2>=Date2), by=.EACHI]
    #    ID      Date1      Date2 CLM_ID
    # 1:  1 2014-01-01 2014-01-17 718182
    # 2:  1 2014-03-20 2014-03-21 885362
    # 3:  2 2015-02-27 2015-05-09 589999
    # 4:  2 2015-05-10 2015-06-10 689959
    # 5:  3 2016-05-01 2016-05-05 656696
    # 6:  3 2016-05-06 2016-05-22 669625
    # 7:  4 2015-02-21 2015-03-04 777777
    # 8:  4 2015-02-01 2015-02-28 778952
    # 9:  4 2015-03-01 2015-03-22 778965
    

    它的作用是,对于dt(方括号内的那一行)中的每一行,它会根据提供给on 参数的条件查找dt(在外部)中匹配的行.

    如果唯一匹配是自匹配,则返回匹配的行索引(因为条件也包括相等)。这是由CLM_IDs[.N == 1L] 完成的,其中.N 保存每个组的观察数。

    【讨论】:

    • 有什么方法可以在不先安装Rtools的情况下安装v1.9.7。我问是因为,我需要管理员权限才能安装Rtools,这目前无法安装 v1.9.7。如果没有,是否有使用 v1.9.6 执行上述操作的不同方法?谢谢。
    • @JosephWood,不幸的是还没有 :-(.. 我们计划在某个时候直接提供 Windows 二进制文件.. 我会问 Jan(另一个 data.table 贡献者)关于这个.. 因为他是在这些事情上做了很多工作。
    • @JosephWood 它在 TODO 列表中,一般在等待r-appveyor#29。您可以从appveyor build artifacts 获取 Windows 二进制文件并从 zip 安装它。不幸的是,由于 r-appveyor#69 二进制文件现在已经 7 天了,所以还不包括非 equi 连接和 .EACHI
    • @jangorecki,知道新版本何时完成并上传到 CRAN 吗?
    • @jangorecki,这很酷。顺便说一句,我想对您的团队使用 data.table 包所做的所有出色工作表示感谢。真是太棒了!!
    【解决方案2】:

    “我也在寻找一种高效的解决方案......(即我试图避免迭代解决方案)。”

    “你的假设是你的世界之窗。每隔一段时间擦掉它们,否则光线不会进来。” - 艾萨克·阿西莫夫

    下面是一个超快的base R 迭代解决方案。它几乎可以立即为非常大的数据帧返回正确的结果。 (它还会“汇总”数据,因此无需执行两种算法):

    MakeDFSubInt <- function(df, includeCost = FALSE) {
        ## Sorting the data frame to allow for fast
        ## creation of the "Contained" logical vector below
        tempDF <- df[order(df$ID, df$Date1, df$Date2), ] 
        UniIDs <- unique(tempDF$ID)
        Len <- length(UniIDs)
    
        ## Determine starting (i.e. "s") and ending (i.e. "e")
        ## points of the respective groups of IDs
        e <- which(diff(tempDF$ID)==1)
        s <- c(1L, e + 1L)
        dfLen <- nrow(tempDF)
        e <- c(e, dfLen)
    
        ## Converting dates to integers so that comparison
        ## will be faster. Internally dates are stored as
        ## integers, so this isn't a problem
        dte1 <- as.integer(tempDF$Date1)
        dte2 <- as.integer(tempDF$Date2)
    
        ## Building logical vector in order to quickly create sub-intervals
        Contained <- rep(FALSE, dfLen)
    
        BegTime <- Sys.time()  ## Included to measure time of for loop execution
    
        for (j in 1:Len) {
            Compare <- ifelse(dte2[s[j]] >= (dte1[s[j]+1L]+1L), max(dte2[s[j]], dte2[s[j]+1L]), dte2[s[j]+1L])
            for (x in (s[j]+1L):e[j]) {
                if (!Contained[x-1L]) {
                    Contained[x] <- dte2[x-1L] >= (dte1[x]-1L)
                } else {
                    Contained[x] <- Compare >= (dte1[x]-1L)
                }
    
                ## could use ifelse, but this construct is faster
                if (Contained[x]) {  
                    Compare <- max(Compare, dte2[x])
                } else {
                    Compare <- dte2[x]
                }
            }
        }
    
        EndTime <- Sys.time()
        TotTime <- EndTime - BegTime
        if (printTime) {print(paste(c("for loop execution time was: ", format(TotTime)), collapse = ""))}
    
        ## identify sub-intervals
        nGrps <- which(!Contained)
    
        ## Create New fields for our new DF
        ID <- tempDF$ID[nGrps]
        CLM_ID <- tempDF$CLM_ID[nGrps]
        Date1 <- tempDF$Date1[nGrps]
        nGrps <- c(nGrps, dfLen+1L)
    
        ## as.Date is converting numbers to dates. 
        ## N.B. This only works if origin is supplied
        Date2 <- as.Date(vapply(1L:(length(nGrps) - 1L), function(x) {
                         max(dte2[nGrps[x]:(nGrps[x+1L]-1L)])}, 1L), origin = "1970-01-01")
    
        ## in a related question the OP had, "Cost" was
        ## included to show how the algorithm would handle
        ## generic summary information
        if (includeCost) {
            myCost <- tempDF$Cost
            Cost <-  vapply(1L:(length(nGrps) - 1L), function(x) sum(myCost[nGrps[x]:(nGrps[x+1L]-1L)]), 100.01)
            NewDf <- data.frame(ID,CLM_ID,Date1,Date2,Cost)
        } else {
            NewDf <- data.frame(ID,CLM_ID,Date1,Date2)
        }
    
        NewDf
    }
    

    对于问题中给出的示例,我们有:

    ID <- c(rep(1,4),rep(2,4),rep(3,2),rep(4,3))
    CLM_ID <- c(718182, 718184, 885236, 885362, 589963, 589999, 594226, 689959, 656696, 669625, 777777, 778952, 778965)
    Date1 <- c("1/1/2014","1/2/2014","1/15/2014","3/20/2014","3/18/2015","2/27/2015","4/11/2015","5/10/2015","5/1/2016","5/6/2016","2/21/2015","2/1/2015","3/1/2015")
    Date2 <- c("1/17/2014","1/8/2014","1/17/2014","3/21/2014","3/22/2015","5/9/2015","4/17/2015","6/10/2015","5/5/2016","5/22/2016","3/4/2015","2/28/2015","3/22/2015")
    myDF <- data.frame(ID, CLM_ID, Date1, Date2)
    myDF$Date1 <- as.Date(myDF$Date1, format = "%m/%d/%Y")
    myDF$Date2 <- as.Date(myDF$Date2, format = "%m/%d/%Y")
    
    MakeDFSubInt(myDF)
    ID CLM_ID      Date1      Date2
    1  1 718182 2014-01-01 2014-01-17
    2  1 885362 2014-03-20 2014-03-21
    3  2 589999 2015-02-27 2015-06-10
    4  3 656696 2016-05-01 2016-05-22
    5  4 778952 2015-02-01 2015-03-22
    

    从 OP 发布的 similar question 中,我们可以添加一个 Cost 字段,以显示我们将如何进行此设置的计算。

    set.seed(7777)
    myDF$Cost <- round(rnorm(13, 450, sd = 100),2)
    
    MakeDFSubInt(myDF,  includeCost = TRUE)
    ID   CLM_ID      Date1      Date2    Cost
    1  1 718182 2014-01-01 2014-01-17 1164.66
    2  1 885362 2014-03-20 2014-03-21  568.16
    3  2 589999 2015-02-27 2015-06-10 2019.16
    4  3 656696 2016-05-01 2016-05-22  990.14
    5  4 778952 2015-02-01 2015-03-22 1578.68
    

    这个算法的扩展性很好。对于 OP 正在寻找的大小的数据帧,返回请求的 DF 几乎立即返回,对于非常大的数据帧,它只需几秒钟即可返回。

    首先我们构建一个函数,该函数将生成一个包含n 行的随机数据框。

    MakeRandomDF <- function(n) {
        set.seed(109)
    
        CLM_Size <- ifelse(n < 10^6, 10^6, 10^(ceiling(log10(n))))
        numYears <- trunc((6/425000)*n + 5)
        StrtYear <- ifelse(numYears > 16, 2000, 2016 - numYears)
        numYears <- ifelse(numYears > 16, 16, numYears)
    
        IDs <- sort(sample(trunc(n/100), n, replace = TRUE))
        CLM_IDs <- sample(CLM_Size, n)
        StrtDate <- as.Date(paste(c(as.character(StrtYear),"-01-01"), collapse = ""))
        myPossibleDates <- StrtDate+(0:(numYears*365))  ## "numYears" years of data
        Date1 <- sample(myPossibleDates, n, replace = TRUE)
        Date2 <- Date1 + sample(1:100, n, replace = TRUE)
        Cost <- round(rnorm(n, 850, 100), 2)
    
        tempDF <- data.frame(IDs,CLM_IDs,Date1,Date2,Cost)
        tempDF$Date1 <- as.Date(tempDF$Date1, format = "%m/%d/%Y")
        tempDF$Date2 <- as.Date(tempDF$Date2, format = "%m/%d/%Y")
    
        tempDF
    }
    

    对于中等大小的 DF(即 75,000 行)

    TestDF <- MakeRandomDF(75000)
    system.time(test1 <- MakeDFSubInt(TestDF, includeCost = TRUE, printTime = TRUE))
    [1] "for loop execution time was: 0.06500006 secs"
      user  system elapsed 
      0.14    0.00    0.14 
    
    nrow(test1)
    [1] 7618
    
    head(test1)
      ID CLM_ID      Date1      Date2     Cost
    1  1 116944 2010-01-29 2010-01-30   799.90  ## The range of dates for 
    2  1 515993 2010-02-15 2011-10-12 20836.83  ## each row are disjoint
    3  1 408037 2011-12-13 2013-07-21 28149.26  ## as requested by the OP
    4  1  20591 2013-07-25 2014-03-11 10449.51
    5  1 338609 2014-04-24 2014-07-31  4219.48
    6  1 628983 2014-08-03 2014-09-11  2170.93
    


    对于非常大的 DF(即 > 500,000 行)

    TestDF2 <- MakeRandomDF(500000)
    system.time(test2 <- MakeDFSubInt(TestDF2, includeCost = TRUE, printTime = TRUE))
    [1] "for loop execution time was: 0.3679998 secs"
      user  system elapsed 
      1.19    0.03    1.21 
    
    nrow(test2)
    [1] 154839
    
    head(test2)
      ID CLM_ID      Date1      Date2    Cost
    1  1  71251 2004-04-19 2004-06-29 2715.69  ## The range of dates for 
    2  1 601676 2004-07-05 2004-09-23 2675.04  ## each row are disjoint
    3  1 794409 2004-12-28 2005-04-05 1760.63  ## as requested by the OP
    4  1 424671 2005-06-03 2005-08-20 1973.67
    5  1 390353 2005-09-16 2005-11-06  785.81
    6  1 496611 2005-11-21 2005-11-24  904.09
    
    system.time(test3 <- MakeDFSubInt(TestDF3, includeCost = TRUE, printTime = TRUE))
    [1] "for loop execution time was: 0.6930001 secs"
      user  system elapsed 
      2.68    0.08    2.79      ## 1 million rows in under 3 seconds!!!
    
    nrow(test3)
    [1] 413668
    


    说明

    算法的主要部分是生成Contained 逻辑向量,用于确定连续日期的子区间。这个向量的生成依赖于数据帧的排序,首先是ID,其次是Date1,最后是Date2。我们首先定位每组 ID 的开始行和结束行。例如,通过 OP 提供的示例,我们有:

    myDF
       ID CLM_ID      Date1      Date2
    1   1 718182 2014-01-01 2014-01-17    ## <- 1   s[1]
    2   1 718184 2014-01-02 2014-01-08
    3   1 885236 2014-01-15 2014-01-17
    4   1 885362 2014-03-20 2014-03-21    ## <- 4   e[1]
    5   2 589963 2015-03-18 2015-03-22    ## <- 5   s[2]
    6   2 589999 2015-02-27 2015-05-09
    7   2 594226 2015-04-11 2015-04-17
    8   2 689959 2015-05-10 2015-06-10    ## <- 8   e[2]
    9   3 656696 2016-05-01 2016-05-05    ## <- 9   s[3]
    10  3 669625 2016-05-06 2016-05-22    ## <- 10  e[3]
    11  4 777777 2015-02-21 2015-03-04    ## <- 11  s[4]
    12  4 778952 2015-02-01 2015-02-28
    13  4 778965 2015-03-01 2015-03-22    ## <- 13  e[4]
    

    下面是生成se的代码。

    ## Determine starting (i.e. "s") and ending (i.e. "e")
    ## points of the respective groups of IDs
    e <- which(diff(tempDF$ID)==1)
    s <- c(1L, e + 1L)
    dfLen <- nrow(tempDF)
    e <- c(e, dfLen)
    
    s
    1  5  9   11
    
    e
    4  8  10  13
    

    现在,我们遍历每个组并开始填充逻辑向量 Contained。如果特定行的日期范围与其上方的日期范围重叠(或延续),我们将Contained 的特定索引设置为TRUE。这就是为什么每组中的第一行设置为FALSE,因为上面没有任何东西可以与之比较。当我们这样做时,我们正在更新最大的日期以与前进进行比较,因此Compare 变量。需要注意的是,Date2[n] &lt; Date2[n+1L] 不一定是真的,这就是为什么Compare &lt;- max(Compare, dte2[x]) 是连续的TRUEs。我们示例的结果如下所示。

       ID CLM_ID      Date1      Date2 Contained
    1   1 718182 2014-01-01 2014-01-17     FALSE
    2   1 718184 2014-01-02 2014-01-08      TRUE  ## These two rows are contained
    3   1 885236 2014-01-15 2014-01-17      TRUE  ## in the date range 1/1 - 1/17
    4   1 885362 2014-03-20 2014-03-21     FALSE  ## This row isn't
    6   2 589999 2015-02-27 2015-05-09     FALSE
    5   2 589963 2015-03-18 2015-03-22      TRUE
    7   2 594226 2015-04-11 2015-04-17      TRUE
    8   2 689959 2015-05-10 2015-06-10      TRUE  ## N.B. 5/10 is a continuance of 5/09
    9   3 656696 2016-05-01 2016-05-05     FALSE
    10  3 669625 2016-05-06 2016-05-22      TRUE
    12  4 778952 2015-02-01 2015-02-28     FALSE
    11  4 777777 2015-02-21 2015-03-04      TRUE
    13  4 778965 2015-03-01 2015-03-22      TRUE
    

    现在我们可以通过使用对应的FALSE 识别所有行来轻松识别“起始”行。在此之后,只需计算您感兴趣的任何内容(例如max(Date2)sum(Cost)),就可以轻而易举地找到每个连续的TRUEs 和Voila!

    【讨论】:

    • 感谢您的解释。除了system.time,还有没有办法在编程时知道循环的效率?
    • @akash87,关于 R 中的 for loops 存在很多误解。是的,它们可能很慢,但正如 Richie Cotton 指出的那样 here(也可以查看接受的答案)来自 Gavin Simpson 的页面),如果你有一个不平凡的任务(就像我们上面所做的那样),for 循环是要走的路(如果实施得当)。我强烈推荐阅读 this postthis post 以更好地理解 R 的循环结构。
    • @akash87,我已经修改了我的算法以返回执行 for loop 所需的时间。
    • @akash87,如果你真的想知道你的代码的特定部分的效率,你可以把那部分变成一个函数,然后使用R profiler
    【解决方案3】:

    这是一个不太漂亮的解决方案,将每一行与所有其他行的日期进行比较。我将 3015 年更正为 2015 年。但是,结果与您的预期不同。要么我误解了你的问题,要么你误读了数据。

    数据:

    dta <- structure(list(ID = c(1L, 1L, 1L, 1L, 2L, 2L, 2L, 2L, 3L, 3L, 4L, 4L, 4L), 
                          CLM_ID = c(718182L, 718184L, 885236L, 885362L, 589963L, 589999L, 594226L, 689959L, 656696L, 669625L, 777777L, 778952L, 778965L), 
                          Date1 = structure(c(1L, 3L, 2L, 9L, 8L, 6L, 10L, 12L, 11L, 13L, 5L, 4L, 7L), .Label = c("1/1/2014", "1/15/2014", "1/2/2014", "2/1/2015", "2/21/2015", "2/27/2015", "3/1/2015", "3/18/2015", "3/20/2014", "4/11/2015", "5/1/2016", "5/10/2015", "5/6/2016"), class = "factor"), 
                          Date2 = structure(c(1L, 2L, 1L, 4L, 5L, 10L, 7L, 11L, 9L, 8L, 6L, 3L, 5L), .Label = c("1/17/2014", "1/8/2014", "2/28/2015", "3/21/2014", "3/22/2015", "3/4/2015", "4/17/2015", "5/22/2016", "5/5/2016", "5/9/2015", "6/10/2015"), class = "factor")), 
                     .Names = c("ID", "CLM_ID", "Date1", "Date2"), class = "data.frame", 
                     row.names = c(NA, -13L))
    

    代码:

    dta$Date1 <- as.Date(dta$Date1, format = "%m/%d/%Y")
    dta$Date2 <- as.Date(dta$Date2, format = "%m/%d/%Y")
    
    # Boolean vector to memorize results
    keep <- logical(length = nrow(dta))
    for(i in 1:nrow(dta)) {
      match <- dta[dta$Date1 <= dta$Date1[i] & dta$Date2 >= dta$Date2[i], ]
      if(nrow(match) == 1) keep[i] <- TRUE
    }
    
    # Result
    dta[keep, ]
    

    【讨论】:

    • 感谢您的解决方案,但我试图避免使用 for 循环,因为实际数据集有 75,000 行长。
    • 那么你应该把它添加到帖子中。
    猜你喜欢
    • 2021-07-10
    • 1970-01-01
    • 1970-01-01
    • 2018-06-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多