Gói dplyr có thể được sử dụng để đột biến có điều kiện?


178

Đột biến có thể được sử dụng khi đột biến có điều kiện (tùy thuộc vào giá trị của các giá trị cột nhất định) không?

Ví dụ này giúp thể hiện những gì tôi muốn nói.

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

Tôi đã hy vọng tìm ra giải pháp cho vấn đề của mình bằng cách sử dụng gói dplyr (và vâng tôi biết đây không phải là mã nên hoạt động, nhưng tôi đoán nó làm cho mục đích rõ ràng) để tạo một cột mới 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})

Kết quả của mã tôi đang tìm kiếm phải có kết quả này trong ví dụ cụ thể này:

  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

Có ai có ý tưởng về cách làm điều này trong dplyr không? Khung dữ liệu này chỉ là một ví dụ, các khung dữ liệu tôi đang xử lý lớn hơn nhiều. Vì tốc độ của nó, tôi đã cố gắng sử dụng dplyr, nhưng có lẽ có những cách khác, tốt hơn để xử lý vấn đề này?


2
Có nhưng dplyr::case_when()rõ ràng hơn nhiều so với ifelse,
smci 2/12/18

Câu trả lời:


216

Sử dụng 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)))

Đã thêm - if_else: Lưu ý rằng trong dplyr 0.5 có một if_elsehàm được xác định để thay thế sẽ được thay thế ifelsebằng if_else; tuy nhiên, lưu ý rằng vì if_elsenghiêm ngặt hơn ifelse(cả hai chân của điều kiện phải có cùng loại) nên NAtrong trường hợp đó sẽ phải được thay thế bằng 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_)))

Đã thêm - case_when Vì câu hỏi này đã được đăng, dplyr đã thêm case_whennên một cách khác sẽ là:

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

Đã thêm - số học / na_if Nếu các giá trị là số và các điều kiện (ngoại trừ giá trị mặc định của NA ở cuối) là loại trừ lẫn nhau, như trường hợp trong câu hỏi, thì chúng ta có thể sử dụng biểu thức số học sao cho mỗi thuật ngữ được nhân bởi kết quả mong muốn bằng cách sử dụng na_ifở cuối để thay thế 0 bằng 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))

3
Logic là gì nếu thay vì NA, tôi muốn các hàng không đáp ứng các điều kiện để giữ nguyên?
xít

10
mutate(g = ifelse(condition1, 2, ifelse(condition2, 3, g))
G. Grothendieck

11
case_when rất đẹp và tôi đã mất rất lâu để nhận ra rằng nó thực sự ở đó. Tôi nghĩ rằng điều này nên có trong các hướng dẫn dplyr đơn giản nhất, rất phổ biến khi có nhu cầu tính toán công cụ cho các tập hợp con của dữ liệu, nhưng vẫn muốn giữ cho dữ liệu hoàn chỉnh.
Javier Fajardo

55

Vì bạn yêu cầu các cách khác tốt hơn để xử lý sự cố, đây là một cách khác sử dụng 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]

Lưu ý thứ tự của các câu điều kiện được đảo ngược để có được gchính xác. Không có bản sao nào gđược thực hiện, ngay cả trong lần chuyển nhượng thứ hai - nó được thay thế tại chỗ .

Trên dữ liệu lớn hơn, điều này sẽ có hiệu suất tốt hơn so với sử dụng lồng nhau if-else , vì nó có thể đánh giá cả hai trường hợp 'có' và 'không' và việc lồng có thể khó đọc / duy trì IMHO hơn.


Đây là một điểm chuẩn về dữ liệu tương đối lớn hơn:

# 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

Không chắc đây có phải là một giải pháp thay thế mà bạn đã yêu cầu hay không, nhưng tôi hy vọng nó có ích.


4
Đoạn mã đẹp! Câu trả lời của G. Grotendieck rất ngắn gọn và vì vậy tôi đã chọn câu trả lời đó cho câu hỏi của tôi, nhưng tôi cảm ơn vì giải pháp của bạn. Tôi chắc chắn sẽ thử nó theo cách này là tốt.
ndatasculptor

DT_funđang sửa đổi đầu vào tại chỗ, điểm chuẩn có thể không hoàn toàn công bằng - ngoài việc không nhận được đầu vào tương tự từ lần lặp thứ 2 về phía trước (có thể ảnh hưởng đến thời gian vì DT$gđã được phân bổ?), Kết quả cũng lan truyền trở lại ans1và do đó có thể ( nếu trình tối ưu hóa của R thấy cần thiết? Không chắc chắn về điều này ...) tránh một bản sao khácDPLYR_funBASE_funcần phải thực hiện?
Ken Williams

Mặc dù vậy, rõ ràng, tôi nghĩ data.tablegiải pháp này rất tuyệt và tôi sử dụng data.tablebất cứ nơi nào tôi thực sự cần tốc độ cho các thao tác trên bàn và tôi không muốn đi đến C ++. Nó đòi hỏi phải thực sự cẩn thận về sửa đổi tại chỗ, mặc dù!
Ken Williams

Tôi đang cố gắng làm quen với những thứ gọn gàng hơn từ data.table và đây là một trong những ví dụ về trường hợp sử dụng khá phổ biến mà data.table vừa dễ đọc và hiệu quả hơn. Lý do chính của tôi để muốn phát triển gọn gàng hơn trong vốn từ vựng của tôi là khả năng đọc cho bản thân và những người khác, nhưng trong trường hợp này có vẻ như data.table thắng.
Paul McMurdie

38

dplyr bây giờ có một chức năng case_whencung cấp một vectơ nếu. Cú pháp hơi lạ so với mosaic:::derivedFactorkhi bạn không thể truy cập các biến theo cách dplyr tiêu chuẩn và cần khai báo chế độ NA, nhưng nó nhanh hơn đáng kể so với 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: Nếu bạn đang sử dụng dplyr::case_when()từ trước phiên bản 0.7.0 của gói, thì bạn cần đặt trước tên biến bằng ' .$' (ví dụ: viết .$a == 1bên trongcase_when ).

Điểm chuẩn : Dành cho điểm chuẩn (sử dụng lại các chức năng từ bài đăng của Arun) và giảm kích thước mẫu:

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
)

Điều này mang lại:

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

case_whencũng có thể được viết là: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
Là điểm chuẩn này tính bằng micro giây / mili giây / ngày, là gì? Điểm chuẩn này là vô nghĩa nếu không có đơn vị đo lường được cung cấp. Ngoài ra, việc đánh dấu băng ghế trên một tập dữ liệu nhỏ hơn 1e6 cũng vô nghĩa vì nó không có tỷ lệ.
David Arenburg

3
Xin sửa đổi câu trả lời của bạn, bạn không cần .$thêm nữa trong phiên bản mới của dplyr
Amit Kohli

14

Các derivedFactorchức năng từ mosaicgói dường như được thiết kế để xử lý này. Sử dụng ví dụ này, nó sẽ trông như:

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

(Nếu bạn muốn kết quả là số thay vì một yếu tố, bạn có thể gói derivedFactortrong mộtas.numeric cuộc gọi.)

derivedFactor cũng có thể được sử dụng cho một số lượng điều kiện tùy ý.


4
@hadley nên đặt đây là cú pháp mặc định cho dplyr. Cần các câu lệnh "ifelse" lồng nhau là phần tồi tệ nhất của gói, điều này chủ yếu là vì các chức năng khác rất tốt
rupten

Bạn cũng có thể ngăn kết quả là một yếu tố sử dụng .asFactor = Ftùy chọn hoặc bằng cách sử dụng derivedVariablechức năng (tương tự) trong cùng một gói.
Jake Fisher

Có vẻ như recodetừ dplyr 0,5 sẽ làm điều này. Tôi chưa điều tra nó mặc dù. Xem blog.rstudio.org/2016/06/27/dplyr-0-5-0
Jake Fisher

12

case_when bây giờ là một triển khai khá rõ ràng của trường hợp kiểu SQL khi:

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

Sử dụng dplyr 0.7.4

Hướng dẫn: http://dplyr.tidyverse.org/reference/case_when.html

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.