R:将一年的日期切成 2 个月的 bin 会产生 7 个 bin 而不是 6 个?

问题描述

我正在尝试使用 R 中的 cut() 函数将一年的日期划分为 6 个两个月的 bin。当我这样做时,它会生成 7 个垃圾箱而不是 6 个垃圾箱,最后一个垃圾箱是空的。我正在使用以下代码:

dates <- seq(as.Date("2021-1-1"),as.Date("2021-12-31"),by="day")
months <- cut(dates,"month",labels=1:12)
table(months)
# months
#  1  2  3  4  5  6  7  8  9 10 11 12 
# 31 28 31 30 31 30 31 31 30 31 30 31 
sextiles <- cut(dates,"2 months",labels=1:6)
# Error in cut.default(unclass(x),unclass(breaks),labels = labels,right = right,: 
#   lengths of 'breaks' and 'labels' differ
sextiles <- cut(dates,labels=1:7)
table(sextiles)
# sextiles
#  1  2  3  4  5  6  7 
# 59 61 61 62 61 61  0 

当我将年份划分为单个月份时,代码工作正常,但是当我划分为 2 个月时会产生错误,除非我在标签参数中考虑了 7 个而不是 6 个。如果我从年末开始删除日期,则在删除一年的最后 3 天后,代码最终可以使用 6 个 bin:

dates_364 <- dates[-length(dates)]
sextiles <- cut(dates_364,: 
#   lengths of 'breaks' and 'labels' differ
dates_363 <- dates_364[-length((dates_364))]
sextiles <- cut(dates_363,: 
#   lengths of 'breaks' and 'labels' differ
dates_362 <- dates_363[-length((dates_363))]
sextiles <- cut(dates_362,labels=1:6)
table(sextiles)
# sextiles
#  1  2  3  4  5  6 
# 59 61 61 62 61 58 

这似乎是函数中的一个错误。任何人都可以对我缺少的东西有所了解吗?谢谢!

解决方法

有两种方法可以为一个数字范围定义“bins”,以便所有提供的数字都在其中一个 bin 内:

  • 找到最小值,找到最大值,并且由于 Date-bins 通常是 right=FALSE 表示右开,因此将最大值稍微突出一点;或
  • 找到最小值,不要找到最大值,而是使用 Inf 以便它始终包含最大值。

cut.Date 选择了两者中的第一个。此外,它不是“从最大值跳出 1 天”,而是选择“跳出‘一步’”。这意味着当您说 "2 months" 时,它将确保下一个 bin“边缘”距离倒数第二个边界 2 个月。

也就是说,如果您查看 cut.Date 的来源:

        start <- as.POSIXlt(min(x,na.rm = TRUE))
# ...
            end <- as.POSIXlt(max(x,na.rm = TRUE))
# and then if 'months',then
            end <- as.POSIXlt(end + (31 * step * 86400))
# and eventually
            breaks <- as.Date(seq(start,end,breaks))

所以我会debug(cut.Date)看看cut(dates,"2 months")

start
# [1] "2021-01-01 UTC"
# debug: end <- as.POSIXlt(max(x,na.rm = TRUE))
# debug: step <- if (length(by2) == 2L) as.integer(by2[1L]) else 1L
end
# [1] "2021-12-31 UTC"
step
# [1] 2

# debug: as.integer(by2[1L])
# debug: end <- as.POSIXlt(end + (31 * step * 86400))
end
# [1] "2022-03-03 UTC"

# debug: end$mday <- 1L
# debug: end$isdst <- -1L
# debug: breaks <- as.Date(seq(start,breaks))
breaks
# [1] "2021-01-01" "2021-03-01" "2021-05-01" "2021-07-01" "2021-09-01" "2021-11-01" "2022-01-01"
# [8] "2022-03-01"

它最终会执行 breaks[-length(breaks)],这就解释了为什么我们没有看到 8。我的猜测是,在某些极端情况下(也许是闰年?),31 * step * 86400(或其他 by-单位)并不总是完美对齐,因此他们对其进行了一些缓冲。

长话短说(为时已晚),我建议您改用 labels=FALSE

sextiles <- cut(dates,"2 months",labels = FALSE)
table(sextiles)
# sextiles
#  1  2  3  4  5  6 
# 59 61 61 62 61 61 

如果您希望它们看起来像整数factor(它们是下面带有真正整数的字符串级别),那么也许

sextiles <- factor(sextiles)
head(sextiles)
# [1] 1 1 1 1 1 1
# Levels: 1 2 3 4 5 6
,

感谢@r2evans 提供的见解,我找到了问题的答案。

cut.Date 函数的代码中存在错误。第 31 行到第 41 行处理以月为单位的中断情况:

if (valid == 3L) {
  start$mday <- 1L
  start$isdst <- -1L
  end <- as.POSIXlt(max(x,na.rm = TRUE))
  step <- if (length(by2) == 2L) 
    as.integer(by2[1L])
  else 1L
  end <- as.POSIXlt(end + (31 * step * 86400))
  end$mday <- 1L
  end$isdst <- -1L
  breaks <- as.Date(seq(start,breaks))

第 38 行,end <- as.POSIXlt(end + (31 * step * 86400)) 将结束提前 31 天乘以步长或每个 bin 中的月数进行调整。因为并不是所有的月份都有 31 天,所以有些情况下,最后会被推到足够远的地方,从而产生一个多余的垃圾箱。这可以通过几行代码轻松纠正,正如我们在休息时间为四分之一的情况下所看到的那样。请参阅第 57 至 75 行:

else if (valid == 5L) {
  qtr <- rep(c(0L,3L,6L,9L),each = 3L)
  start$mon <- qtr[start$mon + 1L]
  start$mday <- 1L
  start$isdst <- -1L
  maxx <- max(x,na.rm = TRUE)           # Note this line
  end <- as.POSIXlt(maxx)                # Note this line
  step <- if (length(by2) == 2L) 
    as.integer(by2[1L])
  else 1L
  end <- as.POSIXlt(end + (93 * step * 86400))
  end$mon <- qtr[end$mon + 1L]
  end$mday <- 1L
  end$isdst <- -1L
  breaks <- as.Date(seq(start,paste(step * 3L,"months")))
  lb <- length(breaks)                   # Note this line
  if (maxx < breaks[lb - 1])             # If extra bin
    breaks <- breaks[-lb]                # Then remove extra bin

如果我们使用同样的方法并修改处理break="months"的代码部分:

if (valid == 3L) {
  start$mday <- 1L
  start$isdst <- -1L
  maxx <- max(x,na.rm = TRUE)     # Line added
  end <- as.POSIXlt(maxx)          # Line modified
  step <- if (length(by2) == 2L) 
    as.integer(by2[1L])
  else 1L
  end <- as.POSIXlt(end + (31 * step * 86400))
  end$mday <- 1L
  end$isdst <- -1L
  breaks <- as.Date(seq(start,breaks))
  lb <- length(breaks)             # Line added
  if (maxx < breaks[lb - 1])       # Line added
    breaks <- breaks[-lb]          # Line added

将修改后的函数存储在 cut_Date 中,我们得到以下内容:

dates <- seq(as.Date("2021-1-1"),as.Date("2021-12-31"),by="day")
sextiles <- cut(dates,labels=1:6)
# Error in cut.default(unclass(x),unclass(breaks),labels = labels,right = right,: 
#   lengths of 'breaks' and 'labels' differ
sextiles <- cut_Date(dates,labels=1:6)
table(sextiles)
# sextiles
#  1  2  3  4  5  6 
# 59 61 61 62 61 61

错误已修复!

相关问答

错误1:Request method ‘DELETE‘ not supported 错误还原:...
错误1:启动docker镜像时报错:Error response from daemon:...
错误1:private field ‘xxx‘ is never assigned 按Alt...
报错如下,通过源不能下载,最后警告pip需升级版本 Requirem...