Apakah paket dplyr dapat digunakan untuk mutasi bersyarat?

179

Dapatkah mutasi digunakan ketika mutasi bersyarat (tergantung pada nilai-nilai nilai kolom tertentu)?

Contoh ini membantu menunjukkan apa yang saya maksud.

structure(list(a = c(1, 3, 4, 6, 3, 2, 5, 1), b = c(1, 3, 4, 
2, 6, 7, 2, 6), c = c(6, 3, 6, 5, 3, 6, 5, 3), d = c(6, 2, 4, 
5, 3, 7, 2, 6), e = c(1, 2, 4, 5, 6, 7, 6, 3), f = c(2, 3, 4, 
2, 2, 7, 5, 2)), .Names = c("a", "b", "c", "d", "e", "f"), row.names = c(NA, 
8L), class = "data.frame")

  a b c d e f
1 1 1 6 6 1 2
2 3 3 3 2 2 3
3 4 4 6 4 4 4
4 6 2 5 5 5 2
5 3 6 3 3 6 2
6 2 7 6 7 7 7
7 5 2 5 2 6 5
8 1 6 3 6 3 2

Saya berharap dapat menemukan solusi untuk masalah saya menggunakan paket dplyr (dan ya saya tahu ini bukan kode yang seharusnya berfungsi, tapi saya rasa ini menjelaskan tujuannya) untuk membuat kolom baru g:

 library(dplyr)
 df <- mutate(df,
         if (a == 2 | a == 5 | a == 7 | (a == 1 & b == 4)){g = 2},
         if (a == 0 | a == 1 | a == 4 | a == 3 |  c == 4) {g = 3})

Hasil dari kode yang saya cari harus memiliki hasil ini dalam contoh khusus ini:

  a b c d e f  g
1 1 1 6 6 1 2  3
2 3 3 3 2 2 3  3
3 4 4 6 4 4 4  3
4 6 2 5 5 5 2 NA
5 3 6 3 3 6 2 NA
6 2 7 6 7 7 7  2
7 5 2 5 2 6 5  2
8 1 6 3 6 3 2  3

Apakah ada yang punya ide tentang bagaimana melakukan ini di dplyr? Kerangka data ini hanyalah contoh, kerangka data yang saya tangani jauh lebih besar. Karena kecepatannya saya mencoba menggunakan dplyr, tetapi mungkin ada cara lain yang lebih baik untuk menangani masalah ini?

rdatasculptor
sumber
2
Ya tapi dplyr::case_when()jauh lebih jelas daripada ifelse,
smci

Jawaban:

216

Menggunakan ifelse

df %>%
  mutate(g = ifelse(a == 2 | a == 5 | a == 7 | (a == 1 & b == 4), 2,
               ifelse(a == 0 | a == 1 | a == 4 | a == 3 |  c == 4, 3, NA)))

Ditambahkan - if_else: Perhatikan bahwa dalam dplyr 0.5 ada if_elsefungsi yang didefinisikan sehingga alternatifnya adalah untuk menggantikannya ifelsedengan if_else; Namun, perhatikan bahwa karena if_elselebih ketat daripada ifelse(kedua kaki kondisi harus memiliki jenis yang sama) sehingga NAdalam hal ini harus diganti NA_real_.

df %>%
  mutate(g = if_else(a == 2 | a == 5 | a == 7 | (a == 1 & b == 4), 2,
               if_else(a == 0 | a == 1 | a == 4 | a == 3 |  c == 4, 3, NA_real_)))

Ditambahkan - case_when Karena pertanyaan ini diposting dplyr telah ditambahkan case_whensehingga alternatif lain adalah:

df %>% mutate(g = case_when(a == 2 | a == 5 | a == 7 | (a == 1 & b == 4) ~ 2,
                            a == 0 | a == 1 | a == 4 | a == 3 |  c == 4 ~ 3,
                            TRUE ~ NA_real_))

Ditambahkan - aritmatika / na_if Jika nilainya numerik dan kondisi (kecuali untuk nilai default NA di akhir) adalah saling eksklusif, seperti halnya dalam pertanyaan, maka kita dapat menggunakan ekspresi aritmatika sedemikian sehingga setiap istilah dikalikan oleh hasil yang diinginkan menggunakan na_ifdi akhir untuk mengganti 0 dengan NA.

df %>%
  mutate(g = 2 * (a == 2 | a == 5 | a == 7 | (a == 1 & b == 4)) +
             3 * (a == 0 | a == 1 | a == 4 | a == 3 |  c == 4),
         g = na_if(g, 0))
G. Grothendieck
sumber
3
Apa logikanya jika alih-alih NA, saya ingin baris yang tidak memenuhi persyaratan tetap sama?
Nazer
10
mutate(g = ifelse(condition1, 2, ifelse(condition2, 3, g))
G. Grothendieck
11
case_when sangaaaat cantik, dan aku butuh waktu lama untuk mengetahui bahwa itu benar-benar ada. Saya pikir ini harus dalam tutorial dplyr yang paling sederhana, sangat umum untuk memiliki kebutuhan untuk menghitung hal-hal untuk subset data, tetapi masih ingin menjaga data lengkap.
Javier Fajardo
55

Karena Anda meminta cara lain yang lebih baik untuk menangani masalah, berikut cara lain menggunakan data.table:

require(data.table) ## 1.9.2+
setDT(df)
df[a %in% c(0,1,3,4) | c == 4, g := 3L]
df[a %in% c(2,5,7) | (a==1 & b==4), g := 2L]

Perhatikan urutan pernyataan bersyarat dibalik untuk mendapatkan yang gbenar. Tidak ada salinan gbuatan, bahkan selama penugasan kedua - diganti di tempat .

Pada data yang lebih besar ini akan memiliki kinerja yang lebih baik daripada menggunakan bersarang if-else , karena dapat mengevaluasi kasus 'ya' dan 'tidak' , dan bersarang dapat menjadi lebih sulit untuk membaca / memelihara IMHO.


Berikut adalah patokan pada data yang relatif lebih besar:

# R version 3.1.0
require(data.table) ## 1.9.2
require(dplyr)
DT <- setDT(lapply(1:6, function(x) sample(7, 1e7, TRUE)))
setnames(DT, letters[1:6])
# > dim(DT) 
# [1] 10000000        6
DF <- as.data.frame(DT)

DT_fun <- function(DT) {
    DT[(a %in% c(0,1,3,4) | c == 4), g := 3L]
    DT[a %in% c(2,5,7) | (a==1 & b==4), g := 2L]
}

DPLYR_fun <- function(DF) {
    mutate(DF, g = ifelse(a %in% c(2,5,7) | (a==1 & b==4), 2L, 
            ifelse(a %in% c(0,1,3,4) | c==4, 3L, NA_integer_)))
}

BASE_fun <- function(DF) { # R v3.1.0
    transform(DF, g = ifelse(a %in% c(2,5,7) | (a==1 & b==4), 2L, 
            ifelse(a %in% c(0,1,3,4) | c==4, 3L, NA_integer_)))
}

system.time(ans1 <- DT_fun(DT))
#   user  system elapsed 
#  2.659   0.420   3.107 

system.time(ans2 <- DPLYR_fun(DF))
#   user  system elapsed 
# 11.822   1.075  12.976 

system.time(ans3 <- BASE_fun(DF))
#   user  system elapsed 
# 11.676   1.530  13.319 

identical(as.data.frame(ans1), as.data.frame(ans2))
# [1] TRUE

identical(as.data.frame(ans1), as.data.frame(ans3))
# [1] TRUE

Tidak yakin apakah ini alternatif yang Anda minta, tapi saya harap ini membantu.

Arun
sumber
4
Sepotong kode yang bagus! Jawaban G. Grotendieck bekerja dan singkat jadi saya memilih yang itu sebagai jawaban untuk pertanyaan saya, tapi saya berterima kasih atas solusi Anda. Saya yakin akan mencobanya dengan cara ini juga.
rdatasculptor
Karena DT_funmemodifikasi inputnya di tempat, patokan mungkin tidak cukup adil - selain tidak menerima input yang sama dari maju iterasi ke-2 (yang mungkin mempengaruhi waktu karena DT$gsudah dialokasikan?), Hasilnya juga menyebar kembali ke ans1dan karena itu mungkin ( jika optimizer dianggapnya R perlu? Tidak yakin tentang ini ...) menghindari lain salinan yang DPLYR_fundan BASE_funkebutuhan untuk membuat?
Ken Williams
Hanya untuk memperjelas, saya pikir data.tablesolusi ini hebat, dan saya gunakan di data.tablemana pun saya benar-benar membutuhkan kecepatan untuk operasi di atas meja & saya tidak ingin pergi jauh ke C ++. Meskipun demikian, memang diperlukan kehati-hatian untuk modifikasi!
Ken Williams
Saya mencoba membiasakan diri dengan hal-hal yang lebih rapi dari data.table, dan ini adalah salah satu contoh kasus penggunaan yang cukup umum sehingga data.table lebih mudah dibaca dan lebih efisien. Alasan utama saya ingin mengembangkan lebih rapi dalam kosakata saya adalah keterbacaan untuk diri sendiri dan orang lain, tetapi dalam hal ini sepertinya data.table menang.
Paul McMurdie
38

dplyr sekarang memiliki fungsi case_whenyang menawarkan jika vektor. Sintaksnya sedikit aneh dibandingkan mosaic:::derivedFactorkarena Anda tidak dapat mengakses variabel dengan cara standar dplyr, dan perlu mendeklarasikan mode NA, tetapi ini jauh lebih cepat daripada mosaic:::derivedFactor.

df %>%
mutate(g = case_when(a %in% c(2,5,7) | (a==1 & b==4) ~ 2L, 
                     a %in% c(0,1,3,4) | c == 4 ~ 3L, 
                     TRUE~as.integer(NA)))

EDIT: Jika Anda menggunakan dplyr::case_when()dari sebelum versi 0.7.0 dari paket, maka Anda harus mendahului nama variabel dengan ' .$' (misalnya menulis .$a == 1di dalam case_when).

Benchmark : Untuk tolok ukur (menggunakan kembali fungsi dari pos Arun) dan mengurangi ukuran sampel:

require(data.table) 
require(mosaic) 
require(dplyr)
require(microbenchmark)

set.seed(42) # To recreate the dataframe
DT <- setDT(lapply(1:6, function(x) sample(7, 10000, TRUE)))
setnames(DT, letters[1:6])
DF <- as.data.frame(DT)

DPLYR_case_when <- function(DF) {
  DF %>%
  mutate(g = case_when(a %in% c(2,5,7) | (a==1 & b==4) ~ 2L, 
                       a %in% c(0,1,3,4) | c==4 ~ 3L, 
                       TRUE~as.integer(NA)))
}

DT_fun <- function(DT) {
  DT[(a %in% c(0,1,3,4) | c == 4), g := 3L]
  DT[a %in% c(2,5,7) | (a==1 & b==4), g := 2L]
}

DPLYR_fun <- function(DF) {
  mutate(DF, g = ifelse(a %in% c(2,5,7) | (a==1 & b==4), 2L, 
                    ifelse(a %in% c(0,1,3,4) | c==4, 3L, NA_integer_)))
}

mosa_fun <- function(DF) {
  mutate(DF, g = derivedFactor(
    "2" = (a == 2 | a == 5 | a == 7 | (a == 1 & b == 4)),
    "3" = (a == 0 | a == 1 | a == 4 | a == 3 |  c == 4),
    .method = "first",
    .default = NA
  ))
}

perf_results <- microbenchmark(
  dt_fun <- DT_fun(copy(DT)),
  dplyr_ifelse <- DPLYR_fun(copy(DF)),
  dplyr_case_when <- DPLYR_case_when(copy(DF)),
  mosa <- mosa_fun(copy(DF)),
  times = 100L
)

Ini memberi:

print(perf_results)
Unit: milliseconds
           expr        min         lq       mean     median         uq        max neval
         dt_fun   1.391402    1.560751   1.658337   1.651201   1.716851   2.383801   100
   dplyr_ifelse   1.172601    1.230351   1.331538   1.294851   1.390351   1.995701   100
dplyr_case_when   1.648201    1.768002   1.860968   1.844101   1.958801   2.207001   100
           mosa 255.591301  281.158350 291.391586 286.549802 292.101601 545.880702   100
Matifou
sumber
case_whenbisa juga ditulis sebagai:df %>% mutate(g = with(., case_when(a %in% c(2,5,7) | (a==1 & b==4) ~ 2L, a %in% c(0,1,3,4) | c==4 ~ 3L, TRUE ~ NA_integer_)))
G. Grothendieck
3
Apakah patokan ini dalam mikrodetik / milidetik / hari, apa? Tolok ukur ini tidak ada artinya tanpa unit pengukuran yang disediakan. Juga, penandaan pada kumpulan data yang lebih kecil dari 1e6 juga tidak berarti karena tidak berskala.
David Arenburg
3
Tolong modifikasi jawaban Anda, Anda tidak perlu .$lagi dalam versi baru dplyr
Amit Kohli
14

The derivedFactorfungsi dari mosaicpaket tampaknya dirancang untuk menangani hal ini. Dengan menggunakan contoh ini, akan terlihat seperti:

library(dplyr)
library(mosaic)
df <- mutate(df, g = derivedFactor(
     "2" = (a == 2 | a == 5 | a == 7 | (a == 1 & b == 4)),
     "3" = (a == 0 | a == 1 | a == 4 | a == 3 |  c == 4),
     .method = "first",
     .default = NA
     ))

(Jika Anda ingin hasilnya menjadi numerik bukan faktor, Anda bisa membungkus derivedFactordalam as.numericpanggilan.)

derivedFactor dapat digunakan untuk sejumlah persyaratan yang sewenang-wenang juga.

Jake Fisher
sumber
4
@Hadley harus menjadikan ini sintaks default untuk dplyr. Memerlukan pernyataan "ifelse" yang bersarang adalah bagian terburuk dari paket, yang terutama terjadi karena fungsi-fungsi lainnya sangat baik
rsoren
Anda juga dapat mencegah hasil dari faktor menggunakan .asFactor = Fopsi atau dengan menggunakan fungsi (serupa) derivedVariabledalam paket yang sama.
Jake Fisher
Sepertinya recodedari dplyr 0,5 akan melakukan ini. Saya belum menyelidiki hal itu. Lihat blog.rstudio.org/2016/06/27/dplyr-0-5-0
Jake Fisher
12

case_when sekarang merupakan implementasi kasus SQL-style yang cukup bersih ketika:

structure(list(a = c(1, 3, 4, 6, 3, 2, 5, 1), b = c(1, 3, 4, 
2, 6, 7, 2, 6), c = c(6, 3, 6, 5, 3, 6, 5, 3), d = c(6, 2, 4, 
5, 3, 7, 2, 6), e = c(1, 2, 4, 5, 6, 7, 6, 3), f = c(2, 3, 4, 
2, 2, 7, 5, 2)), .Names = c("a", "b", "c", "d", "e", "f"), row.names = c(NA, 
8L), class = "data.frame") -> df


df %>% 
    mutate( g = case_when(
                a == 2 | a == 5 | a == 7 | (a == 1 & b == 4 )     ~   2,
                a == 0 | a == 1 | a == 4 |  a == 3 | c == 4       ~   3
))

Menggunakan dplyr 0.7.4

Manual: http://dplyr.tidyverse.org/reference/case_when.html

Rasmus Larsen
sumber