R向量:计算每个日期范围内的日期数

3

我正在寻找实现创建新变量numWithin365的最佳方式,其定义如下:

给定日期列dates,计算该列中在前365天内的其他日期数量。 这个问题可以推广到日期向量以外。

以下是一种实现方法;我正在寻找任何有助于提高其可扩展性的建议。

library(dplyr)

# set seed for reproducibility
set.seed(42)

# function to calculate number of dates in prior year
within365 <- function(col){
  sapply(col, function(x){
    sum(x-365 < col & col <= x-1)
    }
  )
}
# fake data sorted chronologically
df <- data.frame(dates = sample(seq(as.Date('2015/01/01'), as.Date('2020/12/31'), 
                by="day"), 10)) %>% arrange(dates)

# applying the function
df %>% mutate(numWithin365 = within365(dates))

        dates numWithin365
1  2015-12-22            0
2  2016-09-25            1
3  2018-01-02            0
4  2018-02-25            1
5  2018-03-22            2
6  2018-06-05            3
7  2018-08-19            4
8  2019-06-13            1
9  2020-09-02            0
10 2020-09-27            1

这是我的意外失误,但我认为可以利用排序只检查“之前”的日期。 - Jon S
3
您可以使用现在已经相当标准的 data.table 非等值连接,并使用 by = .EACHI 聚合匹配项。 d[ , from := dates - 365]; d[d, on = .(dates < dates, dates >= from), .N, by = .EACHI]。我让您自行利用谷歌搜索在 SO 上找到类似的帖子(那里有大量的相关帖子)。 - Henrik
目前 data.table 没有(官方的)vignette 关于 joins 的说明。因此,请参考 ?data.tableon 参数 - 通过 on 参数,您可以指定要加入的变量,并且对于非等值连接还可以使用二元运算符,如 <>=。在同一帮助页面上还有几个示例。干杯! - Henrik
@Henrik,你应该将你的解决方案添加为答案。当扩展时,它几乎与C++解决方案一样快。 - anjama
说实话,你的解决方案是我会选择的一个。唯一的改变我会做的是将 col <= x - 1 替换为 col < x 以去除不必要的减法操作。Henrik 和 Roland 的解决方案更快,所以如果你实际上处于性能受损的情况(请不要进行过早的优化...),值得考虑他们的方案。但你的解决方案足够快,同时易于阅读,不需要大量内存和额外的依赖。有时候这些东西值得放弃一些性能。 - anjama
显示剩余2条评论
3个回答

7
如果依赖 Rcpp 不是问题,我喜欢它来完成这样的任务,因为易于维护。
library(Rcpp)
cppFunction('
  NumericVector count365(const NumericVector x) {
    // assumes that x is sorted
    
    size_t n = x.size(); 
    
    //initialize vector of zeros for counts
    NumericVector N = NumericVector(n);
    
    double lim;
    
    // start loop from second element of x
    for (size_t i = 1; i < n; ++i) {
      lim = x[i] - 365;
      
      //loop backwards from preceding element
      for (size_t j = i-1; j >= 0; --j) {
      
        //check if within 365 day range
        if (x[j] >= lim) {
          N[i]++;
        } else {
          break;
        }
      }
    }
    
    return N;
  }
')

df$numWithin365 <- count365(df$dates)
#        dates numWithin365
#1  2015-12-22            0
#2  2016-09-25            1
#3  2018-01-02            0
#4  2018-02-25            1
#5  2018-03-22            2
#6  2018-06-05            3
#7  2018-08-19            4
#8  2019-06-13            1
#9  2020-09-02            0
#10 2020-09-27            1

非常感谢。我对Rccp一点也不熟悉,但这很容易阅读和理解,并且似乎(根据另一个答案中的基准测试)非常节省内存。要将其扩展到分组情况(每个ID有多个日期,需要在ID内进行计算),您会采用lapply方法还是将分组构建到Rccp函数中? - Dries
我只是使用data.table包进行分组操作。到目前为止,我还没有遇到过需要在函数中实现分组的情况,因为这样做对我来说并不划算。然而,如果你有数百万个分组,那么这样做可能是值得的。 - Roland

4
这里有一种基本方法,速度很快,但不如@Roland的内存效率(或速度)高。
## assuming dates are ordered
tmp = as.matrix(dist(df$dates, method = 'manhattan')) < 365
colSums(tmp & upper.tri(tmp))

## 1  2  3  4  5  6  7  8  9 10 
## 0  1  0  1  2  3  4  1  0  1 

我们使用dist()函数高效地计算所有元素之间的距离。通过这些数据,我们可以与我们的标准进行比较(例如,在365天内)。
然后,我们利用日期已排序的事实添加日期必须小于当前日期的标准。也就是说,对于第1列,我们知道这是系列中的第一个日期。因此,没有其他日期会比第1列代表的日期更小。对于第2列,我们知道只有第1列的日期比它小。这个模式会一直持续到第10列。这个模式就是upper.tri(matrix)性能 我主要想知道@Roland的速度有多快。应该注意的是,这可能需要更大的数据集才能真正展示@Henrik的解决方案,但赢得了很多。
bench::mark(
  cole_base = {
    tmp = as.matrix(dist(df$dates, method = 'manhattan')) < 365
    colSums(tmp & upper.tri(tmp))
  }
  , OP = {
    df %>% mutate(numWithin365 = within365(dates))
  }
  , henrik_dt = {
    d[ , from := dates - 365]
    d[d, on = .(dates < dates, dates >= from), .N, by = .EACHI]
  }
  , roland_rcpp = {
    count365(df$dates)
  }
  , ronak_fuzzyjoin = {
    fuzzy_left_join(df1, df1, by = c('dates1' = 'dates', 'dates'), 
                    match_fun = c(`<`, `>`)) %>%
      group_by(dates = dates.x) %>%
      summarise(numWithin365 = sum(!is.na(dates.y)))
  }
  , check = FALSE
)

## # A tibble: 5 x 13
##   expression           min   median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc
##   <bch:expr>      <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl> <int> <dbl>
## 1 cole_base        108.3us  125.3us   7338.      6.23KB     4.38  3354     2
## 2 OP                3.64ms   4.36ms    206.      1.59KB     2.10    98     1
## 3 henrik_dt          4.5ms   4.94ms    200.    145.88KB     4.39    91     2
## 4 roland_rcpp        6.1us    6.7us 133645.      2.49KB    13.4   9999     1
## 5 ronak_fuzzyjoin 121.73ms 130.86ms      7.64  154.15KB     7.64     2     2

2
我使用更大的数据集运行了您的基准测试(可能是因为OP实际上有足够大的数据集,所以他们关心性能)。您的解决方案的问题在于,在超过10,000个日期之后,矩阵的内存需求变得不切实际。话虽如此,与模糊连接解决方案相比,它的内存和速度都要差得多,我不得不在扩展过程中早期将其从基准测试中剔除。 - anjama
感谢@anjama。如果你想编辑以包含不同的基准测试或提供如何扩展的信息,我很乐意将其包含在答案中。请注意,[tag:data.table]和[tag:Rcpp]比这个更好并不令人惊讶 - 如果没有那两个令人印象深刻的答案,那将是我的选择! - Cole

2
我们可以创建一个新列,从dates列中减去365天,然后使用fuzzy_left_join根据日期范围进行连接。
library(fuzzyjoin)
library(dplyr)

df1 <- df %>% mutate(dates1 = dates - 365)

fuzzy_left_join(df1, df1, by = c('dates1' = 'dates', 'dates'), 
                match_fun = c(`<`, `>`)) %>%
  group_by(dates = dates.x) %>%
  summarise(numWithin365 = sum(!is.na(dates.y)))

#   dates      numWithin365
# * <date>            <int>
# 1 2015-12-22            0
# 2 2016-09-25            1
# 3 2018-01-02            0
# 4 2018-02-25            1
# 5 2018-03-22            2
# 6 2018-06-05            3
# 7 2018-08-19            4
# 8 2019-06-13            1
# 9 2020-09-02            0
#10 2020-09-27            1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接