Bagaimana mencegah ifelse () dari mengubah objek Date menjadi objek numerik

162

Saya menggunakan fungsi ifelse()untuk memanipulasi vektor tanggal. Saya mengharapkan hasilnya dari kelas Date, dan terkejut mendapatkan numericvektor sebagai gantinya. Berikut ini sebuah contoh:

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)

Ini terutama mengejutkan karena melakukan operasi di seluruh vektor mengembalikan suatu Dateobjek.

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

Haruskah saya menggunakan beberapa fungsi lain untuk beroperasi pada Datevektor? Jika demikian, apa fungsinya? Jika tidak, bagaimana cara memaksa ifelseuntuk mengembalikan vektor dengan tipe yang sama dengan input?

Halaman bantuan untuk ifelsemenunjukkan bahwa ini adalah fitur, bukan bug, tapi saya masih berjuang untuk menemukan penjelasan untuk apa yang saya temukan sebagai perilaku mengejutkan.

Zach
sumber
4
Sekarang ada fungsi if_else()dalam paket dplyr yang bisa menggantikan ifelsesementara mempertahankan kelas objek Date yang benar - itu diposting di bawah ini sebagai jawaban baru-baru ini. Saya membawa perhatian ke sini karena ini memecahkan masalah ini dengan menyediakan fungsi yang diuji unit dan didokumentasikan dalam paket CRAN, tidak seperti banyak jawaban lain yang (pada komentar ini) berada di peringkat di depannya.
Sam Firke

Jawaban:

132

Anda dapat menggunakan data.table::fifelse( data.table >= 1.12.3) atau dplyr::if_else.


data.table::fifelse

Tidak seperti ifelse, fifelsemempertahankan tipe dan kelas input.

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

Dari dplyr 0.5.0catatan rilis :

[ if_else] memiliki semantik yang lebih kuat ifelse():: truedan falseargumen harus bertipe sama. Ini memberikan tipe pengembalian yang kurang mengejutkan, dan mempertahankan vektor S3 seperti tanggal ".

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" 
Henrik
sumber
2
Pasti berguna bahkan jika itu membuat saya kehilangan tanda centang. Versi halaman bantuan saat ini tidak mengatakan apa yang diharapkan dari argumen faktor. Pilihan saya akan untuk objek pengembalian faktor yang memiliki level yang merupakan gabungan dari level level truedan falselevel.
IRTFM
3
Apakah ada cara untuk memiliki salah satu argumen if_elsebe NA? Saya sudah mencoba NA_opsi logis dan tidak ada yang menempel dan saya tidak percaya adaNA_double_
roarkz
11
@Zak Salah satu kemungkinan adalah untuk membungkus NAdi as.Date.
Henrik
Ada NA_real_, @roarkz. dan @ Henrik, komentar Anda di sini memecahkan masalah saya.
BLT
63

Ini terkait dengan Nilai terdokumentasi dari ifelse:

Vektor dengan panjang dan atribut yang sama (termasuk dimensi dan " class") seperti testdan nilai data dari nilai yesatau no. Mode jawaban akan dipaksa dari logis untuk mengakomodasi pertama nilai yang diambil yesdan kemudian nilai yang diambil no.

Dimasukkan ke implikasinya, ifelsemembuat faktor kehilangan level mereka dan Tanggal kehilangan kelas mereka dan hanya mode mereka ("numerik") yang dipulihkan. Coba ini sebagai gantinya:

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"

Anda dapat membuat 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"

Catatan selanjutnya: Saya melihat Hadley telah membangun sebuah if_elsekompleks magrittr / dplyr / tidyr dari paket-paket pembentuk data.

IRTFM
sumber
37
Versi yang lebih elegan:safe.ifelse <- function(cond, yes, no) structure(ifelse(cond, yes, no), class = class(yes))
hadley
5
Bagus. Apakah Anda melihat ada alasan mengapa itu bukan perilaku default?
IRTFM
hanya berhati-hatilah dengan apa yang Anda masukkan "ya" karena saya memiliki NA dan tidak berhasil. Mungkin lebih baik untuk lulus kelas sebagai parameter daripada mengasumsikan itu adalah kelas dari kondisi "ya".
Denis
1
Saya tidak yakin komentar terakhir ini artinya. Hanya karena sesuatu memiliki nilai NA tidak berarti tidak dapat memiliki kelas.
IRTFM
8 tahun sejak masalah ini muncul dan masih ifelse()belum "aman" .
M--
16

Penjelasan DWin tepat. Saya mengutak-atik dan bertarung dengan ini untuk sementara waktu sebelum saya menyadari bahwa saya bisa memaksa kelas setelah pernyataan 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)

Pada awalnya ini terasa sedikit "retas" bagi saya. Tapi sekarang saya hanya menganggapnya sebagai harga kecil untuk membayar pengembalian kinerja yang saya dapatkan dari ifelse (). Plus itu masih jauh lebih ringkas daripada satu lingkaran.

JD Long
sumber
ini (bagus, jika, ya, hackish) teknik tampaknya juga membantu dengan fakta bahwa R forpernyataan yang ditunjuk dengan nilai dari item dalam VECTORuntuk NAME, tetapi tidak mereka kelas .
Greg Minshall
6

Metode yang disarankan tidak bekerja dengan kolom faktor. Saya ingin menyarankan peningkatan ini:

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

Ngomong-ngomong: ifelse menyebalkan ... dengan kekuatan besar datang tanggung jawab besar, yaitu konversi tipe matriks 1x1 dan / atau numerik [ketika harus ditambahkan misalnya] tidak masalah bagi saya tetapi konversi tipe ini dalam ifelse jelas tidak diinginkan. Saya bertemu 'bug' yang sama dari ifelse beberapa kali sekarang dan itu terus mencuri waktu saya :-(

FW

Fabian Werner
sumber
Ini adalah satu-satunya solusi yang berfungsi untuk saya untuk faktor.
bshor
Saya akan berpikir bahwa level yang akan dikembalikan adalah gabungan dari level yesdan nodan Anda akan memeriksa dulu untuk melihat bahwa keduanya adalah faktor. Anda mungkin perlu mengkonversi ke karakter dan kemudian menyatukan kembali dengan tingkat-"serikat".
IRTFM
6

Alasan mengapa ini tidak berhasil adalah karena, fungsi ifelse () mengubah nilai menjadi faktor. Solusi yang bagus adalah dengan mengubahnya menjadi karakter sebelum mengevaluasinya.

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

Ini tidak memerlukan perpustakaan apa pun selain dari basis R.

ananthapadmanabhan s
sumber
5

Jawaban yang diberikan oleh @ fabian-werner sangat bagus, tetapi objek dapat memiliki beberapa kelas, dan "faktor" mungkin belum tentu yang pertama dikembalikan oleh class(yes), jadi saya sarankan modifikasi kecil ini untuk memeriksa semua atribut kelas:

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

Saya juga telah mengajukan permintaan dengan tim Pengembangan R untuk menambahkan opsi yang didokumentasikan untuk memiliki basis :: ifelse () melestarikan atribut berdasarkan pilihan pengguna yang atributnya akan dipertahankan. Permintaannya ada di sini: https://bugs.r-project.org/bugzilla/show_bug.cgi?id=16609 - Sudah ditandai sebagai "WONTFIX" dengan alasan selalu seperti sekarang, tapi saya telah memberikan argumen tindak lanjut tentang mengapa penambahan sederhana mungkin menghemat banyak sakit kepala pengguna R. Mungkin "+1" Anda di utas bug itu akan mendorong tim R Core untuk melihat kembali.

EDIT: Berikut adalah versi yang lebih baik yang memungkinkan pengguna untuk menentukan atribut mana yang harus dipertahankan, baik perilaku "cond" (perilaku ifelse () default), "ya", perilaku sesuai kode di atas, atau "tidak", untuk kasus di mana atribut nilai "tidak" lebih baik:

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
sumber
1
inherits(y, "factor")mungkin "lebih benar" dari"factor" %in% class.y
IRTFM
Memang. inheritsmungkin yang terbaik.
Mekki MacAulay