Cómo evitar que ifelse () convierta objetos Date en objetos numéricos

Estoy usando la función ifelse() para manipular un vector de fecha. Esperaba que el resultado fuera de la clase Date , y me sorprendió obtener un vector numeric lugar. Aquí hay un ejemplo:

 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) 

Esto es especialmente sorprendente porque al realizar la operación en todo el vector se devuelve un objeto 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) 

¿Debería usar alguna otra función para operar en vectores de Date ? Si es así, ¿qué función? Si no, ¿cómo forzar ifelse para devolver un vector del mismo tipo que la entrada?

La página de ayuda para ifelse indica que esta es una característica, no un error, pero todavía estoy luchando por encontrar una explicación para lo que considero que es un comportamiento sorprendente.

Puede usar dplyr::if_else .

De las notas de la versión dplyr 0.5.0 : “[ if_else ] tiene una semántica más estricta que ifelse() : los argumentos true y false deben ser del mismo tipo. Esto da un tipo de retorno menos sorprendente y preserva los vectores S3 como las fechas “.

 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" 

Se relaciona con el valor documentado de ifelse :

Un vector de la misma longitud y atributos (incluidas las dimensiones y ” class “) como valores de test y datos a partir de los valores de yes o no . El modo de la respuesta será coaccionado desde lógico para acomodar primero cualquier valor tomado de yes y luego cualquier valor tomado de no .

Reducido a sus implicaciones, ifelse hace que los factores pierdan sus niveles y las fechas pierden su clase y solo se restaura su modo (“numérico”). Pruebe esto en su lugar:

 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" 

Podrías crear un 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" 

Una nota posterior: veo que Hadley ha construido un if_else en el complejo magrittr / dplyr / tidyr de paquetes de configuración de datos.

La explicación de DWin es perfecta. Jugueteé y luché con esto por un tiempo antes de darme cuenta de que podía simplemente forzar a la clase después de la statement 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) 

Al principio, esto me pareció un poco "hackish". Pero ahora solo lo considero un pequeño precio a pagar por los rendimientos de rendimiento que obtengo de ifelse (). Además, es mucho más conciso que un bucle.

El método sugerido no funciona con columnas de factores. Me gustaría sugerir esta mejora:

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

Por cierto: ifelse apesta ... con gran poder viene una gran responsabilidad, es decir, las conversiones de tipo de matrices 1x1 y / o numéricos [cuando deberían agregarse, por ejemplo] me parecen bien, pero este tipo de conversión en ifelse es claramente no deseado. Me encontré con el mismo 'error' de ifelse varias veces y ahora sigue robando mi tiempo 🙁

FW

La respuesta proporcionada por @ fabian-werner es genial, pero los objetos pueden tener múltiples clases, y “factor” puede no ser necesariamente el primero devuelto por class(yes) , por lo que sugiero esta pequeña modificación para verificar todos los atributos de clase:

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

También he enviado una solicitud al equipo de desarrollo de R para que agregue una opción documentada para que los atributos de reserva de base :: ifelse () se basen en la selección del usuario de qué atributos conservar. La solicitud está aquí: https://bugs.r-project.org/bugzilla/show_bug.cgi?id=16609 - Ya ha sido marcado como "WONTFIX" con el argumento de que siempre ha sido como es ahora, pero proporcioné un argumento de seguimiento sobre por qué una simple adición podría salvar muchos dolores de cabeza a los usuarios de R. Quizás su "+1" en ese hilo de error animará al equipo de R Core a echar un segundo vistazo.

EDITAR: Aquí hay una versión mejor que le permite al usuario especificar qué atributos conservar, ya sea "cond" (comportamiento ifelse () predeterminado), "sí", el comportamiento según el código anterior, o "no", para los casos donde los atributos del valor "no" son mejores:

 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 

La razón por la que esto no funcionará es porque, la función ifelse () convierte los valores en factores. Una buena solución sería convertirlo en caracteres antes de evaluarlo.

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

Esto no requeriría ninguna biblioteca aparte de la base R.