Como evitar que ifelse () transformando objects Date em objects numéricos

Eu estou usando a function ifelse() para manipular um vetor de data. Eu esperava que o resultado fosse da class Date e fiquei surpreso ao receber um vetor numeric . 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 executar a operação em todo o vetor retorna um object 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) 

Devo estar usando alguma outra function para operar em vetores de Date ? Se sim, que function? Se não, como forçar ifelse a retornar um vetor do mesmo tipo da input?

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

Você pode usar dplyr::if_else .

A partir dplyr 0.5.0 notas de lançamento do dplyr 0.5.0 : “[ if_else ] tem semânticas mais estritas que ifelse() : os argumentos true e false devem ser do mesmo tipo. Isso dá 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" 

Relaciona-se com o valor documentado de ifelse :

Um vetor do mesmo comprimento e atributos (incluindo dimensões e ” class “) como valores de test e dados dos valores de yes ou no . O modo da resposta será coagido a partir da lógica para acomodar primeiro quaisquer valores tirados do yes e, em seguida, quaisquer valores tirados do no .

ifelse suas implicações, ifelse faz com que os fatores percam seus níveis e as Datas perdem sua class e somente 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ê poderia 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 nota posterior: vejo que Hadley construiu um if_else no complexo magrittr / dplyr / tidyr de pacotes de modelagem de dados.

A explicação de DWin está no ponto. Eu brinquei e lutei com isso por um tempo antes de perceber que poderia simplesmente forçar a class 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 "agressivo". Mas agora eu só penso nisso como um pequeno preço a pagar pelos retornos de desempenho que recebo do ifelse (). Além disso, ainda é muito mais conciso do que um loop.

O método sugerido não funciona com colunas de fatores. Eu gostaria de sugerir essa 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) } 

By the way: ifelse suga ... com grande poder vem uma grande responsabilidade, ou seja, tipo conversões de matrizes 1x1 e / ou numéricos [quando deveriam ser adicionados por exemplo] é ok para mim, mas este tipo de conversão em ifelse é claramente indesejado. Eu esbarrei no mesmo 'bug' de ifelse várias vezes agora e ele continua roubando meu tempo 🙁

FW

A resposta fornecida pelo @ fabian-werner é ótima, mas os objects podem ter várias classs e o “fator” pode não ser necessariamente o primeiro retornado pela class(yes) , então sugiro esta pequena modificação para verificar todos os atributos da class:

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

Eu também enviei uma solicitação com a equipe de Desenvolvimento de R para adicionar uma opção documentada para ter base :: ifelse () preservar atributos com base na seleção do usuário de quais atributos preservar. O pedido está aqui: https://bugs.r-project.org/bugzilla/show_bug.cgi?id=16609 - Ele já foi marcado como "WONTFIX", alegando que sempre foi assim, mas eu forneci um argumento de acompanhamento sobre o porquê de uma adição simples poder salvar muitas dores de cabeça de usuários de R. Talvez o seu "+1" no tópico do bug encoraje 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 para preservar, ou "cond" (padrão ifelse () comportamento), "sim", o comportamento de acordo com o código acima, ou "não", para os casos em que o os 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 

A razão pela qual isso não funciona é porque a function 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.