在组内获取最大日期的有效方法
Efficient way of taking the max date within groups
我经常有数据集,其中随着时间的推移我有多个事件度量,我想为一个月内的每个事件获取最大日期。为此,我创建了一个年份和月份变量,然后按日期降序排序,然后 group_by
除了日期之外的所有变量,然后使用 slice
获取最大日期。听Hadely在视频里说arrange
是个慢操作。我想知道在 tidyverse 中执行此操作的有效方法是什么。
请 post 基础,data.table,以及其他答案,以便其他用途可能会从这个问题中受益,但我希望这里是一种 tidyverse 方法。
我目前是如何操作的:
library(tidyverse)
set.seed(10)
dat <- data_frame(
date = sample(seq(as.Date('1999/01/01'), as.Date('2001/01/01'), by="day"), 1000, TRUE),
cash = sample(1010:1030, 1000, TRUE),
stage = sample(LETTERS[1:7], 1000, TRUE)
) %>% distinct()
dat %>%
mutate(
year = format(date, '%Y'),
month = format(date, '%B')
) %>%
arrange(desc(date)) %>%
group_by(cash, stage, year, month) %>%
slice(1)
OP 不包含扩展基准的方法,所以我自己做:
library(data.table)
library(dplyr)
n = 3e6
n_days = 20000
set.seed(10)
dat <- data_frame(
date = sample(
seq(as.Date('1999/01/01'), as.Date('1999/01/01') + n_days - 1, by="day")
, n, TRUE),
cash = sample(1010:1030, n, TRUE),
stage = sample(LETTERS[1:7], n, TRUE)
) %>% distinct()
DT = data.table(dat)[, date := as.IDate(date)]
测试:
# OP's approach
system.time(
res <- dat %>%
mutate(
year = format(date, '%Y'),
month = format(date, '%B')
) %>%
arrange(desc(date)) %>%
group_by(cash, stage, year, month) %>%
slice(1)
)
# user system elapsed
# 9.44 0.09 9.54
# a data.table way
system.time({
DTres <- DT[, g := date - mday(date) + 1L ][order(-date), .SD[1L], by=.(cash, stage, g)]
})
# user system elapsed
# 0.51 0.00 0.52
# verify
fsetequal(
data.table(res[, c("cash","stage","date")])[, date := as.IDate(date)][],
DTres[, c("cash","stage","date")]
) # TRUE
翻译回 dplyr:
system.time({
newres <- dat %>% mutate(g = date - as.POSIXlt(date)$mday + 1) %>%
arrange(desc(date)) %>% group_by(cash, stage, g) %>% slice(1L)
})
# Error, who knows why?
system.time({
newres <- dat %>% mutate(g = date + 1 - date %>% as.POSIXlt %>% `[[`("mday")) %>%
arrange(desc(date)) %>% group_by(cash, stage, g) %>% slice(1L)
})
# user system elapsed
# 1.47 0.04 1.52
fsetequal(
data.table(res[c("date","cash","stage")]),
data.table(newres[c("date","cash","stage")])
) # TRUE
# or ...
iddat <- dat %>% mutate(date = data.table::as.IDate(date))
mday <- data.table::mday
system.time({
borrowres <- iddat %>% arrange(desc(date)) %>%
distinct(cash, stage, g = date - mday(date) + 1L)
})
# user system elapsed
# 0.92 0.02 0.94
fsetequal(
data.table(borrowres[names(DTres)]),
DTres
) # TRUE
当我调整 n
和 n_days
时,相对时间变化不大。感谢@Arun 这种舍入方式。以前,我有 round(date, "months")
。似乎关键是使用算术而不是 format
。我不确定时间上的剩余差异;也许它可以通过使用 dtplyr 来解决。切换到 arrange %>% distinct
除了清理语法外没有做太多事情。
旁注:我正在加载 dplyr 而不是 tidyverse,因为我真的不知道后者包含什么。不过,我用 tidyverse 试了一下,得到了相同的时间。
其他几个 data.table
选项:
f.dt <- function(dat) {
DT <- data.table(dat)[,yearmon := format(date, "%Y %B")]
DT[order(-date),idx := 1:.N,
by = .(cash, stage, yearmon)
][idx == 1, !"idx"][]
}
f2.dt <- function(dat) {
DT <- data.table(dat)[,yearmon := format(date, "%Y %B")]
DT[DT[, .I[which.max(date)],
by = .(cash, stage, yearmon)]$V1,][]
}
第二个使用Señor O's second approach in the linked question。
针对
测试这些
f.dplyr <- function(dat) {
dat %>%
mutate(
yearmon = format(date, '%Y %B')
) %>%
arrange(desc(date)) %>%
group_by(cash, stage, yearmon) %>%
slice(1)
}
有了 Frank 的数据,
fsetequal(f.dt(dat), data.table(f.dplyr(dat)))
# [1] TRUE
fsetequal(f2.dt(dat), data.table(f.dplyr(dat)))
# [1] TRUE
microbenchmark::microbenchmark(
f.dplyr(dat),
f.dt(dat),
f2.dt(dat),
times = 10L
)
# Unit: seconds
# expr min lq mean median uq max neval
# f.dplyr(dat) 3.446304 3.562061 3.601803 3.598340 3.625105 3.860911 10
# f.dt(dat) 1.525025 1.540881 1.727772 1.561149 1.718817 2.422788 10
# f2.dt(dat) 1.299834 1.315242 1.510534 1.384346 1.667197 2.262938 10
数据
n = 3e6
n_days = 20000
set.seed(10)
dat <- dplyr::data_frame(
date = sample(
seq(as.Date('1999/01/01'),
as.Date('1999/01/01') + n_days - 1,
by = "day"), n, TRUE),
cash = sample(1010:1030, n, TRUE),
stage = sample(LETTERS[1:7], n, TRUE)
) %>% dplyr::distinct()
我经常有数据集,其中随着时间的推移我有多个事件度量,我想为一个月内的每个事件获取最大日期。为此,我创建了一个年份和月份变量,然后按日期降序排序,然后 group_by
除了日期之外的所有变量,然后使用 slice
获取最大日期。听Hadely在视频里说arrange
是个慢操作。我想知道在 tidyverse 中执行此操作的有效方法是什么。
请 post 基础,data.table,以及其他答案,以便其他用途可能会从这个问题中受益,但我希望这里是一种 tidyverse 方法。
我目前是如何操作的:
library(tidyverse)
set.seed(10)
dat <- data_frame(
date = sample(seq(as.Date('1999/01/01'), as.Date('2001/01/01'), by="day"), 1000, TRUE),
cash = sample(1010:1030, 1000, TRUE),
stage = sample(LETTERS[1:7], 1000, TRUE)
) %>% distinct()
dat %>%
mutate(
year = format(date, '%Y'),
month = format(date, '%B')
) %>%
arrange(desc(date)) %>%
group_by(cash, stage, year, month) %>%
slice(1)
OP 不包含扩展基准的方法,所以我自己做:
library(data.table)
library(dplyr)
n = 3e6
n_days = 20000
set.seed(10)
dat <- data_frame(
date = sample(
seq(as.Date('1999/01/01'), as.Date('1999/01/01') + n_days - 1, by="day")
, n, TRUE),
cash = sample(1010:1030, n, TRUE),
stage = sample(LETTERS[1:7], n, TRUE)
) %>% distinct()
DT = data.table(dat)[, date := as.IDate(date)]
测试:
# OP's approach
system.time(
res <- dat %>%
mutate(
year = format(date, '%Y'),
month = format(date, '%B')
) %>%
arrange(desc(date)) %>%
group_by(cash, stage, year, month) %>%
slice(1)
)
# user system elapsed
# 9.44 0.09 9.54
# a data.table way
system.time({
DTres <- DT[, g := date - mday(date) + 1L ][order(-date), .SD[1L], by=.(cash, stage, g)]
})
# user system elapsed
# 0.51 0.00 0.52
# verify
fsetequal(
data.table(res[, c("cash","stage","date")])[, date := as.IDate(date)][],
DTres[, c("cash","stage","date")]
) # TRUE
翻译回 dplyr:
system.time({
newres <- dat %>% mutate(g = date - as.POSIXlt(date)$mday + 1) %>%
arrange(desc(date)) %>% group_by(cash, stage, g) %>% slice(1L)
})
# Error, who knows why?
system.time({
newres <- dat %>% mutate(g = date + 1 - date %>% as.POSIXlt %>% `[[`("mday")) %>%
arrange(desc(date)) %>% group_by(cash, stage, g) %>% slice(1L)
})
# user system elapsed
# 1.47 0.04 1.52
fsetequal(
data.table(res[c("date","cash","stage")]),
data.table(newres[c("date","cash","stage")])
) # TRUE
# or ...
iddat <- dat %>% mutate(date = data.table::as.IDate(date))
mday <- data.table::mday
system.time({
borrowres <- iddat %>% arrange(desc(date)) %>%
distinct(cash, stage, g = date - mday(date) + 1L)
})
# user system elapsed
# 0.92 0.02 0.94
fsetequal(
data.table(borrowres[names(DTres)]),
DTres
) # TRUE
当我调整 n
和 n_days
时,相对时间变化不大。感谢@Arun 这种舍入方式。以前,我有 round(date, "months")
。似乎关键是使用算术而不是 format
。我不确定时间上的剩余差异;也许它可以通过使用 dtplyr 来解决。切换到 arrange %>% distinct
除了清理语法外没有做太多事情。
旁注:我正在加载 dplyr 而不是 tidyverse,因为我真的不知道后者包含什么。不过,我用 tidyverse 试了一下,得到了相同的时间。
其他几个 data.table
选项:
f.dt <- function(dat) {
DT <- data.table(dat)[,yearmon := format(date, "%Y %B")]
DT[order(-date),idx := 1:.N,
by = .(cash, stage, yearmon)
][idx == 1, !"idx"][]
}
f2.dt <- function(dat) {
DT <- data.table(dat)[,yearmon := format(date, "%Y %B")]
DT[DT[, .I[which.max(date)],
by = .(cash, stage, yearmon)]$V1,][]
}
第二个使用Señor O's second approach in the linked question。
针对
测试这些f.dplyr <- function(dat) {
dat %>%
mutate(
yearmon = format(date, '%Y %B')
) %>%
arrange(desc(date)) %>%
group_by(cash, stage, yearmon) %>%
slice(1)
}
有了 Frank 的数据,
fsetequal(f.dt(dat), data.table(f.dplyr(dat)))
# [1] TRUE
fsetequal(f2.dt(dat), data.table(f.dplyr(dat)))
# [1] TRUE
microbenchmark::microbenchmark(
f.dplyr(dat),
f.dt(dat),
f2.dt(dat),
times = 10L
)
# Unit: seconds
# expr min lq mean median uq max neval
# f.dplyr(dat) 3.446304 3.562061 3.601803 3.598340 3.625105 3.860911 10
# f.dt(dat) 1.525025 1.540881 1.727772 1.561149 1.718817 2.422788 10
# f2.dt(dat) 1.299834 1.315242 1.510534 1.384346 1.667197 2.262938 10
数据
n = 3e6
n_days = 20000
set.seed(10)
dat <- dplyr::data_frame(
date = sample(
seq(as.Date('1999/01/01'),
as.Date('1999/01/01') + n_days - 1,
by = "day"), n, TRUE),
cash = sample(1010:1030, n, TRUE),
stage = sample(LETTERS[1:7], n, TRUE)
) %>% dplyr::distinct()