dplyr包可以用于条件突变吗?


177

当突变是有条件的(取决于某些列值的值)时,可以使用突变吗?

这个例子有助于说明我的意思。

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

Answers:


214

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定义了一个函数,因此可以选择将其替换ifelseif_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_))

补充-算术/ 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要让我不符合条件的行保持不变,那是什么逻辑呢?
Nazer

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

11
case_when太漂亮了,我花了很长时间才弄清楚它实际上在那里。我认为这应该是在最简单的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即使在第二次分配期间也没有make的副本-已就地替换。

在较大的数据上,这比使用nested 具有更好的性能if-else,因为它可以评估“是”和“ no”两种情况,并且嵌套可能会更难于读取/维护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
不错的代码!G. Grotendieck的答案有效且简短,因此我选择了该答案作为我的问题的答案,但我感谢您的解决方案。我当然也会尝试这种方式。
rdatasculptor 2014年

由于DT_fun正在就地修改其输入,因此基准测试可能不太公平-除了未从第二次迭代接收到相同的输入(这可能会影响时序,因为DT$g已经分配了它?)之外,结果还会传播回ans1并因此可能(如果R的优化认为有必要吗?不知道这个...)避免另一个副本DPLYR_fun,并BASE_fun需要做什么呢?
肯·威廉姆斯

不过,需要明确的是,我认为该data.table解决方案很棒,并且我可以data.table在确实需要对表进行操作的地方使用它,而且我也不想一直使用C ++。但是,确实需要非常小心地进行修改!
肯·威廉姆斯

我正试图习惯于从data.table中获取更多整洁的东西,这是一个非常常见的用例示例,其中data.table既易于阅读,又更加高效。我想在自己的词汇中发展更多整洁的主要原因是我自己和其他人的可读性,但是在这种情况下,好像data.table会赢。
Paul McMurdie

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在inside 写case_when)。

基准:对于基准(重用Arun帖子中的功能)并减小样本量:

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的数据集上进行基准测试也没有意义,因为它无法扩展。
David Arenburg

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 2015年

您还可以.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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.