我正在尝试基于dplyr的工作流程(而不是主要使用我习惯的data.table),而且我遇到了一个问题,我无法找到一个等效的dplyr解决方案.我经常遇到需要根据单个条件有条件地更新/替换多个列的场景.这是一些示例代码,我的data.table解决方案:
library(data.table) # Create some sample data set.seed(1) dt <- data.table(site = sample(1:6, 50, replace=T), space = sample(1:4, 50, replace=T), measure = sample(c('cfl', 'led', 'linear', 'exit'), 50, replace=T), qty = round(runif(50) * 30), qty.exit = 0, delta.watts = sample(10.5:100.5, 50, replace=T), cf = runif(50)) # Replace the values of several columns for rows where measure is "exit" dt <- dt[measure == 'exit', `:=`(qty.exit = qty, cf = 0, delta.watts = 13)]
是否有一个简单的dplyr解决方案来解决同样的问题?我想避免使用ifelse,因为我不想多次输入条件 - 这是一个简化的例子,但有时很多基于单个条件的赋值.
在此先感谢您的帮助!
这些解决方案(1)维护管道,(2)不覆盖输入,(3)只需要指定条件一次:
1a)mutate_cond为可以合并到管道中的数据帧或数据表创建一个简单的函数.此函数类似mutate
但仅作用于满足条件的行:
mutate_cond <- function(.data, condition, ..., envir = parent.frame()) { condition <- eval(substitute(condition), .data, envir) .data[condition, ] <- .data[condition, ] %>% mutate(...) .data } DF %>% mutate_cond(measure == 'exit', qty.exit = qty, cf = 0, delta.watts = 13)
1b)mutate_last这是数据帧或数据表的替代函数,它又类似mutate
但仅在其中使用group_by
(如下例所示),并且仅对最后一个组而不是每个组进行操作.请注意,TRUE> FALSE,因此如果group_by
指定条件,则mutate_last
仅对满足该条件的行进行操作.
mutate_last <- function(.data, ...) { n <- n_groups(.data) indices <- attr(.data, "indices")[[n]] + 1 .data[indices, ] <- .data[indices, ] %>% mutate(...) .data } DF %>% group_by(is.exit = measure == 'exit') %>% mutate_last(qty.exit = qty, cf = 0, delta.watts = 13) %>% ungroup() %>% select(-is.exit)
2)因子分解条件通过使条件成为额外的列而将其删除后将其排除.然后使用ifelse
,replace
或如图所示使用逻辑算术.这也适用于数据表.
library(dplyr) DF %>% mutate(is.exit = measure == 'exit', qty.exit = ifelse(is.exit, qty, qty.exit), cf = (!is.exit) * cf, delta.watts = replace(delta.watts, is.exit, 13)) %>% select(-is.exit)
3)sqldf我们可以update
通过管道中的sqldf包使用SQL 来获取数据帧(但不是数据表,除非我们转换它们 - 这可能代表dplyr中的一个错误.请参阅dplyr issue 1579).可能看起来由于存在而不希望地修改该代码中的输入,update
但实际上update
是在临时生成的数据库中而不是在实际输入上作用于输入的副本.
library(sqldf) DF %>% do(sqldf(c("update '.' set 'qty.exit' = qty, cf = 0, 'delta.watts' = 13 where measure = 'exit'", "select * from '.'")))
注1:我们用它作为DF
set.seed(1) DF <- data.frame(site = sample(1:6, 50, replace=T), space = sample(1:4, 50, replace=T), measure = sample(c('cfl', 'led', 'linear', 'exit'), 50, replace=T), qty = round(runif(50) * 30), qty.exit = 0, delta.watts = sample(10.5:100.5, 50, replace=T), cf = runif(50))
注2:如何轻松地更新指定行的子集的问题也讨论了dplyr问题134,631,1518和1573与631是主线程和1573是这里的答案进行了审查.
你可以用magrittr
双向管道做到这一点%<>%
:
library(dplyr) library(magrittr) dt[dt$measure=="exit",] %<>% mutate(qty.exit = qty, cf = 0, delta.watts = 13)
这减少了打字的数量,但仍然慢得多data.table
.
这是我喜欢的解决方案:
mutate_when <- function(data, ...) { dots <- eval(substitute(alist(...))) for (i in seq(1, length(dots), by = 2)) { condition <- eval(dots[[i]], envir = data) mutations <- eval(dots[[i + 1]], envir = data[condition, , drop = FALSE]) data[condition, names(mutations)] <- mutations } data }
它可以让你写出像
mtcars %>% mutate_when( mpg > 22, list(cyl = 100), disp == 160, list(cyl = 200) )
这是非常易读的 - 尽管它可能没有那么高效.
正如上面的eipi10所示,没有一种简单的方法可以在dplyr中进行子集替换,因为DT使用pass-by-reference语法与使用pass-by-value的dplyr.dplyr需要在ifelse()
整个向量上使用,而DT将执行子集并通过引用更新(返回整个DT).因此,对于本练习,DT将大大加快.
您可以选择先进行子集,然后进行更新,最后重新组合:
dt.sub <- dt[dt$measure == "exit",] %>% mutate(qty.exit= qty, cf= 0, delta.watts= 13) dt.new <- rbind(dt.sub, dt[dt$measure != "exit",])
但DT会快得多:(编辑使用eipi10的新答案)
library(data.table) library(dplyr) library(microbenchmark) microbenchmark(dt= {dt <- dt[measure == 'exit', `:=`(qty.exit = qty, cf = 0, delta.watts = 13)]}, eipi10= {dt[dt$measure=="exit",] %<>% mutate(qty.exit = qty, cf = 0, delta.watts = 13)}, alex= {dt.sub <- dt[dt$measure == "exit",] %>% mutate(qty.exit= qty, cf= 0, delta.watts= 13) dt.new <- rbind(dt.sub, dt[dt$measure != "exit",])}) Unit: microseconds expr min lq mean median uq max neval cld dt 591.480 672.2565 747.0771 743.341 780.973 1837.539 100 a eipi10 3481.212 3677.1685 4008.0314 3796.909 3936.796 6857.509 100 b alex 3412.029 3637.6350 3867.0649 3726.204 3936.985 5424.427 100 b
我偶然发现了这个,非常喜欢mutate_cond()
@G.格洛腾迪克,但认为处理新变量可能会派上用场.所以,下面有两个补充:
无关:倒数第二行做多一点dplyr
使用filter()
开头的三个新行获取用于的变量名称mutate()
,并在mutate()
发生之前初始化数据框中的任何新变量.新的变量初始化为的剩余data.frame
使用new_init
,它被设置为缺失(NA
)作为默认值.
mutate_cond <- function(.data, condition, ..., new_init = NA, envir = parent.frame()) { # Initialize any new variables as new_init new_vars <- substitute(list(...))[-1] new_vars %<>% sapply(deparse) %>% names %>% setdiff(names(.data)) .data[, new_vars] <- new_init condition <- eval(substitute(condition), .data, envir) .data[condition, ] <- .data %>% filter(condition) %>% mutate(...) .data }
以下是使用虹膜数据的一些示例:
更改Petal.Length
到88何处Species == "setosa"
.这将适用于原始功能以及此新版本.
iris %>% mutate_cond(Species == "setosa", Petal.Length = 88)
与上面相同,但也创建一个新变量x
(NA
不包括在条件中的行).以前不可能.
iris %>% mutate_cond(Species == "setosa", Petal.Length = 88, x = TRUE)
与上面相同,但条件中未包含的行x
设置为FALSE.
iris %>% mutate_cond(Species == "setosa", Petal.Length = 88, x = TRUE, new_init = FALSE)
此示例显示如何new_init
将a设置list
为初始化具有不同值的多个新变量.在这里,两个新的变量与排除行创建使用不同的值被初始化(x
初始化为FALSE
,y
如NA
)
iris %>% mutate_cond(Species == "setosa" & Sepal.Length < 5, x = TRUE, y = Sepal.Length ^ 2, new_init = list(FALSE, NA))
mutate_cond是一个很棒的函数,但是如果用于创建条件的列中不存在NA,则会产生错误。我觉得有条件的mutation应该只留下这样的行。这与filter()的行为相匹配,当条件为TRUE时,filter()返回行,但是省略了两行都为FALSE和NA。
有了这个小的改动,该功能就可以发挥出魅力:
mutate_cond <- function(.data, condition, ..., envir = parent.frame()) { condition <- eval(substitute(condition), .data, envir) condition[is.na(condition)] = FALSE .data[condition, ] <- .data[condition, ] %>% mutate(...) .data }