Como impedir que ifelse () transforme objetos Date em objetos numéricos

161

Estou usando a função ifelse()para manipular um vetor de data. Eu esperava que o resultado fosse de classe Datee fiquei surpreso ao obter um numericvetor. Aqui está um exemplo:

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)

Isso é especialmente surpreendente porque a execução da operação em todo o vetor retorna um Dateobjeto.

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

Devo estar usando alguma outra função para operar em Datevetores? Se sim, qual função? Caso contrário, como forço ifelsea retornar um vetor do mesmo tipo que a entrada?

A página de ajuda para ifelseindica que esse é um recurso, não um bug, mas ainda estou lutando para encontrar uma explicação para o que achei um comportamento surpreendente.

Zach
fonte
4
Agora existe uma função if_else()no pacote dplyr que pode substituir ifelse, mantendo as classes corretas dos objetos Date - ela é postada abaixo como resposta recente. Estou chamando a atenção aqui, pois resolve esse problema, fornecendo uma função que é testada em unidade e documentada em um pacote CRAN, diferente de muitas outras respostas que (a partir deste comentário) foram classificadas à frente.
Sam Firke 26/08/16

Respostas:

132

Você pode usar data.table::fifelse( data.table >= 1.12.3) ou dplyr::if_else.


data.table::fifelse

Ao contrário ifelse, fifelsepreserva o tipo e a classe das entradas.

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

De dplyr 0.5.0 notas de versão :

[ if_else] têm semânticas mais rígidas que ifelse(): os argumentos truee falsedevem ser do mesmo tipo. Isso fornece um tipo de retorno menos surpreendente e preserva os vetores S3 como datas ".

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
fonte
2
Definitivamente útil, mesmo que isso me soltasse uma marca de seleção. A versão atual da página de ajuda não diz o que esperar dos argumentos dos fatores. Meu voto seria para um objeto de retorno de fator que tivesse níveis que fossem a união dos níveis de true's e false' s.
IRTFM 28/08/16
3
Existe uma maneira de ter um dos argumentos do if_elsebe NA? Eu tenho tentado as lógicas NA_opções e nada está furando e eu não acredito que há umaNA_double_
roarkz
11
@Zak Uma possibilidade é envolver NA-se as.Date.
Henrik
Existe NA_real_, @roarkz. e @ Henrik, seu comentário aqui resolveu meu problema.
BLT
63

Está relacionado ao valor documentado de ifelse:

Um vetor do mesmo comprimento e atributos (incluindo dimensões e " class") teste valores de dados dos valores de yesou no. O modo da resposta será coagido da lógica para acomodar primeiro quaisquer valores retirados yese, em seguida, quaisquer valores retirados no.

Resumindo, suas implicações ifelsefazem com que os fatores percam seus níveis e as Datas percam sua classe e apenas seu modo ("numérico") é restaurado. Tente isso:

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"

Você pode criar um 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"

Uma observação posterior: vejo que Hadley incorporou um if_elsecomplexo magrittr / dplyr / tidyr de pacotes de modelagem de dados.

IRTFM
fonte
37
Versão um pouco mais elegante:safe.ifelse <- function(cond, yes, no) structure(ifelse(cond, yes, no), class = class(yes))
hadley
5
Agradável. Você vê alguma razão para que esse não seja o comportamento padrão?
IRTFM
apenas tome cuidado com o que você colocou "sim" porque eu tinha NA e não funcionou. Provavelmente é melhor passar a classe como parâmetro do que assumir que é a classe da condição "yes".
Denis
1
Não tenho certeza se esse último comentário significa isso. Só porque algo tem um valor de NA não significa que não pode ter uma classe.
IRTFM
8 anos desde que esse problema surgiu e ainda ifelse()não é "seguro" .
M--
16

A explicação de DWin está no local. Eu brinquei e lutei com isso por um tempo antes de perceber que poderia simplesmente forçar a classe após a declaração 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)

No começo, isso me pareceu um pouco "tolo". Mas agora eu penso nisso como um pequeno preço a pagar pelos retornos de desempenho que recebo da ifelse (). Além disso, ainda é muito mais conciso do que um loop.

JD Long
fonte
isso (bom, se, sim, hackish) técnica parece também ajuda com o facto de R forcessionários declaração do valor dos itens em VECTORque NAME, mas não a sua classe .
Greg Minshall
6

O método sugerido não funciona com colunas de fatores. Gostaria de sugerir esta melhoria:

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 propósito: ifelse é uma merda ... com grande poder, vem uma grande responsabilidade, ou seja, conversões de tipo de matrizes 1x1 e / ou numéricos [quando elas devem ser adicionadas por exemplo] é aceitável para mim, mas essa conversão de tipo em ifelse é claramente indesejada. Eu encontrei o mesmo 'bug' do ifelse várias vezes agora e ele continua roubando meu tempo :-(

FW

Fabian Werner
fonte
Esta é a única solução que funciona para mim por fatores.
bshor
Eu pensaria que os níveis a serem retornados seriam a união dos níveis de yese noe que você primeiro verificaria se ambos eram fatores. Você provavelmente precisaria converter para caractere e depois reorganizar com os níveis "sindicalizados".
IRTFM 27/09/16
6

A razão pela qual isso não funciona é porque, a função ifelse () converte os valores em fatores. Uma boa solução seria convertê-lo em caracteres antes de avaliá-lo.

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

Isso não exigiria nenhuma biblioteca além da base R.

ananthapadmanabhan s
fonte
5

A resposta fornecida por @ fabian-werner é ótima, mas os objetos podem ter várias classes e "fator" pode não ser necessariamente o primeiro retornado por class(yes), então sugiro esta pequena modificação para verificar todos os atributos 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)
    }

Também enviei uma solicitação com a equipe de desenvolvimento do R para adicionar uma opção documentada para ter atributos base :: ifelse () preserve com base na seleção do usuário de quais atributos preservar. A solicitação está aqui: https://bugs.r-project.org/bugzilla/show_bug.cgi?id=16609 - Ela já foi sinalizada como "WONTFIX", alegando que sempre foi dessa maneira, mas forneci um argumento de acompanhamento sobre por que uma simples adição pode economizar muitas dores de cabeça dos usuários do R. Talvez o seu "+1" nesse segmento de bug incentive a equipe do R Core a dar uma segunda olhada.

EDIT: Aqui está uma versão melhor que permite ao usuário especificar quais atributos preservar, "cond" (comportamento padrão ifelse ()), "yes", o comportamento conforme o código acima ou "no", nos casos em que o atributos do valor "no" são melhores:

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
fonte
1
inherits(y, "factor")pode ser "mais correto" do que"factor" %in% class.y
IRTFM 28/08/16
De fato. inheritspode ser melhor.
Mekki MacAulay 28/08