Як запобігти перетворенню об'єктів Date в числові об'єкти ifelse ()


161

Я використовую функцію 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вказує на те, що це особливість, а не помилка, але я все ще намагаюся знайти пояснення для того, що мені було дивно.


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 та falses'.
IRTFM

3
Чи є спосіб мати один з аргументів if_elseНА? Я спробував логічні NA_варіанти, і нічого не NA_double_
стикається,

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


37
Дещо більш елегантна версія:safe.ifelse <- function(cond, yes, no) structure(ifelse(cond, yes, no), class = class(yes))
hadley

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 (). Плюс це все-таки набагато більш стисло, ніж петля.


це (добре, якщо так, хак) метод , здається, також допомогти з тим , що Лепеха 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, чудова, але в об'єктах може бути кілька класів, і "фактор" не обов'язково повинен бути першим, який повертається 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, щоб додати задокументований варіант, щоб мати атрибути збереження 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

1
inherits(y, "factor")може бути "правильніше", ніж"factor" %in% class.y
IRTFM

Справді. inheritsможливо, найкраще.
Mekki MacAulay
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.