Jak uniemożliwić ifelse () przekształcanie obiektów Date w obiekty liczbowe

161

Używam funkcji ifelse()do manipulowania wektorem daty. Spodziewałem się, że wynik będzie klasowy Datei byłem zaskoczony, że numericzamiast tego otrzymałem wektor. Oto przykład:

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)

Jest to szczególnie zaskakujące, ponieważ wykonanie operacji na całym wektorze zwraca Dateobiekt.

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

Czy powinienem używać innej funkcji do operowania na Datewektorach? Jeśli tak, jaka funkcja? Jeśli nie, jak zmusić ifelsedo zwrócenia wektora tego samego typu co dane wejściowe?

Strona pomocy dla ifelsewskazuje, że jest to funkcja, a nie błąd, ale wciąż staram się znaleźć wyjaśnienie tego, co uważam za zaskakujące zachowanie.

Zach
źródło
4
W if_else()pakiecie dplyr jest teraz funkcja , która może zastąpić ifelse, zachowując poprawne klasy obiektów Date - została opublikowana poniżej jako ostatnia odpowiedź. Zwracam na to uwagę, ponieważ rozwiązuje ten problem, udostępniając funkcję, która jest przetestowana jednostkowo i udokumentowana w pakiecie CRAN, w przeciwieństwie do wielu innych odpowiedzi, które (jak w tym komentarzu) były przed nim.
Sam Firke

Odpowiedzi:

132

Możesz użyć data.table::fifelse( data.table >= 1.12.3) lub dplyr::if_else.


data.table::fifelse

W przeciwieństwie do tego ifelse, fifelsezachowuje typ i klasę danych wejściowych.

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

Z dplyr 0.5.0informacji o wersji :

[ if_else] mają ściślejszą semantykę, która ifelse(): argumenty truei falsemuszą być tego samego typu. Daje to mniej zaskakujący typ zwrotu i zachowuje wektory S3, takie jak daty ”.

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
źródło
2
Zdecydowanie przydatne, nawet jeśli zgubiłem znacznik wyboru. Obecna wersja strony pomocy nie mówi, czego można oczekiwać od argumentów czynnikowych. Głosowałbym za obiektem zwracającym czynnik, który miałby poziomy będące sumą poziomów true's i false' s.
IRTFM
3
Czy istnieje sposób, aby mieć jeden z argumentów if_elsebe NA? NA_NA_double_
Wypróbowałem
11
@Zak Jedną z możliwości jest owinąć NAw as.Date.
Henrik
Jest NA_real_, @roarkz. i @Henrik, twój komentarz tutaj rozwiązał mój problem.
BLT
63

To dotyczy udokumentowanego Stosunek z ifelse:

Wektor o tej samej długości i atrybutach (łącznie z wymiarami i „ class”) jak testi wartości danych z wartości yeslub no. Tryb odpowiedzi zostanie zmieniony z logicznego, aby najpierw uwzględniał wszelkie wartości pobrane, yesa następnie wszystkie wartości wzięte no.

Sprowadzając się do jego konsekwencji, ifelsepowoduje , że czynniki tracą swoje poziomy, a daty tracą swoją klasę i przywracany jest tylko ich tryb („numeryczny”). Spróbuj tego zamiast tego:

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"

Możesz stworzyć 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"

Późniejsza uwaga: widzę, że Hadley wbudował if_elsew kompleks magrittr / dplyr / tidyr pakietów kształtujących dane.

IRTFM
źródło
37
Nieco bardziej elegancka wersja:safe.ifelse <- function(cond, yes, no) structure(ifelse(cond, yes, no), class = class(yes))
hadley
5
Miły. Czy widzisz powód, dla którego nie jest to zachowanie domyślne?
IRTFM,
po prostu uważaj, co wstawiłeś w „tak”, ponieważ miałem NA i to nie zadziałało. Prawdopodobnie lepiej jest przekazać klasę jako parametr niż zakładać, że jest to klasa warunku „tak”.
Denis
1
Nie jestem pewien, czy ostatni komentarz to oznacza. Tylko dlatego, że coś ma wartość NA, nie oznacza, że ​​nie może mieć klasy.
IRTFM
8 lat odkąd ten problem pojawił się i nadal ifelse()nie jest „bezpieczny” .
M--
16

Wyjaśnienie DWina jest trafne. Bawiłem się i walczyłem z tym przez chwilę, zanim zdałem sobie sprawę, że mogę po prostu zmusić klasę po stwierdzeniu 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)

Na początku wydawało mi się to trochę „hakerskie”. Ale teraz myślę o tym jako o niewielkiej cenie za zwroty wydajności, które otrzymuję od ifelse (). Poza tym jest o wiele bardziej zwięzły niż pętla.

JD Long
źródło
ten (ładne, jeśli tak, hackish) technika wydaje się również pomoc z tym, że R jest forcesjonariusze deklaracji wartości elementów w VECTORcelu NAME, ale nie ich klasa .
Greg Minshall
6

Sugerowana metoda nie działa z kolumnami współczynników. Chciałbym zasugerować tę poprawę:

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

Przy okazji: ifelse jest do niczego ... z wielką mocą wiąże się wielka odpowiedzialność, tzn. Konwersja typów macierzy 1x1 i / lub liczb [kiedy należy je na przykład dodać] jest dla mnie w porządku, ale tego typu konwersja w ifelse jest wyraźnie niepożądana. Kilka razy wpadłem na ten sam `` błąd '' ifelse i po prostu kradnie mój czas :-(

FW

Fabian Werner
źródło
To jedyne rozwiązanie, które działa dla mnie na czynniki.
bshor
Pomyślałbym, że poziomy, które mają zostać zwrócone, będą sumą poziomów yesi noi że najpierw sprawdzisz, czy są to oba czynniki. Prawdopodobnie będziesz musiał przekonwertować na znak, a następnie ponownie połączyć z poziomami „uzwiązkowionymi”.
IRTFM
6

Powodem, dla którego to nie zadziała, jest to, że funkcja ifelse () konwertuje wartości na czynniki. Dobrym obejściem byłoby przekonwertowanie go na znaki przed oceną.

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

Nie wymagałoby to żadnej biblioteki poza podstawowym R.

ananthapadmanabhan s
źródło
5

Odpowiedź udzielona przez @ fabian-werner jest świetna, ale obiekty mogą mieć wiele klas, a „czynnik” niekoniecznie musi być pierwszym zwracanym przez class(yes), więc proponuję tę małą modyfikację, aby sprawdzić wszystkie atrybuty klas:

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

Wysłałem również prośbę do zespołu R Development o dodanie udokumentowanej opcji zachowania atrybutów base :: ifelse () na podstawie wyboru przez użytkownika, które atrybuty mają być zachowane. Żądanie jest tutaj: https://bugs.r-project.org/bugzilla/show_bug.cgi?id=16609 - zostało już oflagowane jako „WONTFIX”, ponieważ zawsze było tak, jak jest teraz, ale przedstawiłem kolejny argument wyjaśniający, dlaczego proste dodanie może zaoszczędzić wielu użytkownikom języka R ból głowy. Być może twoje „+1” w tym wątku błędu zachęci zespół R Core do ponownego przyjrzenia się.

EDYCJA: Oto lepsza wersja, która pozwala użytkownikowi określić, które atrybuty mają być zachowane: „cond” (domyślne zachowanie ifelse ()), „tak”, zachowanie zgodnie z powyższym kodem lub „nie” w przypadkach, gdy atrybuty wartości „nie” są lepsze:

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
źródło
1
inherits(y, "factor")może być „bardziej poprawne” niż"factor" %in% class.y
IRTFM
W rzeczy samej. inheritsmoże być najlepszy.
Mekki MacAulay