Как запретить ifelse () превращать объекты Date в числовые объекты

Я использую функцию ifelse() для управления вектором даты. Я ожидал, что результат будет класс Date, и был удивлен, получив numeric вместо вектора. Вот пример:

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)

это особенно удивительно, потому что выполнение операции по всему вектору возвращает 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)

должен ли я использовать какую-либо другую функцию для работы с Date векторов? Если да, то какая функция? Если нет, то как мне заставить ifelse в вернуть вектор того же типа, что и входные данные?

страница справки для ifelse указывает, что это функция, а не ошибка, но я все еще пытаюсь найти объяснение тому, что я нашел удивительным поведением.

6 ответов


вы можете использовать dplyr::if_else.

С dplyr 0.5.0 заметки: "[if_else] имейте более строгую семантику, что ifelse(): the true и false аргументы должны быть одного типа. Это дает менее удивительный тип возврата и сохраняет векторы S3, такие как времени" .

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" 

это относится к документированной стоимостью of ifelse:

вектор той же длины и атрибутов (включая размеры и "class") в качестве test и значения данных из значений yes или no. Режим ответа будет принуждаться от логического для размещения сначала любых значений, взятых из yes а затем любые значения, взятые из no.

сводится к его последствиям,ifelse делает факторы теряют уровни и даты теряют свой класс и восстанавливается только их режим ("числовой"). Попробуйте вместо этого:

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"

вы можете создать 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"

более поздняя заметка: я вижу, что Хэдли построил if_else в комплекс magrittr/dplyr/tidyr пакетов формирования данных.


объяснение Двина на месте. Я возился и боролся с этим некоторое время, прежде чем понял, что могу просто заставить класс после утверждения 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)

сначала это показалось мне немного "хакерским". Но теперь я просто думаю об этом как о небольшой цене, чтобы заплатить за производительность, которую я получаю от ifelse(). К тому же, это все еще более лаконично, чем Петля.


предлагаемый метод не работает со столбцами факторов. Id хотел бы предложить это улучшение:

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

кстати: ifelse отстой... с большой силой приходит большая ответственность, т. е. преобразование типов матриц 1x1 и / или цифр [когда они должны быть добавлены, например] для меня нормально, но это преобразование типов в ifelse явно нежелательно. Я наткнулся на ту же самую "ошибку" ifelse несколько раз, и она просто продолжает красть мое время : - (

FW


ответ, предоставленный @fabian-werner, велик, но объекты могут иметь несколько классов, и "фактор" не обязательно может быть первым, возвращенным class(yes), поэтому я предлагаю эту небольшую модификацию для проверки всех атрибутов класса:

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

Я также отправил запрос с командой разработчиков R, чтобы добавить задокументированную опцию для сохранения атрибутов base::ifelse() на основе выбора пользователем атрибутов для сохранения. Запрос здесь: https://bugs.r-project.org/bugzilla/show_bug.cgi?id=16609 - он уже был помечен как "WONTFIX" на том основании, что он всегда был таким, как сейчас, но я предоставил следующий аргумент о том, почему простое добавление может сэкономить много головных болей пользователей R. Возможно, ваш "+1 " в этом потоке ошибок побудит команду R Core взглянуть еще раз.

EDIT: вот лучшая версия, которая позволяет пользователю указать, какие атрибуты следует сохранить, либо " cond" (по умолчанию ifelse () поведение), "да", поведение в соответствии с приведенным выше кодом или" нет", для случаев, когда атрибуты значения" нет " лучше:

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

причина, по которой это не будет работать, заключается в том, что функция ifelse() преобразует значения в факторы. Хорошим решение будет преобразовать его в символы, прежде чем оценивать его.

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

для этого не потребуется никакой библиотеки, кроме базы R.