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

162

Estoy usando la función ifelse()para manipular un vector de fecha. Esperaba que el resultado fuera de clase Date, y me sorprendió obtener un numericvector en su 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 realizar la operación en todo el vector devuelve un Dateobjeto.

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

¿Debo estar usando alguna otra función para operar en Datevectores? Si es así, ¿qué función? Si no es así, ¿cómo fuerzo ifelsea devolver un vector del mismo tipo que la entrada?

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

Zach
fuente
44
Ahora hay una función if_else()en el paquete dplyr que puede sustituir a la ifelsevez que conserva las clases correctas de objetos Date: se publica a continuación como una respuesta reciente. Estoy llamando la atención aquí, ya que resuelve este problema al proporcionar una función que está probada y documentada en un paquete CRAN, a diferencia de muchas otras respuestas que (a partir de este comentario) se clasificaron por delante.
Sam Firke

Respuestas:

132

Puede usar data.table::fifelse( data.table >= 1.12.3) o dplyr::if_else.


data.table::fifelse

A diferencia ifelse, fifelseconserva el tipo y la clase de las entradas.

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

De las dplyr 0.5.0notas de la versión :

[ if_else] tienen una semántica más estricta que ifelse(): los argumentos truey falsedeben ser del mismo tipo. Esto proporciona un tipo de retorno menos sorprendente y conserva los vectores S3 como 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" 
Henrik
fuente
2
Definitivamente útil incluso si me hizo perder una marca de verificación. La versión actual de la página de ayuda no dice qué esperar de los argumentos de factor. Mi voto sería por un objeto de devolución de factores que tuviera niveles que fueran la unión de los niveles de true'sy false'.
IRTFM
3
¿Hay alguna manera de tener uno de los argumentos de if_elseser NA? He intentado las NA_opciones lógicas y no hay nada pegado y no creo que haya unNA_double_
roarkz
11
@Zak Una posibilidad es envolver NAen as.Date.
Henrik
Hay NA_real_, @roarkz. y @Henrik, tu comentario aquí resolvió mi problema.
BLT
63

Se relaciona con el valor documentado de ifelse:

Un vector de la misma longitud y atributos (incluidas dimensiones y " class") que testlos valores de datos de los valores de yeso no. El modo de la respuesta se convertirá de lógico para acomodar primero cualquier valor tomado de yesy luego cualquier valor tomado de no.

Reducido a sus implicaciones, los ifelsefactores pierden sus niveles y las fechas pierden su clase y solo se restaura su modo ("numérico"). Intenta 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 incorporado if_elseel complejo magrittr / dplyr / tidyr de paquetes de modelado de datos.

IRTFM
fuente
37
Versión algo más elegante:safe.ifelse <- function(cond, yes, no) structure(ifelse(cond, yes, no), class = class(yes))
hadley
55
Agradable. ¿Ves alguna razón por la cual ese no es el comportamiento predeterminado?
IRTFM
solo ten cuidado con lo que pones en "sí" porque tenía NA y no funcionó. Probablemente sea mejor pasar la clase como parámetro que asumir que es la clase de la condición "sí".
Denis
1
No estoy seguro de que el último comentario que esto significa. El hecho de que algo tenga un valor de NA no significa que no pueda tener una clase.
IRTFM
8 años desde que surgió este problema y todavía ifelse()no es "seguro" .
M--
16

La explicación de DWin es acertada. 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 declaración 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 "hack". Pero ahora solo lo considero un pequeño precio a pagar por los rendimientos de rendimiento que obtengo de ifelse (). Además, sigue siendo mucho más conciso que un bucle.

JD Long
fuente
Esta técnica (agradable, si, sí, hack) parece ayudar también con el hecho de que la fordeclaración de R asigna el valor de los elementos VECTORa NAME, pero no su clase .
Greg Minshall
6

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 conlleva una gran responsabilidad, es decir, las conversiones de tipo de matrices 1x1 y / o numéricos [cuando deberían agregarse, por ejemplo] está bien para mí, pero esta conversión de tipo en ifelse es claramente no deseada. Me topé con el mismo 'error' de ifelse varias veces ahora y sigue robando mi tiempo :-(

FW

Fabian Werner
fuente
Esta es la única solución que me funciona para los factores.
bshor
Yo hubiera pensado que los niveles sean devueltos sería la unión de los niveles de yesy noy que usted primero comprobar para ver que eran ambos factores. Probablemente necesite convertir a personaje y luego volver a combinar con los niveles "sindicalizados".
IRTFM
6

La razón por la cual 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.

ananthapadmanabhan m
fuente
5

La respuesta proporcionada por @ fabian-werner es excelente, pero los objetos pueden tener varias clases, y "factor" puede no ser necesariamente el primero que devuelva 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 con el equipo de Desarrollo de R para agregar una opción documentada para tener base :: ifelse () preservar atributos en función de la selección del usuario de qué atributos preservar. La solicitud está aquí: https://bugs.r-project.org/bugzilla/show_bug.cgi?id=16609 - Ya se ha marcado como "WONTFIX" porque siempre ha sido así, pero he proporcionado un argumento de seguimiento sobre por qué una simple adición podría ahorrarle muchos dolores de cabeza a los usuarios de R. Quizás su "+1" en ese hilo de errores alentará al equipo de R Core a echar un segundo vistazo.

EDITAR: Aquí hay una versión mejor que permite al usuario especificar qué atributos conservar, ya sea "cond" (comportamiento predeterminado ifelse ()), "sí", el comportamiento según el código anterior o "no", para los casos en que 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
Mekki MacAulay
fuente
1
inherits(y, "factor")podría ser "más correcto" que"factor" %in% class.y
IRTFM
En efecto. inheritspodría ser el mejor
Mekki MacAulay