dplyr:使用滚动时间窗分组和汇总/改变数据

时间:2016-03-23 20:18:07

标签: r time-series dplyr lubridate

我有不规则的时间序列数据,代表用户的某种交易类型。每行数据都带有时间戳,表示当时的事务。根据数据的不规则性,一些用户一天可能有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个交易,而同一用户的交易数量可能是{{1}之前5天内的交易数量。 1}}等于5个事务。因此,回顾一定数量的行将无法正常工作。

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

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

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

5 个答案:

答案 0 :(得分:4)

这可以使用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

答案 1 :(得分: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天,而不是最近30天。因此,您可能希望白天首先summarise信息,然后应用此信息。

答案 2 :(得分:1)

我在研究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

此版本相当具体,但您可能可以制作更通用的功能版本。

答案 3 :(得分:1)

为简单起见,我建议使用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

答案 4 :(得分:0)

根据以下评论编辑。

您最多可以尝试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