R 中如何用 zoo 按自然分钟聚合不规则时间序列?
结论
可以使用 aggregate():先将每条观测的时间归到所在分钟,再对每分钟的数据执行自定义函数。
在 rollapply(z, width = 60, FUN = mean, by = 60) 中,width = 60 表示窗口包含 60 条观测,by = 60 表示每隔 60 条观测计算一次。它们不会自动解释为 60 秒,也不会自动对齐自然分钟。缺少某些秒级记录时,60 条观测可能跨越多个分钟。
先正确设置时间索引
原代码把格式化后的时间字符串放在 tt 的 zoo 数据部分,而 z 使用默认的序号索引。merge(z, tt) 不会把这些字符串转换成 z 的时间索引,合并结果还包含字符数据,不适合直接求均值。
应把观测值作为数据,把 POSIXct 时间作为 order.by。下面的示例只使用 base R 和 zoo,统一采用 UTC;实际使用时应换成数据对应的时区。
library(zoo)
tt <- seq(
from = as.POSIXct("2020-01-01 00:00:00", tz = "UTC"),
to = as.POSIXct("2020-01-01 00:02:00", tz = "UTC"),
by = "1 sec"
)
z <- zoo(
runif(length(tt), 1, 10),
order.by = tt
)
原示例使用的 ymd_hms() 来自 lubridate。如果继续使用该写法,需要加载相应包,或使用 lubridate::ymd_hms()。
按所在分钟执行聚合
将时间戳向下取整到分钟,就能得到每条观测所属的分组。下面的函数适用于索引为 POSIXct 的 zoo 对象:
aggregate_by_minute <- function(x, FUN, ...) {
minute_index <- as.POSIXct(
floor(as.numeric(index(x)) / 60) * 60,
origin = "1970-01-01",
tz = "UTC"
)
aggregate(x, by = minute_index, FUN = FUN, ...)
}
output <- aggregate_by_minute(z, mean)
output
分组区间左闭右开,结果以分钟起点标记:
00:00:00 ≤ t < 00:01:00,标记为00:00:00。00:01:00 ≤ t < 00:02:00,标记为00:01:00。00:02:00 ≤ t < 00:03:00,标记为00:02:00。
示例会产生 3 行:前两组各包含 60 条观测,最后一组只有 00:02:00 这一条。最后一组的均值就是该观测值。由于输入使用 runif(),具体数值每次运行可能不同。
缺失秒和自定义函数如何处理
缺少某些秒级记录不会影响分组边界。聚合函数使用该分钟实际存在的观测,不必先补齐到 60 条。
# 删除若干秒级观测,模拟不规则采样
z_irregular <- z[-c(5, 18, 64, 90)]
custom_fun <- function(values) {
mean(values, na.rm = TRUE)
}
output_irregular <- aggregate_by_minute(
z_irregular,
FUN = custom_fun
)
将 custom_fun 的函数体替换为实际统计逻辑即可。对于这里的单变量序列,如果希望每分钟得到一个统计值,函数应返回一个标量。
需要区分两种情况:某秒缺少记录时,该秒不会参与计算;记录存在但值为 NA 时,如何处理由聚合函数决定。示例中的 na.rm = TRUE 会忽略 NA,但某分钟全部为 NA 时,mean() 会返回 NaN。
分钟完整性与结果标签
上述代码会聚合所有出现过观测的分钟,即使某分钟只有部分数据也会保留,这与问题要求的 3 行结果一致。如果只保留完整的分钟,需要另行定义完整性条件;在允许缺失秒的情况下,不能一概将少于 60 条观测的分钟视为无效。
某一分钟完全没有观测时,aggregate() 不会自动生成该分钟的结果行。如果需要连续的分钟索引,并为空分钟填入 NA,还需在聚合后补齐索引。
当前结果标签表示分钟起点。如果希望用分钟终点标记同一组数据,可以在聚合后执行 index(output) <- index(output) + 60。这只改变标签,不改变分组成员。
备注:内容仅供参考。
