Come impedire a ifelse () di trasformare gli oggetti Date in oggetti numerici


162

Sto usando la funzione ifelse()per manipolare un vettore data. Mi aspettavo che il risultato fosse di classe Datee invece sono rimasto sorpreso di ottenere un numericvettore. Ecco un esempio:

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)

Ciò è particolarmente sorprendente perché l'esecuzione dell'operazione sull'intero vettore restituisce un Dateoggetto.

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

Dovrei usare qualche altra funzione per operare sui Datevettori? In tal caso, quale funzione? In caso contrario, come posso forzare ifelsea restituire un vettore dello stesso tipo dell'input?

La pagina di aiuto per ifelseindica che questa è una funzionalità, non un bug, ma sto ancora cercando di trovare una spiegazione per quello che ho scoperto essere un comportamento sorprendente.


4
Ora c'è una funzione if_else()nel pacchetto dplyr che può sostituire ifelsepur mantenendo le classi corrette di oggetti Date - è pubblicata di seguito come una risposta recente. Sto attirando l'attenzione su di esso in quanto risolve questo problema fornendo una funzione che è testata in unità e documentata in un pacchetto CRAN, a differenza di molte altre risposte che (a partire da questo commento) sono state classificate prima di esso.
Sam Firke,

Risposte:


132

È possibile utilizzare data.table::fifelse( data.table >= 1.12.3) o dplyr::if_else.


data.table::fifelse

Diversamente ifelse, fifelseconserva il tipo e la classe degli input.

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

Dalle dplyr 0.5.0note di rilascio :

[ if_else] hanno una semantica più rigorosa che ifelse(): gli argomenti truee falsedevono essere dello stesso tipo. Ciò fornisce un tipo di ritorno meno sorprendente e conserva i vettori S3 come le date ".

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" 

2
Sicuramente utile anche se mi ha fatto perdere un segno di spunta. La versione corrente della pagina di aiuto non dice cosa aspettarsi dagli argomenti dei fattori. Il mio voto sarebbe per un oggetto di ritorno fattore che avesse livelli che erano l'unione dei livelli dei livelli di true"s false".
IRTFM,

3
C'è un modo per avere uno degli argomenti di if_elseessere NA? Ho tentato le NA_opzioni logiche e non c'è niente che non si attacca e non credo che ci sia unNA_double_
roarkz,

11
@Zak Una possibilità è quella di avvolgere NAin as.Date.
Henrik,

C'è NA_real_, @roarkz. e @Henrik, il tuo commento qui ha risolto il mio problema.
BLT

63

Si riferisce al valore documentato di ifelse:

Un vettore della stessa lunghezza e attributi (inclusi dimensioni e " class") teste valori di dati dai valori di yeso no. La modalità della risposta sarà forzata dalla logica per accogliere prima qualsiasi valore preso da yese poi qualsiasi valore preso da no.

Ridotto alle sue implicazioni, ifelsefa perdere i livelli ai fattori e le date perdono la loro classe e viene ripristinata solo la loro modalità ("numerica"). Prova questo invece:

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"

È possibile creare 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"

Una nota successiva: vedo che Hadley ha integrato if_elsenel complesso magrittr / dplyr / tidyr dei pacchetti di data shaping.


37
Versione un po 'più elegante:safe.ifelse <- function(cond, yes, no) structure(ifelse(cond, yes, no), class = class(yes))
Hadley,

5
Bello. Vedi qualche motivo per cui questo non è il comportamento predefinito?
IRTFM,

fai solo attenzione a cosa hai inserito "sì" perché avevo NA e non ha funzionato. Probabilmente è meglio passare la classe come parametro piuttosto che supporre che sia la classe della condizione "yes".
Denis,

1
Non sono sicuro che l'ultimo commento significhi. Solo perché qualcosa ha un valore NA non significa che non può avere una classe.
IRTFM,

8 anni da quando è emerso questo problema e ifelse()non è ancora "sicuro" .
M--

16

La spiegazione di DWin è perfetta. Ho armeggiato e combattuto per un po 'prima di rendermi conto che potevo semplicemente forzare la classe dopo l'affermazione 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)

All'inizio mi è sembrato un po '"hacker". Ma ora penso solo a un piccolo prezzo da pagare per i rendimenti delle prestazioni che ottengo da ifelse (). Inoltre è ancora molto più conciso di un loop.


questo (bello, se sì, hacker) tecnica sembra anche aiuto con il fatto che di R forassegna dichiarazione del valore di elementi in VECTORa NAME, ma non la loro classe .
Greg Minshall,

6

Il metodo suggerito non funziona con le colonne dei fattori. Mi piacerebbe suggerire questo miglioramento:

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

A proposito: ifelse fa schifo ... da un grande potere derivano grandi responsabilità, cioè le conversioni di tipo di matrici e / o numeri numerici 1x1 [quando dovrebbero essere aggiunti per esempio] mi va bene, ma questa conversione di tipo in ifelse è chiaramente indesiderata. Mi sono imbattuto nello stesso 'bug' di ifelse più volte e continua a rubare il mio tempo :-(

FW


Questa è l'unica soluzione che funziona per me per fattori.
bshor,

Avrei pensato che i livelli da restituire sarebbe l'unione dei livelli di yese noe che si dovrebbe prima verificare che erano entrambi i fattori. Probabilmente avresti bisogno di convertirti in personaggio e poi ridondare con i livelli "sindacalizzati".
IRTFM,

6

Il motivo per cui questo non funzionerà è perché, la funzione ifelse () converte i valori in fattori. Una soluzione alternativa sarebbe quella di convertirlo in caratteri prima di valutarlo.

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

Ciò non richiederebbe alcuna libreria oltre alla base R.


5

La risposta fornita da @ fabian-werner è ottima, ma gli oggetti possono avere più classi e "fattore" potrebbe non essere necessariamente il primo restituito da class(yes), quindi suggerisco questa piccola modifica per controllare tutti gli attributi di 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)
    }

Ho anche inviato una richiesta al team di sviluppo R per aggiungere un'opzione documentata per mantenere gli attributi di base :: ifelse () in base alla selezione dell'utente di quali attributi conservare. La richiesta è qui: https://bugs.r-project.org/bugzilla/show_bug.cgi?id=16609 - È già stata contrassegnata come "WONTFIX" in quanto è sempre stata così com'è ora, ma ho fornito un argomento di follow-up sul perché una semplice aggiunta potrebbe salvare molti mal di testa agli utenti R. Forse il tuo "+1" in quel thread di bug incoraggerà il team R Core a dare una seconda occhiata.

EDIT: ecco una versione migliore che consente all'utente di specificare quali attributi conservare, "cond" (comportamento ifelse () predefinito), "yes", il comportamento secondo il codice sopra o "no", nei casi in cui il gli attributi del valore "no" sono migliori:

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

1
inherits(y, "factor")potrebbe essere "più corretto" di"factor" %in% class.y
IRTFM il

Infatti. inheritspotrebbe essere il migliore.
Mekki MacAulay,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.