PAYATHON 2026

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。这只改变标签,不改变分组成员。

备注:内容仅供参考。