如何防止ifelse()将Date对象转换为数字对象

我正在使用函数ifelse()来操作date向量。 我期待的结果是类的Date ,并惊讶得到一个numeric向量。 这里是一个例子:

 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) 

这是特别令人惊讶的,因为在整个vector上执行操作会返回一个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) 

我应该使用其他函数来操作Date向量吗? 如果是这样,有什么function? 如果不是,我该如何强制ifelse返回一个与input相同types的向量?

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

你可以使用dplyr::if_else

dplyr 0.5.0发行说明 :“[ if_else ]有更严格的语义ifelse()truefalse参数必须是相同的types,这样的返回types不太令人吃惊,并保留S3向量,如date ”。

 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" 

这在rhelp之前已经出现过,并涉及到ifelse的logging值:一个具有相同长度和属性的向量(包括维度和“类”)作为来自yes或no值的testing和数据值。 答案的模式将从逻辑上被强制,以适应从yes开始的任何值,然后从no开始的任何值。

归结到它的含义, ifelse使因素失去他们的水平和date失去了他们的类,只有他们的模式(数字)被恢复。 试试这个:

 > 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" 

你可以创build一个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" 

后面的说明:我看到哈德利已经在数据整形软件包的magrittr / dplyr / tidyr复合体中build立了一个if_else

迪文的解释是现货。 在我意识到自己的陈述之后,我可以简单地强迫这个class级,

 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()得到的性能回报的一个小的代价。 再加上它比循环更简洁。

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

 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) } 

顺便说一句:ifelse很糟糕…很大的责任,即1x1matrix和/或数字的types转换[当他们应该被添加例如]对我来说是好的,但是这种types的ifelse转换显然是不需要的。 我现在多次碰到同样的“错误”,并且一直在偷窃我的时间:-(

FW

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

 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开发团队提交了一个请求,添加一个logging的选项来让base :: ifelse()根据用户select保留哪些属性来保留属性。 请求是在这里: https ://bugs.r-project.org/bugzilla/show_bug.cgi?id = 16609 – 它已被标记为“WONTFIX”的理由是,它一直是现在的样子,但是我已经提供了一个关于为什么简单的添加可以节省大量R用户头痛的后续论证。 也许你在这个bug线程中的“+1”会鼓励R核心团队再次看看。

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

 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 

之所以这不起作用是因为,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.