dplyr:使用滚动时间窗口对数据进行分组和汇总/变异

dplyr:使用滚动时间窗口对数据进行分组和汇总/变异,r,time-series,dplyr,lubridate,R,Time Series,Dplyr,Lubridate,我有不规则的timeseries数据,表示用户的某种类型的事务。每行数据都有时间戳,表示当时的一个事务。由于数据的不规则性,一些用户一天可能有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-

我有不规则的timeseries数据,表示用户的某种类型的事务。每行数据都有时间戳,表示当时的一个事务。由于数据的不规则性,一些用户一天可能有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
df %>%
   arrange(id, date) %>%
   group_by(id) %>%
   filter(as.numeric(difftime(max(date), date, unit = 'days')) <= 5) %>%
   summarise(n_total_widgets = sum(n_widgets))
我经常想知道一些关于用户的滚动统计数据。例如:对于该用户,在某一天,前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%>%groupby(id,date)%>%summary(…)

同样,如果时间窗口为每月,这对于lubridate也相对简单:
data%>%groupby(id,year(date),month(date))%>%summary(…)

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

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

例如,对于用户1,前5天的
2015-01-01
交易数可能等于100笔交易,对于同一用户,前5天的
2015-02-01
交易数可能等于5笔交易。因此,回溯一定数量的行根本不起作用

此外,还有一个SO线程讨论不规则时间序列类型数据的滚动日期(),但公认的解决方案是使用
data.table
,我特别寻找一种
dplyr
方法来实现这一点

我想,在这个问题的核心,这个问题可以通过回答这个问题来解决:我如何在
dplyr
中按任意时间段对u进行分组。或者,如果有一种不同的
dplyr
方法来实现上述目标,而不需要复杂的
groupby
,我该怎么做


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

根据下面的评论进行编辑

您可以尝试这样的方法最多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))

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

另一种方法是扩展数据集以包含所有可能的天数(使用
tidyr::complete
),然后使用滚动函数(
RcppRoll::roll\u 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"))
library(tidyr)
图书馆(RcppRoll)
df2%
变异(日期=as.date(日期))
##创建包含所有可能日期的完整数据集(第一次观察时,甚至返回30天)
df_满%
变异(日期=as.date(日期))%>%
完成(id,
日期=序号(从=最小(.$日期)-30,到=最大(.$日期),由=1),
填充=列表(n_widgets=0))
##现在使用滚动功能,只保留原始行(左连接)
df_辊%
分组依据(id)%>%
mutate(n_trans_30=roll_sum(x=n_widgets!=0,n=30,fill=0,align=“right”),
总计\u小部件\u 30=滚动\u和(x=n\u小部件,n=30,fill=0,align=“right”))%>%
解组()%>%
右连接(df2,by=c(“日期”、“id”、“n\u小部件”))
结果和你的一样(偶然)

id date n\u widgets n\u trans\u 30总计\u widgets\u 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

但如前所述,它将失败数天,因为它计算最后30个OB,而不是最后30天。因此,您可能希望首先按天总结信息,然后应用此方法

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


df为了简单起见,我推荐处理滑动窗口操作的包。在OP请求中,窗口大小
k=30
,窗口取决于日期
idx=date
。您可以使用
runner
函数在给定窗口上应用任何R函数,以及
sum\u 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
库(运行程序)
图书馆(dplyr)
df%>%
分组依据(id)%>%
安排(日期,.by_group=TRUE)%>%
变异(
n_trans30=runner(n_小部件,k=30,idx=date,函数(x)长度(x)),
n_widgets30=sum_run(n_小部件,k=30,idx=date),
)
#id日期n_widgets n_trans30 n_widgets30
#                        
# 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
应按升序排列


有关更多信息,请转到我编辑的和

,您可以修改difftime以完全符合您的计算方式。是否包含当前日期,或者从另一个日期开始计数。当我将每一行与当前日期或设定日期进行比较时,上述内容肯定很有用。然而,每一行都将对应
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
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
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