【问题标题】:dplyr: grouping and summarizing/mutating data with rolling time windowsdplyr:使用滚动时间窗口对数据进行分组和汇总/变异
【发布时间】:2023-03-29 16:48:01
【问题描述】:

我有代表用户某种交易类型的不规则时间序列数据。每行数据都带有时间戳,并代表当时的交易。由于数据的不规则性,一些用户一天可能有 100 行,而其他用户一天可能有 0 或 1 笔交易。

数据可能如下所示:

data.frame(
  id = c(1, 1, 1, 1, 1, 2, 2, 3, 4),
  date = c("2015-01-01", 
           "2015-01-01", 
           "2015-01-05", 
           "2015-01-25",
           "2015-02-15",
           "2015-05-05", 
           "2015-01-01", 
           "2015-08-01", 
           "2015-01-01"),
  n_widgets = c(1,2,3,4,4,5,2,4,5)
)

   id       date n_widgets
1  1 2015-01-01         1
2  1 2015-01-01         2
3  1 2015-01-05         3
4  1 2015-01-25         4
5  1 2015-02-15         4
6  2 2015-05-05         5
7  2 2015-01-01         2
8  3 2015-08-01         4
9  4 2015-01-01         5

我经常想知道一些关于用户的滚动统计信息。例如:对于这个用户,在某一天,过去 30 天内发生了多少笔交易,过去 30 天内卖出了多少小部件等。

对应上面的例子,数据应该是这样的:

   id     date    n_widgets  n_trans_30  total_widgets_30
1  1 2015-01-01         1           1             1
2  1 2015-01-01         2           2             3
3  1 2015-01-05         3           3             6
4  1 2015-01-25         4           4             10
5  1 2015-02-15         4           2             8
6  2 2015-05-05         5           1             5
7  2 2015-01-01         2           1             2
8  3 2015-08-01         4           1             4
9  4 2015-01-01         5           1             5

如果时间窗口是每天,那么解决方法很简单:data %>% group_by(id, date) %>% summarize(...)

类似地,如果时间窗口是每月,这对于 lubridate 来说也相对简单:data %>% group_by(id, year(date), month(date)) %>% summarize(...)

但是我面临的挑战是如何为任意时间段设置时间窗口:5 天、10 天等。

还有RcppRoll 库,但RcppRollzoo 中的滚动函数似乎更适合常规时间序列。据我所知,这些窗口函数是根据行数而不是指定的时间段工作的——关键区别在于,某个时间段可能有不同的行数,具体取决于日期和用户。

例如,对于用户 1,2015-01-01 前 5 天的交易数可能等于 100 次交易,而对于同一用户,2015-02-01 前 5 天的交易数可能是等于 5 笔交易。因此,回顾一组行数根本行不通。

此外,还有另一个 SO 线程讨论不规则时间序列类型数据 (Create new column based on condition that exists within a rolling date) 的滚动日期,但是公认的解决方案是使用 data.table,而我正在专门寻找 dplyr 实现此目的的方法。

我想在这个问题的核心,这个问题可以通过回答这个问题来解决:我如何group_bydplyr 中的任意时间段。或者,如果在没有复杂的group_by 的情况下有不同的dplyr 方式来实现上述目标,我该怎么做?

编辑:更新示例以使滚动窗口的性质更加清晰。

【问题讨论】:

    标签: r time-series dplyr lubridate


    【解决方案1】:

    这可以使用 SQL 来完成:

    library(sqldf)
    
    dd <- transform(data, date = as.Date(date))
    sqldf("select a.*, count(*) n_trans30, sum(b.n_widgets) 'total_widgets30' 
           from dd a 
           left join dd b on b.date between a.date - 30 and a.date 
                             and b.id = a.id
                             and b.rowid <= a.rowid
           group by a.rowid")
    

    给予:

      id       date n_widgets n_trans30 total_widgets30
    1  1 2015-01-01         1         1               1
    2  1 2015-01-01         2         2               3
    3  1 2015-01-05         3         3               6
    4  1 2015-01-25         4         4              10
    5  2 2015-05-05         5         1               5
    6  2 2015-01-01         2         1               2
    7  3 2015-08-01         4         1               4
    8  4 2015-01-01         5         1               5
    

    【讨论】:

      【解决方案2】:

      另一种方法是扩展您的数据集以包含所有可能的日期(使用tidyr::complete),然后使用滚动函数(RcppRoll::roll_sum

      您每天有多次观察的事实可能会造成问题......

      library(tidyr)
      library(RcppRoll)
      df2 <- df %>%
         mutate(date=as.Date(date))
      
      ## create full dataset with all possible dates (go even 30 days back for first observation)
      df_full<- df2 %>%
       mutate(date=as.Date(date))  %>%
         complete(id, 
             date=seq(from=min(.$date)-30,to=max(.$date), by=1), 
             fill=list(n_widgets=0))
      
      ## now use rolling function, and keep only original rows (left join)
      df_roll <- df_full %>%
        group_by(id) %>%
        mutate(n_trans_30=roll_sum(x=n_widgets!=0, n=30, fill=0, align="right"),
               total_widgets_30=roll_sum(x=n_widgets, n=30, fill=0, align="right")) %>%
        ungroup() %>%
        right_join(df2, by = c("date", "id", "n_widgets"))
      

      结果和你的一样(偶然)

           id       date n_widgets n_trans_30 total_widgets_30
        <dbl>     <date>     <dbl>      <dbl>            <dbl>
      1     1 2015-01-01         1          1                1
      2     1 2015-01-01         2          2                3
      3     1 2015-01-05         3          3                6
      4     1 2015-01-25         4          4               10
      5     1 2015-02-15         4          2                8
      6     2 2015-05-05         5          1                5
      7     2 2015-01-01         2          1                2
      8     3 2015-08-01         4          1                4
      9     4 2015-01-01         5          1                5
      

      但如前所述,它会在几天内失败,因为它会持续 30 obs,而不是持续 30 天。所以你可能想先summarise每天的信息,然后应用它。

      【讨论】:

        【解决方案3】:

        根据下面的评论编辑。

        您最多可以尝试这样的事情 5 天:

        df %>%
          arrange(id, date) %>%
          group_by(id) %>%
          filter(as.numeric(difftime(Sys.Date(), date, unit = 'days')) <= 5) %>%
          summarise(n_total_widgets = sum(n_widgets))
        

        在这种情况下,当前五天内没有任何天数。所以,它不会产生任何输出。

        要获取每个 ID 的最后五天,您可以执行以下操作:

        df %>%
           arrange(id, date) %>%
           group_by(id) %>%
           filter(as.numeric(difftime(max(date), date, unit = 'days')) <= 5) %>%
           summarise(n_total_widgets = sum(n_widgets))
        

        结果输出将是:

        Source: local data frame [4 x 2]
        
             id n_total_widgets
          (dbl)           (dbl)
        1     1               4
        2     2               5
        3     3               4
        4     4               5
        

        【讨论】:

        • 我编辑了...您可以修改 difftime 以完全符合您想要的计算方式。包括当前日期,或不包括,或从另一个日期开始计数。
        • 当我将每一行与当前日期或设定日期进行比较时,上述内容肯定很有用。但是,每一行都对应一个不同的时间窗口来检查:即对于 2015-01-01,我需要汇总 2014-12-28 - 2015-01-01 和 2015-01-25 的数据,我需要从 2015 年 1 月 21 日到 2015 年 1 月 25 日的汇总数据。
        • 例如,您可以使用max(date) 代替Sys.Date(),这样每个组就有不同的参考(id)。
        • 确实如此,但即使在特定的id 内,也不一定会将每个日期窗口与单个日期进行比较。例如,如果id = 12015-01-102015-01-112015-01-12 有交易,那么max(date) = 2015-01-12,但是这3 个交易中的每一个都有3 个不同的日期窗口需要汇总:2015-01-06 to 2015-01-10、@ 987654334@ 和2015-01-08 to 2015-01-12 等。每个id 可能有数百笔交易相隔几个月,我仍然需要为每个交易日期建立一个窗口。
        【解决方案4】:

        我在处理这个question时找到了一种方法

        df <- data.frame(
          id = c(1, 1, 1, 1, 1, 2, 2, 3, 4),
          date = c("2015-01-01", 
                   "2015-01-01", 
                   "2015-01-05", 
                   "2015-01-25",
                   "2015-02-15",
                   "2015-05-05", 
                   "2015-01-01", 
                   "2015-08-01", 
                   "2015-01-01"),
          n_widgets = c(1,2,3,4,4,5,2,4,5)
        )
        
        count_window <- function(df, date2, w, id2){
          min_date <- date2 - w
          df2 <- df %>% filter(id == id2, date >= min_date, date <= date2)
          out <- length(df2$date)
          return(out)
        }
        v_count_window <- Vectorize(count_window, vectorize.args = c("date2","id2"))
        
        sum_window <- function(df, date2, w, id2){
          min_date <- date2 - w
          df2 <- df %>% filter(id == id2, date >= min_date, date <= date2)
          out <- sum(df2$n_widgets)
          return(out)
        }
        v_sum_window <- Vectorize(sum_window, vectorize.args = c("date2","id2"))
        
        res <- df %>% mutate(date = ymd(date)) %>% 
          mutate(min_date = date - 30,
                 n_trans = v_count_window(., date, 30, id),
                 total_widgets = v_sum_window(., date, 30, id)) %>% 
          select(id, date, n_widgets, n_trans, total_widgets)
        res
        
        
        id       date n_widgets n_trans total_widgets
        
        1  1 2015-01-01         1       2             3
        2  1 2015-01-01         2       2             3
        3  1 2015-01-05         3       3             6
        4  1 2015-01-25         4       4            10
        5  1 2015-02-15         4       2             8
        6  2 2015-05-05         5       1             5
        7  2 2015-01-01         2       1             2
        8  3 2015-08-01         4       1             4
        9  4 2015-01-01         5       1             5
        

        这个版本是相当具体的,但您可能可以制作一个更通用的函数版本。

        【讨论】:

          【解决方案5】:

          为了简单起见,我推荐处理滑动窗口操作的runner 包。在 OP 请求窗口大小 k = 30 和窗口取决于日期 idx = date。您可以使用 runner 函数在给定窗口上应用任何 R 函数,以及 sum_run

          library(runner)
          library(dplyr)
          
          df %>%
            group_by(id) %>%
            arrange(date, .by_group = TRUE) %>%
            mutate(
              n_trans30 = runner(n_widgets, k = 30, idx = date, function(x) length(x)),
              n_widgets30 = sum_run(n_widgets, k = 30, idx = date),
            )
          
          # id      date       n_widgets n_trans30 n_widgets30
          #<dbl>   <date>         <dbl>     <dbl>       <dbl>
          # 1    2015-01-01         1         1           1
          # 1    2015-01-01         2         2           3
          # 1    2015-01-05         3         3           6
          # 1    2015-01-25         4         4          10
          # 1    2015-02-15         4         2           8
          # 2    2015-01-01         2         1           2
          # 2    2015-05-05         5         1           5
          # 3    2015-08-01         4         1           4
          # 4    2015-01-01         5         1           5
          

          重要提示:idx = date 应按升序排列。

          更多信息请访问documentationvignettes

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2017-05-31
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2021-01-20
            相关资源
            最近更新 更多