So verhindern Sie, dass ifelse () Datumsobjekte in numerische Objekte verwandelt

161

Ich benutze die Funktion ifelse(), um einen Datumsvektor zu manipulieren. Ich erwartete, dass das Ergebnis von Klasse sein würde Date, und war überrascht, numericstattdessen einen Vektor zu erhalten. Hier ist ein Beispiel:

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)

Dies ist besonders überraschend, da die Ausführung der Operation über den gesamten Vektor ein DateObjekt zurückgibt .

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

Sollte ich eine andere Funktion verwenden, um DateVektoren zu bearbeiten? Wenn ja, welche Funktion? Wenn nicht, wie erzwinge ich ifelsedie Rückgabe eines Vektors des gleichen Typs wie die Eingabe?

Die Hilfeseite für ifelsezeigt an, dass dies eine Funktion ist, kein Fehler, aber ich habe immer noch Schwierigkeiten, eine Erklärung für das zu finden, was ich als überraschendes Verhalten empfunden habe.

Zach
quelle
4
if_else()Das dplyr-Paket enthält jetzt eine Funktion, die ifelsebei Beibehaltung der korrekten Klassen von Datumsobjekten ersetzt werden kann. Sie wird unten als aktuelle Antwort aufgeführt. Ich mache hier darauf aufmerksam, da es dieses Problem löst, indem es eine Funktion bereitstellt, die in einem CRAN-Paket getestet und dokumentiert ist, im Gegensatz zu vielen anderen Antworten, die (ab diesem Kommentar) davor eingestuft wurden.
Sam Firke

Antworten:

132

Sie können data.table::fifelse( data.table >= 1.12.3) oder verwenden dplyr::if_else.


data.table::fifelse

Im Gegensatz dazu ifelsewerden fifelseTyp und Klasse der Eingaben beibehalten.

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

Aus dplyr 0.5.0Versionshinweisen :

[ if_else] haben eine strengere Semantik ifelse(): Die Argumente trueund falsemüssen vom selben Typ sein. Dies ergibt einen weniger überraschenden Rückgabetyp und bewahrt S3-Vektoren wie Datumsangaben ".

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
quelle
2
Auf jeden Fall nützlich, auch wenn ich dadurch ein Häkchen verloren habe. Die aktuelle Version der Hilfeseite sagt nicht aus, was von Faktorargumenten zu erwarten ist. Ich würde für ein Faktor-Rückgabeobjekt stimmen, dessen Ebenen die Vereinigung der Ebenen von true's und false' s Ebenen waren.
IRTFM
3
Gibt es eine Möglichkeit, eines der Argumente der if_elsebe NA zu haben? Ich habe versucht, die logischen NA_Optionen und nichts bleibt hängen und ich glaube nicht, dass es einenNA_double_
roarkz
11
@Zak Eine Möglichkeit besteht darin einzuwickeln NAin as.Date.
Henrik
Es gibt NA_real_@roarkz. und @Henrik, dein Kommentar hier hat mein Problem gelöst.
BLT
63

Es bezieht sich auf den dokumentierten Wert von ifelse:

Ein Vektor mit derselben Länge und denselben Attributen (einschließlich Dimensionen und " class") wie testund Datenwerten aus den Werten von yesoder no. Der Modus der Antwort wird von logisch erzwungen, um zuerst alle Werte yesund dann alle Werte zu berücksichtigen no.

Auf die Implikationen ifelsereduziert, verlieren Faktoren ihre Level und Daten verlieren ihre Klasse und nur ihr Modus ("numerisch") wird wiederhergestellt. Versuchen Sie stattdessen Folgendes:

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"

Sie könnten ein 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"

Ein späterer Hinweis: Ich sehe, dass Hadley einen if_elsein den magrittr / dplyr / tidyr-Komplex von Datenformungspaketen eingebaut hat.

IRTFM
quelle
37
Etwas elegantere Version:safe.ifelse <- function(cond, yes, no) structure(ifelse(cond, yes, no), class = class(yes))
Hadley
5
Nett. Sehen Sie einen Grund, warum dies nicht das Standardverhalten ist?
IRTFM
Sei einfach vorsichtig, was du mit "Ja" eingegeben hast, weil ich NA hatte und es nicht funktioniert hat. Es ist wahrscheinlich besser, die Klasse als Parameter zu übergeben, als anzunehmen, dass es sich um die Klasse der Bedingung "Ja" handelt.
Denis
1
Ich bin mir nicht sicher, ob der letzte Kommentar dies bedeutet. Nur weil etwas einen NA-Wert hat, heißt das nicht, dass es keine Klasse haben kann.
IRTFM
8 Jahre, seit dieses Problem aufgetreten ist und immer noch ifelse()nicht "sicher" ist .
M--
16

DWins Erklärung ist genau richtig. Ich spielte eine Weile damit herum, bevor mir klar wurde, dass ich die Klasse nach der ifelse-Aussage einfach zwingen konnte:

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)

Anfangs fühlte sich das für mich ein wenig "hackisch" an. Aber jetzt sehe ich es nur als einen kleinen Preis für die Performance-Renditen, die ich von ifelse () bekomme. Außerdem ist es immer noch viel prägnanter als eine Schleife.

JD Long
quelle
diese (schön, wenn ja, hackish) scheint Technik auch Hilfe mit der Tatsache , dass R - forAnweisung weist der Wert der Elemente in VECTORzu NAME, nicht aber ihre Klasse .
Greg Minshall
6

Die vorgeschlagene Methode funktioniert nicht mit Faktorspalten. Ich möchte diese Verbesserung vorschlagen:

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

Übrigens: ifelse saugt ... mit großer Kraft geht eine große Verantwortung einher, dh Typkonvertierungen von 1x1-Matrizen und / oder Zahlen [wenn sie zum Beispiel hinzugefügt werden sollten] sind für mich in Ordnung, aber diese Typkonvertierung in ifelse ist eindeutig unerwünscht. Ich bin jetzt mehrmals auf den gleichen 'Bug' von ifelse gestoßen und er stiehlt mir immer wieder meine Zeit :-(

FW

Fabian Werner
quelle
Dies ist die einzige Lösung, die für mich bei Faktoren funktioniert.
bshor
Ich hätte gedacht, dass die zurückzugebenden Ebenen die Vereinigung der Ebenen von yesund sind nound dass Sie zuerst überprüfen würden, ob sie beide Faktoren sind. Sie müssten wahrscheinlich in Charakter konvertieren und sich dann mit den "gewerkschaftlich organisierten" Ebenen neu bündeln.
IRTFM
6

Der Grund, warum dies nicht funktioniert, liegt darin, dass die Funktion ifelse () die Werte in Faktoren konvertiert. Eine gute Problemumgehung wäre, es vor der Auswertung in Zeichen umzuwandeln.

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

Dies würde keine Bibliothek außer Basis R erfordern.

ananthapadmanabhan s
quelle
5

Die Antwort von @ fabian-werner ist großartig, aber Objekte können mehrere Klassen haben, und "Faktor" muss nicht unbedingt der erste sein, der von zurückgegeben wird. Daher empfehle class(yes)ich diese kleine Änderung, um alle Klassenattribute zu überprüfen:

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

Ich habe auch eine Anfrage an das R-Entwicklungsteam gesendet, um eine dokumentierte Option hinzuzufügen, mit der base :: ifelse () Attribute beibehalten soll, basierend auf der Benutzerauswahl, welche Attribute beibehalten werden sollen. Die Anfrage ist hier: https://bugs.r-project.org/bugzilla/show_bug.cgi?id=16609 - Sie wurde bereits als "WONTFIX" gekennzeichnet, da sie immer so war, wie sie jetzt ist. Aber ich habe ein nachfolgendes Argument geliefert, warum eine einfache Hinzufügung viele Kopfschmerzen von R-Benutzern ersparen könnte. Vielleicht ermutigt Ihr "+1" in diesem Bug-Thread das R Core-Team, einen zweiten Blick darauf zu werfen.

BEARBEITEN: Hier ist eine bessere Version, mit der der Benutzer angeben kann, welche Attribute beibehalten werden sollen, entweder "cond" (Standardverhalten von ifelse ()), "yes", das Verhalten gemäß dem obigen Code oder "no" für Fälle, in denen die Attribute des "no" -Werts sind besser:

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
quelle
1
inherits(y, "factor")könnte "korrekter" sein als"factor" %in% class.y
IRTFM
Tatsächlich. inheritskönnte am besten sein.
Mekki MacAulay