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

162

Я использую функцию 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указывает, что это функция, а не ошибка, но я все еще пытаюсь найти объяснение тому, что я считаю удивительным поведением.

Zach
источник
4
Теперь if_else()в пакете dplyr есть функция, которая может заменить ifelseпри сохранении правильных классов объектов Date - она опубликована ниже как недавний ответ. Я обращаю внимание на это здесь, поскольку оно решает эту проблему, предоставляя функцию, которая тестируется модулем и документируется в пакете CRAN, в отличие от многих других ответов, которые (по состоянию на этот комментарий) были ранжированы впереди нее.
Сэм Фирке

Ответы:

132

Вы можете использовать data.table::fifelse( data.table >= 1.12.3) или dplyr::if_else.


data.table::fifelse

В отличие от ifelse, fifelseсохраняет тип и класс входов.

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

Из dplyr 0.5.0заметок о выпуске :

[ if_else] Имеет более строгую семантику ifelse(): 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" 
Хенрик
источник
2
Определенно полезно, даже если это заставило меня потерять галочку. Текущая версия справочной страницы не говорит, что ожидать от факторных аргументов. Мой голос был бы за объект возврата фактора, у которого были уровни, которые были объединением уровней true's и false' уровней.
IRTFM
3
Есть ли способ иметь один из аргументов if_elseбыть NA? Я попробовал логические NA_варианты, и ничто не мешает, и я не верю, что естьNA_double_
roarkz
11
@Zak Одна возможность обернуть NAв as.Date.
Хенрик
Есть NA_real_, @roarkz. и @Henrik, ваш комментарий решил мою проблему.
BLT
63

Это относится к документированной стоимости из 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 пакетов для формирования данных.

IRTFM
источник
37
Несколько более элегантная версия:safe.ifelse <- function(cond, yes, no) structure(ifelse(cond, yes, no), class = class(yes))
Хэдли
5
Ницца. Видите ли вы там причину, почему это не поведение по умолчанию?
IRTFM
просто будьте осторожны с тем, что вы положили в «да», потому что у меня был АН, и это не сработало. Вероятно, лучше передать класс в качестве параметра, чем предполагать, что это класс условия «да».
Денис
1
Я не уверен, что последний комментарий это означает. То, что у чего-то есть значение NA, не означает, что у него не может быть класса.
IRTFM
8 лет с тех пор, как эта проблема возникла и все еще ifelse()не является "безопасной" .
M--
16

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

JD Long
источник
это (хорошо, если да, хак) метод , кажется, также помочь с тем , что АиР forоператор присваивает значение элементов в VECTORк NAME, но не их классу .
Грег Миншалл
6

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

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

Фабиан Вернер
источник
Это единственное решение, которое работает на меня для факторов.
bshor
Я бы подумал , что уровни должны быть возвращены будет объединение уровней yesи noи что вы бы сначала проверить, что они оба фактора. Вам, вероятно, потребуется преобразовать в символ, а затем заново сопоставить с «объединенными» уровнями.
IRTFM
6

Причина, по которой это не сработает, заключается в том, что функция 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.

анантападманабхан с
источник
5

Ответ, предоставленный @ fabian-werner, великолепен, но объекты могут иметь несколько классов, и «factor» может не обязательно быть первым, возвращаемым 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 взглянуть еще раз.

РЕДАКТИРОВАТЬ: Вот лучшая версия, которая позволяет пользователю указать, какие атрибуты сохранять, либо «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
Mekki MacAulay
источник
1
inherits(y, "factor")может быть "более правильным", чем"factor" %in% class.y
IRTFM
На самом деле. inheritsможет быть лучше.
Мекки Маколей