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

162

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 numericvecteur à 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 tout le vecteur renvoie un Dateobjet.

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 des Datevecteurs? Si oui, quelle fonction? Sinon, comment forcer ifelseà renvoyer un vecteur du même type que l'entrée?

La page d'aide pour ifelseindique qu'il s'agit d'une fonctionnalité, pas d'un bogue, mais j'ai encore du mal à trouver une explication à ce que j'ai trouvé être un comportement surprenant.

Zach
la source
4
Il existe maintenant une fonction if_else()dans le package dplyr qui peut remplacer ifelsetout en conservant les classes correctes d'objets Date - elle est publiée ci-dessous en tant que réponse récente. J'attire l'attention sur cela ici car il résout ce problème en fournissant une fonction qui est testée unitaire et documentée dans un package CRAN, contrairement à de nombreuses autres réponses qui (à partir de ce commentaire) ont été classées avant elle.
Sam Firke

Réponses:

132

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


data.table::fifelse

Contrairement à ifelse, fifelsepré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

À partir des dplyr 0.5.0notes de version :

[ if_else] ont une sémantique plus stricte que ifelse(): les arguments trueet falsedoivent être du même type. Cela donne un type de retour moins surprenant et préserve les vecteurs S3 comme les 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" 
Henrik
la source
2
Certainement utile même si cela m'a fait perdre une coche. La version actuelle de la page d'aide ne dit pas à quoi s'attendre des arguments de facteur. Mon vote serait pour un objet de retour de facteur qui avait des niveaux qui étaient l'union des niveaux de true's et false' s.
IRTFM
3
Existe-t-il un moyen d'avoir l'un des arguments du if_elsebe NA? J'ai essayé les NA_options logiques et rien ne colle et je ne crois pas qu'il y ait unNA_double_
roarkz
11
@Zak Une possibilité est d'envelopper NAdans as.Date.
Henrik
Il y a NA_real_, @roarkz. et @Henrik, votre commentaire ici a résolu mon problème.
BLT
63

Il se rapporte à la valeur documentée de ifelse:

Un vecteur de la même longueur et des mêmes attributs (y compris les dimensions et " class") que testles valeurs de données à partir des valeurs de yesou no. Le mode de réponse sera forcé de logique à accepter d'abord toutes les valeurs prises à partir de yes, puis toutes les valeurs prises à partir de no.

Réduit à ses implications, ifelsefait perdre aux facteurs leur niveau 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 plus tard: je vois que Hadley a intégré un if_elsedans le complexe magrittr / dplyr / tidyr de paquets de mise en forme de données.

IRTFM
la source
37
Version un peu plus élégante:safe.ifelse <- function(cond, yes, no) structure(ifelse(cond, yes, no), class = class(yes))
hadley
5
Agréable. Voyez-vous une raison pour laquelle ce n'est pas le comportement par défaut?
IRTFM
faites juste attention à ce que vous mettez dans «oui» parce que j'avais NA et cela n'a pas fonctionné. Il vaut probablement mieux passer la classe en paramètre que de supposer que c'est la classe de la condition «oui».
Denis
1
Je ne suis pas sûr que ce dernier commentaire signifie. Ce n'est pas parce que quelque chose a une valeur NA qu'il ne peut pas avoir de classe.
IRTFM
8 ans depuis que ce problème est survenu et ifelse()n'est toujours pas «sûr» .
M--
16

L'explication de DWin est parfaite. J'ai tripoté et combattu avec cela 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 me semblait un peu "hackish". Mais maintenant, je pense simplement à cela comme un petit prix à payer pour les rendements de performance que j'obtiens d'ifelse (). De plus, c'est encore beaucoup plus concis qu'une boucle.

JD Long
la source
cette technique (sympa, si oui, hackish) semble également aider avec le fait que la fordéclaration de R attribue la valeur des éléments VECTORà NAME, mais pas à leur classe .
Greg Minshall
6

La méthode suggéré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 craint ... avec une grande puissance vient une grande responsabilité, c'est-à-dire que les conversions de type de matrices 1x1 et / ou numériques [quand il faut les ajouter par exemple] me convient 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 cela continue de me voler mon temps :-(

FW

Fabian Werner
la source
C'est la seule solution qui fonctionne pour moi pour les facteurs.
bshor le
J'aurais pensé que les niveaux à renvoyer seraient l'union des niveaux de yeset noet que vous vérifieriez d'abord qu'il s'agissait des deux facteurs. Vous auriez probablement besoin de convertir en caractère et ensuite de réassembler avec les niveaux "syndiqués".
IRTFM
6

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

ananthapadmanabhan s
la source
5

La réponse fournie par @ fabian-werner est excellente, mais les objets peuvent avoir plusieurs classes, et "factor" n'est pas forcément le premier retourné par class(yes), donc je suggère cette petite modification pour vérifier tous les attributs 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 ajouter une option documentée pour que base :: ifelse () conserve les attributs en fonction de la sélection par l'utilisateur des attributs à 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 il est maintenant, mais j'ai fourni un argument complémentaire sur les raisons pour lesquelles un simple ajout pourrait sauver beaucoup de maux de tête aux utilisateurs de R. Peut-être que votre "+1" dans ce fil de bug encouragera l'équipe R Core à jeter un second regard.

EDIT: Voici une meilleure version qui permet à l'utilisateur de spécifier les attributs à conserver, soit "cond" (comportement ifelse () par défaut), "yes", le comportement selon le code ci-dessus, ou "no", pour les cas où le les attributs de la valeur "non" 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
Mekki MacAulay
la source
1
inherits(y, "factor")pourrait être "plus correct" que"factor" %in% class.y
IRTFM
En effet. inheritspourrait être le meilleur.
Mekki MacAulay