203 votes

Comment empêcher ifelse() de transformer les objets Date en objets numériques ?

J'utilise la fonction ifelse() pour manipuler un vecteur de date. Je m'attendais à ce que le résultat soit de la classe Date et j'ai été surpris de recevoir un numeric à la 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)

Ceci est particulièrement surprenant car l'exécution de l'opération sur l'ensemble du vecteur renvoie un Date objet.

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

Devrais-je utiliser une autre fonction pour opérer sur Date vecteurs ? Si oui, quelle fonction ? Si non, comment puis-je forcer ifelse pour retourner un vecteur du même type que l'entrée ?

La page d'aide de ifelse indique qu'il s'agit d'une fonctionnalité et non d'un bogue, mais je m'efforce toujours de trouver une explication à ce comportement que j'ai trouvé surprenant.

168voto

Henrik Points 12148

Vous pouvez utiliser data.table::fifelse ( data.table >= 1.12.3 ) ou dplyr::if_else .


data.table::fifelse

Contrairement à ifelse , fifelse préserve le type et la classe des entrées.

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

Desde dplyr 0.5.0 notes de mise à jour :

[ if_else ] ont une sémantique plus stricte que ifelse() : le true y false Les arguments doivent être du même type. Cela donne un type de retour moins surprenant, et préserve les vecteurs S3 comme 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"

67voto

BondedDust Points 105234

Il s'agit des documents suivants Valeur de ifelse :

Un vecteur de la même longueur et des mêmes attributs (y compris les dimensions et les " class ") comme test et des valeurs de données à partir des valeurs de yes o no . Le mode de la réponse sera forcé à partir de la logique pour s'adapter d'abord à toutes les valeurs prises à partir de yes et ensuite toutes les valeurs prises dans no .

Réduit à ses implications, ifelse fait perdre aux facteurs leurs niveaux et aux dates 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 pourriez 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 plus tard : Je vois que Hadley a construit une if_else dans le complexe de paquets de mise en forme de données magrittr/dplyr/tidyr.

16voto

JD Long Points 20477

L'explication de DWin est parfaite. Je me suis battu avec ça pendant un moment avant de réaliser que je pouvais simplement forcer la classe après l'instruction 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 m'a semblé un peu "bidouilleur". Mais maintenant, je pense que c'est un petit prix à payer pour les retours de performance que j'obtiens de ifelse(). De plus, c'est toujours beaucoup plus concis qu'une boucle.

9voto

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

6voto

Fabian Werner Points 401

La méthode proposée ne fonctionne pas avec les colonnes de facteurs. J'aimerais 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)
}

Au fait : ifelse est nul... avec un grand pouvoir vient une grande responsabilité, c'est à dire que les conversions de type de matrices 1x1 et/ou de numériques [quand ils doivent être ajoutés par exemple] me conviennent mais cette conversion de type dans ifelse est clairement indésirable. Je suis tombé sur le même 'bug' d'ifelse plusieurs fois maintenant et il continue à me faire perdre mon temps :-(

FW

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X