【问题标题】:R vector count number of dates within range of each dateR向量计数每个日期范围内的日期数
【发布时间】:2021-05-06 05:36:47
【问题描述】:

我正在寻找实现创建新变量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

【问题讨论】:

  • 这是我无意的,但我认为可以利用排序并仅检查“之前”的日期。
  • @Dries 您可以使用(现在相当标准的)data.table non-equi join 并使用by = .EACHI 聚合匹配项。 d[ , from := dates - 365]; d[d, on = .(dates &lt; dates, dates &gt;= from), .N, by = .EACHI]。我把它留给你的 google-fu 在 SO 上找到类似的帖子(有很多)。
  • @Henrik 为什么需要包含on?我很难在 wiki 中找到它。
  • data.table 连接上还没有(官方)小插图。所以请参考 ?data.tableon 参数 - 使用 on 指定要连接的变量,对于非等值连接还有二元运算符,如 &lt;&gt;=。另请参阅同一帮助页面上的几个示例。干杯
  • @Henrik 您应该添加您的解决方案作为答案。放大后,它几乎与 C++ 解决方案一样快。

标签: r date optimization dplyr


【解决方案1】:

如果对 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 函数中?
  • 我只是使用包 data.table 进行 goup-by 操作。我还没有一个案例可以很好地利用我的时间在函数中实现 group-by。但是,如果您有数百万个组,这可能是值得的。
【解决方案2】:

这是一种快速但不会像@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

【讨论】:

  • 我用更大的数据集运行了你的基准测试(大概 OP 关心性能,因为他们实际上有一个足够大的数据集,这很重要)。您的解决方案的问题是,过去 10,000 个日期后,矩阵的内存要求变得不切实际。话虽如此,fuzzyjoin 解决方案的内存和速度都要差得多,我不得不在扩大规模的早期将其从基准测试中删除。
  • 谢谢@anjama。如果您想编辑以包含不同的基准或提供您的缩放方式,我很乐意将其包含在答案中。请注意,data.tablercpp 比这更好也就不足为奇了 - 如果我们还没有这两个很棒的答案,那将是我的方法!
【解决方案3】:

我们可以创建一个新列,从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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2010-11-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多