web-dev-qa-db-fra.com

Comment empêcher ifelse () de transformer des 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 classe Date et j'ai été surpris d'obtenir un vecteur 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)

Cela est particulièrement surprenant car l'exécution de l'opération sur l'ensemble du vecteur renvoie un objet 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 Date? Si oui, quelle fonction? Sinon, comment puis-je forcer ifelse à renvoyer un vecteur du même type que l'entrée?

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

131
Zach

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"

Pour plus d'informations sur fifelse, y compris un benchmark, voir NEWS item # 21 for development version 1.12. . Pour l'installation de la version de développement, voir ici .


dplyr::if_else

De dplyr 0.5.0 Notes de version : "[if_else] Ont une sémantique plus stricte que ifelse(): les true et false les arguments doivent ê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" 
99
Henrik

Elle se rapporte à la Value documentée de ifelse:

Un vecteur de la même longueur et des mêmes attributs (y compris les dimensions et "class") que test et les valeurs de données des valeurs de yes ou no. Le mode de la réponse sera contraint de logique pour s'adapter d'abord à toutes les valeurs tirées de yes puis à toutes les valeurs prises de no.

Réduit à ses implications, ifelse fait que les facteurs perdent leurs niveaux 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 construit un if_else dans le complexe magrittr/dplyr/tidyr des paquets de mise en forme des données.

62
42-

L'explication de DWin est parfaite. J'ai tripoté et combattu avec cela pendant un certain temps avant de réaliser que je pouvais simplement 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 m'a semblé un peu "hackish". Mais maintenant, je pense simplement que c'est un petit prix à payer pour les performances que je reçois de ifelse (). De plus, c'est toujours beaucoup plus concis qu'une boucle.

15
JD Long

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 entraîne une grande responsabilité, c'est-à-dire que les conversions de types de matrices 1x1 et/ou de chiffres [quand ils devraient ê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' de ifelse plusieurs fois maintenant et il continue de voler mon temps :-(

FW

6
Fabian Werner

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.

5

La réponse fournie par @ fabian-werner est excellente, mais les objets peuvent avoir plusieurs classes, et "facteur" n'est pas nécessairement 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 afin que la base :: ifelse () conserve les attributs en fonction de la sélection par l'utilisateur des attributs à conserver. La demande est ici: https://bugs.r-project.org/bugzilla/show_bug.cgi?id=16609 - Elle a déjà été signalée comme "WONTFIX" au motif qu'elle a toujours été la façon dont il est maintenant, mais j'ai fourni un argument de suivi sur les raisons pour lesquelles un simple ajout pourrait sauver beaucoup de maux de tête des utilisateurs R. Peut-être que votre "+1" dans ce fil de bogue encouragera l'équipe R Core à jeter un deuxième coup d'œil.

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", dans 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
5
Mekki MacAulay