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.
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"
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
") quetest
et les valeurs de données des valeurs deyes
ouno
. Le mode de la réponse sera contraint de logique pour s'adapter d'abord à toutes les valeurs tirées deyes
puis à toutes les valeurs prises deno
.
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.
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.
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
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.
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