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

I use the ifelse() function to control the date vector. I expected the result to have a Date class, and was surprised to get numeric instead. Here is an example:

 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) 

This is especially surprising because executing an operation across the entire vector returns a Date object.

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

Should I use any other function to work with Date vectors? If so, what is the function? If not, how to make ifelse return a vector of the same type as the input?

The help page for ifelse indicates that this is a function, not an error, but I'm still trying to find an explanation of what seemed to me to be surprising.

+131
datetime r if-statement
Jul 12 '11 at 18:11
source share
6 answers

You can use data.table::fifelse ( data.table >= 1.12.3 ) or dplyr::if_else .




data.table::fifelse

Unlike ifelse , fifelse retains the type and class of inputs.

 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" 

For more information on fifelse , including tests, see NEWS No. 21 for version 1.12.3 for development . To install the developer version, see here .




dplyr::if_else

From the dplyr 0.5.0 release dplyr 0.5.0 : "[ if_else ] has more strict semantics that ifelse() : the arguments true and false must be of the same type. This gives a less surprising return type, and saves S3 vectors as dates."

 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" 
+100
Jun 29 '16 at 7:31
source share

This refers to the documented ifelse value :

A vector with the same length and attributes (including dimensions and " class ") as test and data values ​​from yes or no . The response mode will be forcibly taken from the logical one to first transfer any values ​​taken from yes , and then any values ​​taken from no .

Resetting its consequences, ifelse causes factors to lose their levels, and Dates lose their class and only their mode is restored ("numerical"). Try instead:

 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" 

You can create 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" 

Late note: I see that Hadley built if_else in the magrittr / dplyr / tidyr package of data generation packages.

+62
July 42--12 '11 at 18:33
source share

Description DWin - spot. I messed around and struggled with this for a while before I realized that I could just force the class after the ifelse statement:

 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) 
At first it seemed a little "hacky" to me. But now I just think of it as a small price to pay for the performance results that I get from ifelse (). Plus it is still much shorter than the loop.
+15
Jul 12 2018-11-18T00:
source share

The proposed method does not work with factor columns. Id would like to suggest this improvement:

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

By the way: ifelse sucks ... with great power comes great responsibility, i.e. conversions of the types of 1x1 matrices and / or numbers [when they should be added, for example] suit me, but converting this type to ifelse is clearly undesirable. I encountered the same ifelse “error” several times, and it just keeps stealing my time :-(

Fw

+6
Jul 09 '15 at 9:38
source share

The answer provided by @ fabian-werner is great, but objects can have several classes, and the “factor” may not necessarily be the first one returned by class(yes) , so I suggest this little modification to check all the attributes of the class:

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

I also sent a request to the R Development team to add a documented option so that base :: ifelse () saves attributes based on the user's choice of which attributes are saved. Request here: https://bugs.r-project.org/bugzilla/show_bug.cgi?id=16609 - It is already marked as "WONTFIX" on the grounds that it has always been the way it is now, but I introduced the next argument is why simply adding can save a lot of R users ’troubles. Perhaps your“ +1 ”in this error stream will make the R Core team take a second look.

EDIT: here is a more efficient version that allows the user to specify which attributes to save, either "cond" (default behavior ifelse ()), "yes", behavior according to the code above, or "no", for cases when no attributes are better:

 safe_ifelse <- function(cond, yes, no, preserved_attributes = "yes") { # Capture the user 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 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 
+5
Nov 23 '15 at 20:57
source share

The reason this will not work is because the ifelse () function converts values ​​to factors. A good workaround would be to convert it to characters before evaluating.

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

This does not require any library other than R base.

+5
Aug 07 '17 at 11:32 on
source share



All Articles