【问题标题】:R - Speeding up approximate date match. idata.frame?R - 加快大致日期匹配。 idata.frame?
【发布时间】:2012-03-05 08:47:01
【问题描述】:

我正在努力在两个数据框之间有效地执行“关闭”日期匹配。这个问题探讨了使用 plyr 包中的 idata.frame 的解决方案,但我也会对其他建议的解决方案感到非常满意。

这是两个数据框的一个非常简单的版本:

sampleticker<-data.frame(cbind(ticker=c("A","A","AA","AA"),
  date=c("2005-1-25","2005-03-30","2005-02-15","2005-04-21")))
sampleticker$date<-as.Date(sampleticker$date,format="%Y-%m-%d")

samplereport<-data.frame(cbind(ticker=c("A","A","A","AA","AA","AA"),
  rdate=c("2005-2-15","2005-03-15","2005-04-15",
  "2005-03-01","2005-04-20","2005-05-01")))
samplereport$rdate<-as.Date(samplereport$rdate,format="%Y-%m-%d")

在实际数据中,sampleticker 超过 30,000 行 40 列,samplereport 近 300,000 行 25 列。

我想做的是合并两个数据框,以便sampleticker 中的每一行与samplereport 中最接近的日期匹配相结合,这发生在sampleticker 中的日期之后。过去,我通过在代码字段上进行简单合并、升序排序,然后选择代码和日期的唯一组合,解决了类似的问题。然而,由于这个数据集的大小,合并的速度非常快。

据我所知,merge 不允许这种近似匹配。我见过一些使用findInterval 的解决方案,但由于日期之间的距离会有所不同,我不确定是否可以指定适用于所有行的间隔。

在另一个帖子here 之后,我编写了以下代码以在每一行上使用adply 并执行连接:

library(plyr)
merge<-adply(sampleticker,1,function(x){
  y<-subset(samplereport,ticker %in% x$ticker & rdate > x$date)
  y[which.min(y$rdate),]
  }))

这很好用:对于示例数据,我得到了以下内容,这就是我想要的。

   date       ticker      rdate
 1 2005-01-25  A          2005-02-15
 2 2005-03-30  A          2005-04-15
 3 2005-02-15  AA         2005-03-01
 4 2005-04-21  AA         2005-05-01

但是,由于代码执行了 30,000+ 次子集操作,所以速度非常慢:我运行了上面的查询一天多才最终将​​其杀死。

我看到here plyr 1.0 有一个结构,idata.frame,它通过引用调用数据帧,大大加快了子集操作。但是,我无法让以下代码工作:

isamplereport<-idata.frame(samplereport)
adply(sampleticker,1,function(x){
  y<-subset(isamplereport,isamplereport$ticker %in% x$ticker & 
    isamplereport$rdate > x$date)
  y[which.min(y$rdate),]
})

我得到了错误

Error in list_to_dataframe(res, attr(.data, "split_labels")) : 
Results must be all atomic, or all data frames

这对我来说很有意义,因为该操作返回一个idata.frame(我假设)。但是,将最后一行更改为:

as.data.frame(y[which.min(y$rdate),]) 

也会抛出错误:

Error in `[.data.frame`(x$`_data`, x$`_rows`, x$`_cols`) : 
undefined columns selected.

请注意,在普通的旧 samplereport 上调用 as.data.frame 会按预期返回原始数据帧。

我知道idata.frame 是实验性的,所以我不一定期望它能够正常工作。但是,如果有人对如何解决此问题有任何想法,我将不胜感激。或者,如果有人能提出一种运行效率更高的完全不同的方法,那就太好了。

马特

更新 Data.table 是解决此问题的正确方法。见下文。

【问题讨论】:

    标签: r data.table plyr subset


    【解决方案1】:

    这是一个基于data.table 的解决方案,它可能比您当前使用的更好:

    library(data.table)
    ST <- data.table(sampleticker, key="ticker")
    SR <- data.table(samplereport, key="ticker")
    SR <- SR[with(SR, order(ticker, rdate)),] # rdates need to be in increasing order
    
    SR[ST, list(date = date,
                rdate = rdate[match(TRUE, (rdate > date))]), ]
         ticker       date      rdate
    [1,]      A 2005-01-25 2005-02-15
    [2,]      A 2005-03-30 2005-04-15
    [3,]     AA 2005-02-15 2005-03-01
    [4,]     AA 2005-04-21 2005-05-01
    

    当然,听起来您真正想做的是将两个更宽的 data.frame 合并在一起。为了演示实现这一点的一种方法,在下面的示例中,我向两个 data.tables 添加了一些列,然后展示了如何合并适当的行:

    # Add some columns to both data.tables
    ST$alpha <- letters[seq_len(nrow(ST))]
    SR$n     <- seq_len(nrow(SR))
    SR$ALPHA <- LETTERS[seq_len(nrow(SR))]
    
    # Perform a merge that includes the whole rows from samplereport
    # corresponding to the selected rdate
    RES <- SR[ST, cbind(date, .SD[match(TRUE,(rdate>date)),-1]), ]
    
    # Merge res (containing the selected rows from samplereport) back together
    # with sampleticker
    keycols <- c("ticker", "date")
    setkeyv(RES, keycols)
    setkeyv(ST, keycols)
    ST[RES]
    #      ticker       date alpha      rdate n ALPHA
    # [1,]      A 2005-01-25     a 2005-02-15 1     A
    # [2,]      A 2005-03-30     b 2005-04-15 3     C
    # [3,]     AA 2005-02-15     c 2005-03-01 4     D
    # [4,]     AA 2005-04-21     d 2005-05-01 6     F
    

    【讨论】:

    • roll=TRUE 就是为此而设计的。要获得最近的之后,然后可能是X[ X[Y,roll=TRUE,which=TRUE]+1 ],或者反转它并执行Y[X,roll=TRUE]
    • 但下一个 after 要求非常罕见。在实践中mult="last"DT[J(date,23:00),roll=TRUE] 通常更好。
    • 我实际上会大量使用“next after”要求;在事件之后选择第一条记录来衡量对事件的响应并不罕见。我将尝试使用roll=TRUE 来达到正确的效果。我的初步(可能不正确)尝试给了我相反的结果:samplereport 最接近 sampleticker,但是颠倒过程给我提供了samplereport 中的所有记录和一堆 NA,这绝对不是什么我想要。我将不得不了解有关 data.table 的更多信息,因为它看起来非常有用。
    • @Matt -- 澄清有帮助。由于您也在查看roll=TRUE,因此我会花时间发布一个我发现使用它的解决方案。请让我知道它是否适用于您的数据。
    • 谢谢乔希。我认为您上面的解决方案也有效;我很确定在我开始玩roll=TRUE 之前我一直在工作,然后我把它搞砸了。我现在将尝试恢复您的解决方案。我肯定也会对roll=TRUE 解决方案感兴趣。它更简洁,而且似乎要快得多。
    【解决方案2】:

    感谢 Matthew Dowle 以及他在 data.table 中添加了向后和向前滚动的功能,现在执行此合并变得更加简单。

    ST <- data.table(sampleticker)
    SR <- data.table(samplereport)
    setkey(ST,ticker,date)
    SR[,mergerdate:=rdate]
    setkey(SR,ticker,mergerdate)
    merge<-SR[ST,roll=-Inf]
    setnames(merge,"mergerdate","date")
    
    #    ticker       date      rdate
    # 1:      A 2005-01-25 2005-02-15
    # 2:      A 2005-03-30 2005-04-15
    # 3:     AA 2005-02-15 2005-03-01
    # 4:     AA 2005-04-21 2005-05-01
    

    【讨论】:

    • 您还可以通过输入负数而不是“Inf”来限制滚动量。很棒的东西!
    • 优秀。感谢您发布此内容。
    【解决方案3】:

    根据 Matthew Dowle 的观察,这是一个解决方案,即这是应用 data.tableroll=TRUE 参数的自然场所。

    如果你要涂抹它,有一个皱纹需要熨平。 roll=TRUE 的设计使得当键的最后一列(此处为日期)未找到完全匹配时,将从最近的上一个日期开始滚动向前时间>。但是,您需要相反的结果(即使存在完全匹配,您仍然需要下一个可用日期的值)。

    第一次尝试可能是按"ticker" 排序,然后按"rdate" 排序以相反的顺序,与重新排序的结果SR 合并。这会起作用,除了data.table 不想让您以相反的顺序排序:通过"rdate" 键入会强制该列按升序排列。 (data.table 需要这样做才能实现其设计的快速匹配和连接)。

    下面我的解决方案是在两个 data.tables 中创建一个新列 -- "rnd",用于“反向数字日期”,其值是通过执行 -as.numeric(date) 形成的。这会为每个日期分配一个唯一值。此外,由于这些值已乘以-1按升序排列它们具有按降序排列日期的效果

    (另一个细节:因为您不想要完全匹配,而是总是想要当前日期之后的下一个日期,所以我从 sampleticker 的 rnd 中减去了 1,这具有预期的效果。确认它正确地完成了它的工作,我稍微编辑了您的示例数据以包含一个可能的完全匹配("2005-1-25"),它不应该被合并选择)。

    # Create sample data.tables
    library(data.table)
    
    ST <- data.table(ticker = c("A","A","AA","AA"),
                     date = as.Date(c("2005-1-25","2005-03-30","2005-02-15",
                                      "2005-04-21"), format="%Y-%m-%d"),
                     alpha = letters[1:4])    
    
    SR <- data.table(ticker = c("A","A","A","AA","AA","AA"),
                     rdate = as.Date(c("2005-1-25","2005-03-15","2005-04-15",
                                       "2005-03-01","2005-04-20","2005-05-01"), 
                                       format="%Y-%m-%d"),
                     ALPHA = LETTERS[1:6])
    

    手头有样本数据,设置并执行所需的合并:

    # Create a "reverse numerical date" column, which will uniquely
    # identify date, and allow them to be sorted in reverse temporal order
    ST$rnd <- -(as.numeric(ST$date) + 1)
    SR$rnd <- -(as.numeric(SR$rdate))
    
    # key (and thus sort) both data.tables by ticker and "reverse numerical date"
    keycols <- c("ticker", "rnd")
    setkeyv(ST, keycols)
    setkeyv(SR, keycols)
    
    # The syntax of the merge is now as simple as can be
    res <- SR[ST, roll=TRUE]
    
    # Finally, put the results back in temporal order, and pretty up the column order
    setkeyv(res, c("ticker", "date"))
    setcolorder(res, c("ticker", "date", "rdate", "alpha", "ALPHA", "rnd"))
    res
    #      ticker       date      rdate alpha ALPHA    rnd
    # [1,]      A 2005-01-25 2005-03-15     a     B -12809
    # [2,]      A 2005-03-30 2005-04-15     b     C -12873
    # [3,]     AA 2005-02-15 2005-03-01     c     D -12830
    # [4,]     AA 2005-04-21 2005-05-01     d     F -12895
    

    【讨论】:

    • 这也是一个很好的解决方案,它的运行速度比公认的解决方案快得多。
    • 非常好!我没有完全检查它,但我明白了这个概念。好的,你们说服了我:[.data.table 需要一个新的论据来回滚下一个观察结果,不是吗。顺便说一句,这是一个简单的内部切换。选项:revrollrollbackrollbacktofirstnextafter 或某种组合?或者,roll=-1|0|1 表示 after|equal|previous 而不是新参数,其中 TRUEFALSE 不需要更改,因为它们分别映射到先前的和相等的。
    • 抱歉回复晚了。如果您可以添加此附加参数,那就太好了!就我目前的目的而言,向roll=-1 添加一个选项就足够了。但是,我可以看到您可能需要rolltofirst 或类似的东西,因此添加任何必要的功能以在相反方向复制现有roll 函数可能是有意义的。感谢您的考虑!
    猜你喜欢
    • 2018-02-25
    • 2020-12-25
    • 2013-04-21
    • 1970-01-01
    • 2018-04-29
    • 1970-01-01
    • 1970-01-01
    • 2018-03-22
    • 1970-01-01
    相关资源
    最近更新 更多