Comment empêcher ifelse () de transformer des objects Date en objects numériques

J’utilise la fonction ifelse() pour manipuler un vecteur de date. Je m’attendais à ce que le résultat soit de classe Date , et j’ai été surpris d’obtenir un vecteur numeric place. Voici un exemple:

 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) 

Cela est particulièrement surprenant, car l’exécution de l’opération sur l’ensemble du vecteur renvoie un object 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) 

Dois-je utiliser une autre fonction pour opérer sur les vecteurs de Date ? Si oui, quelle fonction? Sinon, comment forcer ifelse à renvoyer un vecteur du même type que l’entrée?

La page d’aide pour ifelse indique qu’il s’agit d’une fonctionnalité et non d’un bogue, mais j’ai toujours du mal à trouver une explication à ce que j’ai trouvé être un comportement surprenant.

Vous pouvez utiliser dplyr::if_else .

A partir des notes de publication de dplyr 0.5.0 : “[ if_else ] sémantique plus ssortingcte que ifelse() : les arguments true et false doivent être du même type. Cela donne un type de retour moins surprenant et préserve les vecteurs S3 comme des 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" 

Il concerne la valeur documentée de ifelse :

Un vecteur de même longueur et atsortingbuts (y compris les dimensions et la ” class “) comme valeurs de test et de données à partir des valeurs de yes ou no . Le mode de la réponse sera contraint de logique pour accueillir d’abord toutes les valeurs sockets à partir de yes , puis toutes les valeurs sockets à partir de no .

Réduit à ses implications, ifelse fait perdre ses niveaux aux facteurs et les dates perdent leur classe et seul leur mode (“numérique”) est restauré. Essayez plutôt ceci:

 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" 

Vous pouvez créer 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" 

Une note ultérieure: Je vois que Hadley a construit un if_else dans le complexe magrittr / dplyr / tidyr de paquets de mise en forme de données.

L’explication de DWin est sur place. J’ai sortingpoté et combattu pendant un moment avant de réaliser que je pouvais forcer la classe après la déclaration 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) 

Au début, cela me semblait un peu "hackish". Mais maintenant, je le considère comme un petit prix à payer pour les rendements que je reçois de ifelse (). De plus, il rest beaucoup plus concis qu'une boucle.

La méthode suggérée ne fonctionne pas avec les colonnes de facteurs. Je voudrais suggérer cette amélioration:

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

Soit dit en passant: ifelse suce ... avec une grande puissance vient une grande responsabilité, c.-à-d. Que les conversions de type masortingces et / ou numériques 1x1 [quand ils doivent être ajoutés par exemple] me conviennent mais cette conversion est clairement indésirable. Je suis tombé sur le même "bug" de ifelse plusieurs fois maintenant et ça continue à voler mon temps 🙁

FW

La réponse fournie par @ fabian-werner est géniale, mais les objects peuvent avoir plusieurs classes, et “factor” n’est pas nécessairement le premier retourné par la class(yes) , alors je suggère cette petite modification pour vérifier tous les atsortingbuts de classe:

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

J'ai également soumis une demande à l'équipe de développement R pour append une option documentée permettant à base :: ifelse () de conserver les atsortingbuts en fonction de la sélection par l'utilisateur des atsortingbuts à préserver. La requête est ici: https://bugs.r-project.org/bugzilla/show_bug.cgi?id=16609 - Elle a déjà été signalée comme "WONTFIX" au motif que cela a toujours été comme ça maintenant, mais j'ai fourni un argument de suivi sur la raison pour laquelle une simple addition peut sauver beaucoup de maux de tête d'utilisateurs R. Peut-être que votre "+1" dans ce fil de bogue encouragera l'équipe R Core à jeter un deuxième coup d'oeil.

EDIT: Voici une meilleure version qui permet à l'utilisateur de spécifier quels atsortingbuts conserver, soit "cond" (comportement ifelse () par défaut), "yes", le comportement selon le code ci-dessus, ou "no", pour les cas où le les atsortingbuts de la valeur "no" sont meilleurs:

 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 raison pour laquelle cela ne fonctionnera pas est que la fonction ifelse () convertit les valeurs en facteurs. Une bonne solution serait de le convertir en caractères avant de l’évaluer.

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

Cela ne nécessiterait aucune bibliothèque en dehors de la base R.