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

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

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

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


person rdatasculptor    schedule 27.06.2014    source источник
comment
Да, но dplyr::case_when() намного яснее, чем ifelse,   -  person smci    schedule 02.12.2018


Ответы (5)


Используйте 1_

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))
person G. Grothendieck    schedule 27.06.2014
comment
В чем логика, если вместо NA я хочу, чтобы строки, не соответствующие условиям, просто оставались такими же? - person Nazer; 30.03.2017
comment
case_when оооочень прекрасен, и мне потребовалось очень много времени, чтобы понять, что он действительно там был. Я думаю, что это должно быть в простейших руководствах по dplyr, очень часто возникает необходимость в вычислении данных для подмножеств данных, но при этом требуется, чтобы данные оставались полными. - person Javier Fajardo; 16.03.2018
comment
@ГРАММ. Гротендик: Из приведенного выше обсуждения и файлов справки я понимаю разницу между if_else() и ifelse(). Можете ли вы правильно использовать ifelse() с функцией mutate? Есть ли какие-либо рекомендации использовать dplyr::if_else() вместо base:ifelse() - т.е. использование более строгого if_else() может избежать ненужных проблем ниже по течению? - person phargart; 14.05.2021
comment
Вы можете использовать любой из них. В основном вопрос в том, предпочитаете ли вы строгость или гибкость. - person G. Grothendieck; 14.05.2021

Поскольку вы просите других, более эффективных способов решения проблемы, вот еще один способ, использующий 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, поскольку он может оценивать как "да", так и " нет 'case, и вложенность может стать труднее читать / поддерживать ИМХО.


Вот эталон для относительно больших данных:

# 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

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

person Arun    schedule 27.06.2014
comment
Хороший фрагмент кода! Ответ Г. Гротендика работает и краток, поэтому я выбрал его в качестве ответа на свой вопрос, но я благодарю вас за ваше решение. Я обязательно попробую и так. - person rdatasculptor; 28.06.2014
comment
Поскольку DT_fun изменяет свой ввод на месте, эталонный тест может быть не совсем справедливым - помимо того, что он не получает тот же ввод со второй итерации вперед (что может повлиять на время, поскольку DT$g уже выделено?), Результат также распространяется обратно на ans1 и следовательно, можно ли (если оптимизатор R считает это необходимым? Не уверен в этом ...) избежать еще одной копии, которую DPLYR_fun и BASE_fun должны сделать? - person Ken Williams; 12.12.2017
comment
Но для ясности: я думаю, что это data.table решение отличное, и я использую data.table везде, где мне действительно нужна скорость для операций с таблицами, и я не хочу полностью переходить на C ++. Тем не менее, это требует особой осторожности при внесении изменений! - person Ken Williams; 12.12.2017
comment
Я пытаюсь привыкнуть к большему количеству вещей tidyverse из data.table, и это один из тех примеров довольно распространенного варианта использования, когда data.table легче читать и более эффективно. Моя основная причина, по которой я хочу разработать больше тидиверсии в моем словарном запасе, - это удобочитаемость для меня и других, но в этом случае, похоже, выигрывает data.table. - person Paul McMurdie; 07.02.2020

В 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
person Matifou    schedule 08.10.2016
comment
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_))) - person G. Grothendieck; 04.02.2017
comment
Это эталонный показатель в микросекундах / миллисекундах / днях, что? Этот тест не имеет смысла без предоставленной единицы измерения. Кроме того, бенчмаркинг для набора данных меньше 1e6 тоже бессмыслен, поскольку он не масштабируется. - person David Arenburg; 13.04.2017
comment
Пожалуйста, измените свой ответ, вам больше не нужен .$ в новой версии dplyr - person Amit Kohli; 05.07.2017
comment
@Matifou в чем разница между DT[, col1 := case_when(col2 > value ~ TRUE)] и DT[col2 > value, col1 := TRUE]? - person ivan866; 16.10.2020

Функция 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 также можно использовать для произвольного количества условных выражений.

person Jake Fisher    schedule 22.10.2015
comment
@hadley должен сделать это синтаксисом по умолчанию для dplyr. Необходимость вложенных операторов ifelse - это худшая часть пакета, что в основном так, потому что другие функции настолько хороши - person rsoren; 07.11.2015
comment
Вы также можете предотвратить влияние результата на фактор, используя параметр .asFactor = F или (аналогичную) функцию derivedVariable в том же пакете. - person Jake Fisher; 27.07.2016
comment
Похоже, это сделает recode из dplyr 0.5. Но я еще не исследовал это. См. blog.rstudio.org/2016/06/27/ dplyr-0-5-0 - person Jake Fisher; 15.08.2016

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

person Rasmus Larsen    schedule 12.10.2017