【问题标题】:dplyr 包可以用于条件变异吗?
【发布时间】:2014-08-19 00:45:43
【问题描述】:

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

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

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,但也许还有其他更好的方法来处理这个问题?

【问题讨论】:

标签: r if-statement case-when dplyr


【解决方案1】:

使用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_elseifelse 更严格(条件的两条腿必须具有相同的类型),因此在这种情况下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))

【讨论】:

  • 如果我希望不满足条件的行保持不变,而不是NA,逻辑是什么?
  • mutate(g = ifelse(condition1, 2, ifelse(condition2, 3, g))
  • case_when 太漂亮了,我花了很长时间才弄清楚它确实在那里。我认为这应该在最简单的 dplyr 教程中,需要计算数据子集的东西是很常见的,但仍然希望保持数据完整。
  • @G。 Grothendieck:从上面的讨论和帮助文件中,我理解了if_else()ifelse() 之间的区别。你能正确使用ifelse()mutate 函数吗?是否有任何建议使用dplyr::if_else() 而不是base:ifelse() - 即使用更严格的if_else() 可以避免下游不必要的问题?
  • 你可以使用任何一个。这主要取决于您更喜欢严格还是灵活。
【解决方案2】:

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

编辑:如果您使用的是包的 0.7.0 版之前的 dplyr::case_when(),那么您需要在变量名前加上 '.$'(例如在里面写 .$a == 1 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 %&gt;% mutate(g = with(., case_when(a %in% c(2,5,7) | (a==1 &amp; b==4) ~ 2L, a %in% c(0,1,3,4) | c==4 ~ 3L, TRUE ~ NA_integer_)))
  • 这个基准是微秒/毫秒/天吗,什么?如果没有提供测量单位,这个基准是没有意义的。此外,对小于 1e6 的数据集进行基准测试也没有意义,因为它无法扩展。
  • 请修改您的答案,在新版本的 dplyr 中您不再需要 .$
  • @Matifou DT[, col1 := case_when(col2 &gt; value ~ TRUE)]DT[col2 &gt; value, col1 := TRUE] 有什么区别?
【解决方案3】:

由于您要求其他更好的方法来处理问题,这里有另一种方法使用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-elseit can evaluate both 'yes' and 'no' cases 具有更好的性能,并且嵌套可能更难读取/维护恕我直言。


以下是相对较大数据的基准:

# 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

不确定这是否是您要求的替代方法,但我希望它有所帮助。

【讨论】:

  • 一段不错的代码! G. Grotendieck 的答案有效且简短,因此我选择了该答案作为我的问题的答案,但我感谢您的解决方案。我肯定也会这样尝试。
  • 由于 DT_fun 正在就地修改其输入,因此基准测试可能不太公平 - 除了没有从第二次迭代中接收到相同的输入(这可能会影响时间,因为 DT$g 已经分配?),结果也传播回ans1,因此可能(如果R的优化器认为有必要?对此不确定......)避免另一个复制DPLYR_funBASE_fun需要制作吗?
  • 只是要明确一点,我认为data.table 解决方案很棒,我使用data.table 任何我真正需要对表进行操作的速度并且我不想一路走下去到 C++。不过,它确实需要非常小心地进行修改!
  • 我正在尝试习惯 data.table 中更整洁的东西,这是一个非常常见的用例示例之一,即 data.table 更易于阅读且更高效.我想要在我的词汇表中开发更多 tidyverse 的主要原因是我自己和其他人的可读性,但在这种情况下,似乎 data.table 胜出。
【解决方案4】:

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

【讨论】:

    【解决方案5】:

    mosaic 包中的 derivedFactor 函数似乎旨在处理这个问题。使用这个例子,它看起来像:

    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 也可以用于任意数量的条件。

    【讨论】:

    • @hadley 应该使它成为 dplyr 的默认语法。需要嵌套的“ifelse”语句是包中最糟糕的部分,这主要是因为其他功能非常好
    • 您还可以使用.asFactor = F 选项或在同一包中使用(类似的)derivedVariable 函数来防止结果成为因素。
    • 看起来像来自 dplyr 0.5 的 recode 会这样做。不过我还没有调查过。见blog.rstudio.org/2016/06/27/dplyr-0-5-0
    猜你喜欢
    • 1970-01-01
    • 2017-06-02
    • 1970-01-01
    • 2020-01-16
    • 1970-01-01
    • 1970-01-01
    • 2014-04-15
    • 1970-01-01
    • 2017-12-30
    相关资源
    最近更新 更多