Как предотвратить 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
указывает, что это функция, а не ошибка, но я все еще пытаюсь найти объяснение того, что, как мне показалось, вызывает удивление.
Ответы
Ответ 1
Вы можете использовать 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"
Подробнее о fifelse
, в том числе о тестах, см. НОВОСТИ № 21 для версии 1.12.3 для разработки. Для установки версии для разработчиков см. здесь.
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
Это относится к документированному Значение 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 пакетов формирования данных.
Ответ 3
описание 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(). Плюс это все еще намного более кратким, чем цикл.
Ответ 4
Предлагаемый метод не работает с столбцами факторов. 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
Ответ 5
Ответ, предоставленный @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 Development team, чтобы добавить документированную опцию, чтобы base:: ifelse() сохранял атрибуты на основе выбора пользователем, какие атрибуты сохраняются. Запрос здесь: https://bugs.r-project.org/bugzilla/show_bug.cgi?id=16609 - Он уже отмечен как "WONTFIX" на том основании, что он всегда был таким, каким он есть сейчас, но я представил последующий аргумент о том, почему простое добавление может сэкономить много головных задач пользователей R. Возможно, ваш "+1" в этом потоке ошибок заставит команду R Core сделать второй взгляд.
EDIT: здесь более эффективная версия, которая позволяет пользователю указать, какие атрибуты сохранить, либо "cond" (поведение по умолчанию ifelse()), "yes", поведение в соответствии с приведенным выше кодом, либо "no", для случаи, когда атрибуты значения "нет" лучше:
safe_ifelse <- function(cond, yes, no, preserved_attributes = "yes") {
# Capture the user 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 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
Ответ 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.