Можно ли использовать пакет dplyr для условного мутации?

179

Можно ли использовать мутацию, если она является условной (в зависимости от значений определенных значений столбца)?

Этот пример помогает показать, что я имею в виду.

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, но, возможно, есть и другие, более эффективные способы решения этой проблемы?

rdatasculptor
источник
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_)))

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

Добавлено - арифметика / 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))
Г. Гротендик,
11
case_when sooooo красивый, и мне понадобилось так много времени, чтобы понять, что это было на самом деле там. Я думаю, что это должно быть в простейших уроках dplyr, очень часто нужно вычислять вещи для подмножеств данных, но все же хотеть сохранять данные полными.
Хавьер Фахардо
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

Не уверен, что это альтернатива, которую вы просили, но я надеюсь, что это поможет.

Arun
источник
4
Хороший кусок кода! Ответ Г. Гротендика работает и является коротким, поэтому я выбрал его в качестве ответа на мой вопрос, но я благодарю вас за ваше решение. Я уверен, что попробую и так.
rdatasculptor
Поскольку DT_funпроисходит изменение входных данных на месте, эталонный тест может быть не совсем справедливым - в дополнение к тому, что он не получает тот же входной сигнал от 2-й итерации вперед (что может повлиять на время, так 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)))

РЕДАКТИРОВАТЬ: Если вы используете 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
Matifou
источник
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_)))
Г. Гротендик
3
Этот тест в микросекундах / миллисекундах / днях, что? Этот тест не имеет смысла без предоставленной единицы измерения. Кроме того, бенчмаркинг для набора данных, меньшего чем 1e6, также не имеет смысла, поскольку он не масштабируется.
Давид Аренбург
3
Пожалуйста, измените ваш ответ, вам больше не нужно .$больше в новой версии dplyr
Amit Kohli
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

Расмус Ларсен
источник