如何防止 ifelse() 将 Date 对象变成数值对象

How to prevent ifelse() from turning Date objects into numeric objects

提问人:Zach 提问时间:7/13/2011 最后编辑:HenrikZach 更新时间:10/20/2023 访问量:55648

问:

我正在使用该函数来操作日期向量。我以为结果是类的,但很惊讶地得到了一个向量。下面是一个示例:ifelse()Datenumeric

dates <- as.Date(c('2011-01-01', '2011-01-02', '2011-01-03', '2011-01-04', '2011-01-05'))
dates <- ifelse(dates == '2011-01-01', dates - 1, dates)
str(dates)

这尤其令人惊讶,因为对整个向量执行操作会返回一个对象。Date

dates <- as.Date(c('2011-01-01', '2011-01-02', '2011-01-03', '2011-01-04','2011-01-05'))
dates <- dates - 1
str(dates)

我应该使用其他函数来对向量进行操作吗?如果是这样,有什么功能?如果没有,如何强制返回与输入类型相同的向量?Dateifelse

的帮助页面表明这是一个功能,而不是一个错误,但我仍在努力寻找对我发现的令人惊讶的行为的解释。ifelse

r 日期 时间 if 语句

评论

14赞 Sam Firke 8/27/2016
现在 dplyr 包中有一个函数可以替代同时保留正确的 Date 对象类 - 它作为最近的答案发布在下面。我在这里提请注意它,因为它通过提供一个经过单元测试并记录在 CRAN 包中的功能来解决这个问题,这与许多其他答案(截至本评论时)排在它之前不同。if_else()ifelse

答:

70赞 IRTFM 7/13/2011 #1

它与以下记录的值有关:ifelse

长度和属性(包括维度和 “”)与 或 值的数据值相同。答案的模式将被强制从逻辑中强制化,以首先容纳从中获取的任何值,然后从中获取任何值。classtestyesnoyesno

归根结底,它使因子失去其水平,日期失去其类,只有它们的模式(“数字”)被恢复。请尝试以下操作:ifelse

dates[dates == '2011-01-01'] <- dates[dates == '2011-01-01'] - 1
str(dates)
# Date[1:5], format: "2010-12-31" "2011-01-02" "2011-01-03" "2011-01-04" "2011-01-05"

您可以创建一个:safe.ifelse

safe.ifelse <- function(cond, yes, no){ class.y <- class(yes)
                                  X <- ifelse(cond, yes, no)
                                  class(X) <- class.y; return(X)}

safe.ifelse(dates == '2011-01-01', dates - 1, dates)
# [1] "2010-12-31" "2011-01-02" "2011-01-03" "2011-01-04" "2011-01-05"

稍后的说明:我看到 Hadley 在数据整形包的 magrittr/dplyr/tidyr 复合体中构建了一个,它确实保留了后续的类。if_else

评论

42赞 hadley 7/13/2011
更优雅的版本:safe.ifelse <- function(cond, yes, no) structure(ifelse(cond, yes, no), class = class(yes))
7赞 IRTFM 7/13/2011
好。你认为这不是默认行为有什么原因吗?
0赞 Denis 2/26/2019
只是要小心你输入的“是”,因为我有 NA 并且它不起作用。将类作为参数传递可能比假设它是“yes”条件的类更好。
1赞 IRTFM 2/27/2019
我不确定最后一条评论是什么意思。仅仅因为某些东西具有 NA 值并不意味着它不能有类。
0赞 Gregor Thomas 5/3/2023
@IRTFM我可以看到一个问题,那么作为结果所需类的假设不一定正确。x <- 1:10; safe.ifelse(x < 5, NA, x)class(NA)
17赞 JD Long 7/13/2011 #2

DWin的解释是正确的。我摆弄了一会儿,然后才意识到我可以简单地在ifelse语句之后强迫全班:

dates <- as.Date(c('2011-01-01','2011-01-02','2011-01-03','2011-01-04','2011-01-05'))
dates <- ifelse(dates=='2011-01-01',dates-1,dates)
str(dates)
class(dates)<- "Date"
str(dates)

起初,这对我来说有点“黑客”。但现在我只是把它看作是我从ifelse()获得的性能回报所付出的一个小代价。另外,它仍然比循环简洁得多。

评论

0赞 Greg Minshall 6/12/2019
这种(很好,如果,是的,hackish)技术似乎也有助于解决这样一个事实,即 R 的语句将项的分配给 ,而不是它们的forVECTORNAME
6赞 Fabian Werner 7/9/2015 #3

建议的方法不适用于因子列。我想建议这个改进:

safe.ifelse <- function(cond, yes, no) {
  class.y <- class(yes)
  if (class.y == "factor") {
    levels.y = levels(yes)
  }
  X <- ifelse(cond,yes,no)
  if (class.y == "factor") {
    X = as.factor(X)
    levels(X) = levels.y
  } else {
    class(X) <- class.y
  }
  return(X)
}

顺便说一句:如果其他很烂......权力越大,责任越大,即 1x1 矩阵和/或数字的类型转换 [例如,当它们应该添加时] 对我来说是可以的,但 ifelse 中的这种类型转换显然是不需要的。我现在多次遇到同样的“错误”,它只是不断偷走我的时间:-(

固件

评论

0赞 bshor 1/29/2016
这是唯一适合我的因素解决方案。
0赞 IRTFM 9/27/2016
我本来以为要返回的水平将是 和 的水平的并集,并且您将首先检查它们是否都是因子。您可能需要转换为角色,然后与“工会化”级别重新捆绑。yesno
5赞 Mekki MacAulay 11/24/2015 #4

@fabian-werner 提供的答案很好,但是对象可以有多个类,而 “factor” 不一定是 返回的第一个类,所以我建议做这个小修改来检查所有类属性:class(yes)

safe.ifelse <- function(cond, yes, no) {
      class.y <- class(yes)
      if ("factor" %in% class.y) {  # Note the small condition change here
        levels.y = levels(yes)
      }
      X <- ifelse(cond,yes,no)
      if ("factor" %in% class.y) {  # Note the small condition change here
        X = as.factor(X)
        levels(X) = levels.y
      } else {
        class(X) <- class.y
      }
      return(X)
    }

我还向 R 开发团队提交了一个请求,要求添加一个记录在案的选项,以便根据用户选择要保留的属性来保留 base::ifelse() 属性。请求在这里: https://bugs.r-project.org/bugzilla/show_bug.cgi?id=16609 - 它已经被标记为“WONTFIX”,理由是它一直是现在的样子,但我提供了一个后续论据,说明为什么一个简单的添加可能会为很多 R 用户省去麻烦。也许您在该错误线程中的“+1”会鼓励 R Core 团队重新审视。

编辑:这是一个更好的版本,它允许用户指定要保留的属性,“cond”(默认的ifelse()行为),“yes”,根据上面的代码的行为,或“no”,对于“no”值的属性更好:

safe_ifelse <- function(cond, yes, no, preserved_attributes = "yes") {
    # Capture the user's choice for which attributes to preserve in return value
    preserved           <- switch(EXPR = preserved_attributes, "cond" = cond,
                                                               "yes"  = yes,
                                                               "no"   = no);
    # Preserve the desired values and check if object is a factor
    preserved_class     <- class(preserved);
    preserved_levels    <- levels(preserved);
    preserved_is_factor <- "factor" %in% preserved_class;

    # We have to use base::ifelse() for its vectorized properties
    # If we do our own if() {} else {}, then it will only work on first variable in a list
    return_obj <- ifelse(cond, yes, no);

    # If the object whose attributes we want to retain is a factor
    # Typecast the return object as.factor()
    # Set its levels()
    # Then check to see if it's also one or more classes in addition to "factor"
    # If so, set the classes, which will preserve "factor" too
    if (preserved_is_factor) {
        return_obj          <- as.factor(return_obj);
        levels(return_obj)  <- preserved_levels;
        if (length(preserved_class) > 1) {
          class(return_obj) <- preserved_class;
        }
    }
    # In all cases we want to preserve the class of the chosen object, so set it here
    else {
        class(return_obj)   <- preserved_class;
    }
    return(return_obj);

} # End safe_ifelse function

评论

1赞 IRTFM 8/29/2016
inherits(y, "factor")可能比“更正确”"factor" %in% class.y
0赞 Mekki MacAulay 8/29/2016
事实上。 可能是最好的。inherits
192赞 Henrik 6/29/2016 #5

您可以使用 () 或 .data.table::fifelsedata.table >= 1.12.3dplyr::if_else


data.table::fifelse

与 不同,保留输入的类型和类。ifelsefifelse

library(data.table)
dates <- fifelse(dates == '2011-01-01', dates - 1, dates)
str(dates)
# Date[1:5], format: "2010-12-31" "2011-01-02" "2011-01-03" "2011-01-04" "2011-01-05"

dplyr::if_else

来自 dplyr 0.5.0 发行说明

[if_else] 具有更严格的语义:和 参数必须是相同的类型。这给出了一个不那么令人惊讶的返回类型,并保留了 S3 向量,如日期”。ifelse()truefalse

library(dplyr)
dates <- if_else(dates == '2011-01-01', dates - 1, dates)
str(dates)
# Date[1:5], format: "2010-12-31" "2011-01-02" "2011-01-03" "2011-01-04" "2011-01-05" 

评论

4赞 roarkz 6/27/2017
有没有办法让 be NA 的参数之一?我已经尝试了合乎逻辑的选项,但没有什么坚持下去,我不相信有一个if_elseNA_NA_double_
17赞 Henrik 7/10/2017
@Zak 一种可能性是换行。NAas.Date
0赞 BLT 5/8/2018
有,@roarkz。@Henrik,你在这里的评论解决了我的问题。NA_real_
11赞 ananthapadmanabhan s 8/7/2017 #6

这不起作用的原因是,ifelse() 函数将值转换为因子。一个很好的解决方法是在评估之前将其转换为字符。

dates <- as.Date(c('2011-01-01',
                   '2011-01-02',
                   '2011-01-03',
                   '2011-01-04',
                   '2011-01-05'))
dates_new <- dates - 1
dates <- as.Date(ifelse(dates == '2011-01-01',
                        as.character(dates_new),
                        as.character(dates)))

除了基本 R 之外,这不需要任何库。

0赞 sashahafner 2/20/2022 #7

为什么不在这里使用索引呢?

> dates <- as.Date(c('2011-01-01', '2011-01-02', '2011-01-03', '2011-01-04', '2011-01-05'))
> dates[dates == '2011-01-01'] <- NA
> str(dates)
 Date[1:5], format: NA "2011-01-02" "2011-01-03" "2011-01-04" "2011-01-05"