Чи можна використовувати пакет dplyr для умовної мутації?


178

Чи можна використовувати мутацію, коли мутація умовна (залежно від значень певних значень стовпців)?

Цей приклад допомагає показати, що я маю на увазі.

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

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

Результат коду, який я шукаю, повинен мати такий результат у цьому конкретному прикладі:

  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

Хтось має уявлення про те, як це зробити в dplyr? Цей кадр даних - лише приклад, кадри даних, з якими я маю справу, набагато більше. Через його швидкість я намагався використовувати dplyr, але, можливо, є й інші, кращі способи впоратися з цією проблемою?


2
Так, але dplyr::case_when()набагато ясніше, ніж ifelse,
smci

Відповіді:


216

Використовуйте 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)))

Додано - if_else: Зауважте, що в dplyr 0.5 є if_elseвизначена функція, тому альтернативою було б замінити ifelseна if_else; однак зауважте, що оскільки if_elseсуворіше ifelse(обидві ноги умови повинні мати один і той же тип), тому NAв такому випадку доведеться замінити 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_)))

Додано - case_when Оскільки це питання було розміщено, додано dplyr, case_whenтому іншою альтернативою буде:

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

Додано - arithmetic / na_if Якщо значення числові і умови (крім значення за замовчуванням NA в кінці) взаємно виключають, як це стосується питання, то ми можемо використовувати арифметичний вираз таким чином, що кожен доданок множимо за бажаним результатом, використовуючи na_ifв кінці заміну 0 на 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
У чому полягає логіка, якщо замість цього NAя хочу, щоб рядки, які не відповідають умовам, просто залишалися однаковими?
Назер

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

11
case_when є sooooo красивий, і мені знадобилося так довго, щоб зрозуміти, що він насправді є. Я думаю, що це має бути в найпростіших навчальних посібниках, дуже часто виникає потреба в обчисленні матеріалів для підмножини даних, але все ж хочеться зберегти ці дані.
Хав'єр Фахардо

55

Оскільки ви просите інші кращі способи вирішення проблеми, ось інший спосіб використання 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]

Зверніть увагу, що порядок умовних висловлювань змінюється для gправильного отримання . Немає gзробленої копії навіть під час другого завдання - її замінюють на місці .

Що стосується більш великих даних, це матиме кращі показники, ніж використання вкладених if-else , оскільки воно може оцінювати як «так», так і «ні» , і вкладення може бути важче читати / підтримувати IMHO.


Ось орієнтир щодо відносно більших даних:

# 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

Не впевнений, що це альтернатива, яку ви просили, але я сподіваюся, що це допоможе.


4
Гарний фрагмент коду! Відповідь Г. Гротендіка працює і коротка, тому я вибрав її як відповідь на моє запитання, але я дякую вам за ваше рішення. Я впевнений, що спробую це також.
rdatasculptor

Оскільки DT_funмодифікація свого вхідного сигналу змінюється на місці, тест може бути не зовсім справедливим - окрім того, що не надходить той самий вхід від другої ітерації вперед (що може вплинути на час, оскільки DT$gвже призначений?), Результат також поширюється назад ans1і, отже, може ( якщо Лепеха оптимізатор вважатиме це за необхідне? Не впевнений , що на цьому ...) уникнути інший копії, DPLYR_funі BASE_funпотрібно зробити?
Кен Вільямс

Для того, щоб було зрозуміло, я вважаю, що це data.tableрішення є чудовим, і я використовую data.tableтам, де мені справді потрібна швидкість для операцій над таблицями, і я не хочу йти цілком до C ++. Це вимагає бути дуже обережним щодо змін на місці, хоча!
Кен Вільямс

Я намагаюся звикнути до більш охайних матеріалів із data.table, і це один із тих прикладів досить поширеного випадку використання, який data.table є простішим для читання та більш ефективним. Моя головна причина, що хочу розвивати більше охайного мого словника, - це читаність для себе та інших, але в цьому випадку, здається, виграє data.table.
Пол МакМерді

38

dplyr тепер має функцію, case_whenяка пропонує векторизований if. Синтаксис є дещо дивним порівняно з тим mosaic:::derivedFactor, що ви не можете отримати доступ до змінних стандартним способом dplyr, і вам потрібно оголосити режим NA, але це значно швидше, ніж 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: Якщо ви використовуєте dplyr::case_when()з версії 0.7.0 пакету, вам потрібно передувати імена змінних з ' .$' (наприклад, записувати .$a == 1всередину case_when).

Бенчмарк : Для еталону (повторне використання функцій з поста Аруна) та зменшення розміру вибірки:

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
)

Це дає:

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_whenможна також записати так: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
Чи є це орієнтиром у мікросекундах / мілісекундах / днях? Цей контрольний показник безглуздий без надання вимірювальної одиниці. Крім того, маркування на стенді на наборі даних менше 1e6 також безглуздо, оскільки воно не масштабується.
Девід Аренбург

3
Pls модифікуйте свою відповідь, вам більше не потрібно .$в новій версії dplyr
Amit

14

derivedFactorФункція з mosaicпакета , здається, призначена для обробки цього. Використовуючи цей приклад, це виглядатиме так:

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

(Якщо ви хочете , результат буде цифровим замість фактора, можна обернути derivedFactorв as.numericвиклику.)

derivedFactor може також використовуватися для довільної кількості умовних умов.


4
@hadley повинен зробити це синтаксисом для dplyr за замовчуванням. Потрібні вкладені "ifelse" заяви - це найгірша частина пакету, що в основному так, тому що інші функції настільки хороші
rsoren

Ви також можете запобігти тому, щоб результат був фактором, використовуючи .asFactor = Fопцію або використовуючи (подібну) derivedVariableфункцію в одному пакеті.
Джейк Фішер

Схоже, recodeвід dplyr 0,5 це зробить. Я ще цього не досліджував. Дивіться blog.rstudio.org/2016/06/27/dplyr-0-5-0
Джейк Фішер

12

case_when тепер досить чиста реалізація випадку SQL-стилю, коли:

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

Використовуючи dplyr 0.7.4

Посібник: http://dplyr.tidyverse.org/reference/case_when.html

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