Cách ngăn ifelse () biến đối tượng Date thành đối tượng số


161

Tôi đang sử dụng hàm ifelse()để thao tác một vectơ ngày. Tôi mong đợi kết quả là của lớp Date, và thật ngạc nhiên khi có được một numericvectơ thay thế. Đây là một ví dụ:

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)

Điều này đặc biệt đáng ngạc nhiên vì thực hiện thao tác trên toàn bộ vector trả về một Dateđối tượng.

dates <- as.Date(c('2011-01-01', '2011-01-02', '2011-01-03', '2011-01-04','2011-01-05'))
dates <- dates - 1
str(dates)

Tôi có nên sử dụng một số chức năng khác để hoạt động trên Datecác vectơ? Nếu vậy thì có chức năng gì? Nếu không, làm thế nào để tôi buộc ifelsetrả về một vectơ cùng loại với đầu vào?

Trang trợ giúp ifelsechỉ ra rằng đây là một tính năng, không phải là lỗi, nhưng tôi vẫn đang cố gắng tìm lời giải thích cho những gì tôi thấy là hành vi đáng ngạc nhiên.


4
Hiện tại có một chức năng if_else()trong gói dplyr có thể thay thế ifelsetrong khi vẫn giữ đúng các lớp đối tượng Date - nó được đăng dưới đây như một câu trả lời gần đây. Tôi đang chú ý đến nó ở đây vì nó giải quyết vấn đề này bằng cách cung cấp một chức năng được kiểm tra đơn vị và ghi lại trong gói CRAN, không giống như nhiều câu trả lời khác (như nhận xét này) được xếp trước nó.
Sam Firke

Câu trả lời:


132

Bạn có thể sử dụng data.table::fifelse( data.table >= 1.12.3) hoặc dplyr::if_else.


data.table::fifelse

Không giống như ifelse, fifelsebảo tồn loại và lớp của đầu vào.

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

Từ dplyr 0.5.0ghi chú phát hành :

[ if_else] có ngữ nghĩa chặt chẽ hơn rằng ifelse(): truevà các falseđối số phải cùng loại. Điều này mang lại một kiểu trả về ít ngạc nhiên hơn và bảo toàn các vectơ S3 như ngày ".

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
Chắc chắn hữu ích ngay cả khi nó làm tôi mất dấu. Phiên bản hiện tại của trang trợ giúp không cho biết những gì mong đợi từ các đối số yếu tố. Phiếu bầu của tôi sẽ dành cho một đối tượng trả lại nhân tố có các cấp độ là sự kết hợp giữa các cấp độ truefalsecấp độ.
IRTFM

3
Có cách nào để có một trong những lập luận của if_elseNA không? Tôi đã thử các NA_tùy chọn hợp lý và không có gì dính vào và tôi không tin có mộtNA_double_
roarkz

11
@Zak Một khả năng là quấn NAtrong as.Date.
Henrik

NA_real_, @roarkz. và @Henrik, bình luận của bạn ở đây đã giải quyết vấn đề của tôi.
BLT

63

Nó liên quan đến Giá trị tài liệu của ifelse:

Một vectơ có cùng độ dài và thuộc tính (bao gồm kích thước và " class") testvà giá trị dữ liệu từ các giá trị của yeshoặc no. Chế độ của câu trả lời sẽ được ép buộc từ logic để phù hợp với bất kỳ giá trị nào được lấy từ trước yesvà sau đó là bất kỳ giá trị nào được lấy từ đó no.

Được đun sôi theo ý nghĩa của nó, ifelselàm cho các yếu tố mất cấp độ và Ngày mất lớp và chỉ chế độ của họ ("số") được khôi phục. Hãy thử điều này thay thế:

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"

Bạn có thể tạo một 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"

Một lưu ý sau: Tôi thấy rằng Hadley đã xây dựng một if_elsetổ hợp magrittr / dplyr / tidyr của các gói định hình dữ liệu.


37
Một số phiên bản thanh lịch hơn:safe.ifelse <- function(cond, yes, no) structure(ifelse(cond, yes, no), class = class(yes))
hadley

5
Đẹp. Bạn có thấy có bất kỳ lý do tại sao đó không phải là hành vi mặc định?
IRTFM

chỉ cần cẩn thận những gì bạn đưa vào "có" bởi vì tôi đã có NA và nó không hoạt động. Có lẽ tốt hơn để vượt qua lớp như một tham số hơn là giả sử đó là lớp của điều kiện "có".
Denis

1
Tôi không chắc rằng bình luận cuối cùng này có nghĩa. Chỉ vì thứ gì đó có giá trị NA không có nghĩa là nó không thể có một lớp.
IRTFM

8 năm kể từ khi vấn đề này xuất hiện và vẫn ifelse()không "an toàn" .
M--

16

Lời giải thích của DWin là tại chỗ. Tôi loay hoay và chiến đấu với điều này trong một thời gian trước khi tôi nhận ra rằng tôi có thể đơn giản là ép buộc lớp học sau tuyên bố 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)

Lúc đầu, điều này cảm thấy một chút "hackish" với tôi. Nhưng bây giờ tôi chỉ nghĩ về nó như một cái giá nhỏ để trả cho hiệu suất mà tôi nhận được từ ifelse (). Thêm vào đó, nó vẫn ngắn gọn hơn nhiều so với một vòng lặp.


này (tốt đẹp, nếu, vâng, hackish) kỹ thuật dường như cũng giúp đỡ với thực tế là R của forchuyển nhượng tuyên bố các giá trị của các mục trong VECTORđể NAME, nhưng không phải của họ lớp .
Greg Minshall

6

Phương pháp được đề xuất không hoạt động với các cột yếu tố. Id muốn đề xuất cải tiến này:

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

Nhân tiện: ifelse hút ... với sức mạnh lớn đi kèm với trách nhiệm lớn, tức là chuyển đổi loại ma trận 1x1 và / hoặc số [ví dụ khi chúng nên được thêm vào], nhưng chuyển đổi loại này trong ifelse rõ ràng là không mong muốn. Bây giờ tôi đã va vào 'lỗi' của ifelse nhiều lần và nó cứ tiếp tục đánh cắp thời gian của tôi :-(

FW


Đây là giải pháp duy nhất phù hợp với tôi cho các yếu tố.
bshor

Tôi đã nghĩ rằng các mức được trả lại sẽ là sự kết hợp của các cấp yesnovà trước tiên bạn sẽ kiểm tra xem chúng có phải là cả hai yếu tố không. Bạn có thể sẽ cần phải chuyển đổi thành nhân vật và sau đó hoàn trả lại với các mức giá "hợp nhất".
IRTFM

6

Lý do tại sao điều này không hoạt động là vì, hàm ifelse () chuyển đổi các giá trị thành các yếu tố. Một cách giải quyết tốt sẽ là chuyển đổi nó thành các ký tự trước khi đánh giá nó.

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

Điều này sẽ không yêu cầu bất kỳ thư viện nào ngoài cơ sở R.


5

Câu trả lời được cung cấp bởi @ fabian-werner là tuyệt vời, nhưng các đối tượng có thể có nhiều lớp và "yếu tố" có thể không nhất thiết phải là lớp đầu tiên được trả về class(yes), vì vậy tôi đề nghị sửa đổi nhỏ này để kiểm tra tất cả các thuộc tính của lớp:

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

Tôi cũng đã gửi yêu cầu với nhóm Phát triển R để thêm tùy chọn được ghi thành tài liệu để có các thuộc tính cơ sở :: ifelse () dựa trên lựa chọn của người dùng về các thuộc tính cần bảo tồn. Yêu cầu có ở đây: https://bugs.r-project.org/ormszilla/show_orms.cgi?id=16609 - Nó đã được gắn cờ là "WONTFIX" với lý do nó vẫn luôn như hiện tại, nhưng tôi đã cung cấp một lập luận tiếp theo về lý do tại sao một bổ sung đơn giản có thể cứu được rất nhiều người dùng R đau đầu. Có lẽ "+1" của bạn trong chuỗi lỗi đó sẽ khuyến khích nhóm R Core xem xét lại lần thứ hai.

EDIT: Đây là phiên bản tốt hơn cho phép người dùng chỉ định các thuộc tính cần bảo tồn, "hành vi" (mặc định ifelse ()), "có", hành vi theo mã ở trên hoặc "không", trong trường hợp các thuộc tính của giá trị "không" là tốt hơn:

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")có thể "đúng" hơn"factor" %in% class.y
IRTFM

Thật. inheritscó thể là tốt nhất
Mekki MacAulay
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.