dplyr:使用滚动时间窗口对数据进行分组和汇总/变异
我有不规则的timeseries数据,表示用户的某种类型的事务。每行数据都有时间戳,表示当时的一个事务。由于数据的不规则性,一些用户一天可能有100行,而其他用户一天可能有0或1个事务 数据可能如下所示: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-
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