我正在尝试基于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,因为我不想多次输入条件 - 这是一个简化的例子,但有时很多基于单个条件的赋值。
提前感谢您的帮助!
答案 0 :(得分:62)
这些解决方案(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&gt;如果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 我们可以通过管道中的sqldf包使用SQL update
来获取数据帧(但不是数据表,除非我们转换它们 - 这可能代表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和{}中还讨论了如何轻松指定更新行子集的问题。 {3}} 1573是主要帖子,631是对答案的评论。
答案 1 :(得分:18)
您可以使用magrittr
的双向管道%<>%
执行此操作:
library(dplyr)
library(magrittr)
dt[dt$measure=="exit",] %<>% mutate(qty.exit = qty,
cf = 0,
delta.watts = 13)
这减少了输入的数量,但仍然比data.table
慢得多。
答案 2 :(得分:15)
这是我喜欢的解决方案:
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)
)
这是非常易读的 - 虽然它可能没有那么高效。
答案 3 :(得分:12)
正如上面的eipi10所示,在dplyr中进行子集替换并不是一种简单的方法,因为DT使用pass-by-value使用pass-by-reference语义而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&#39的新答案)
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
答案 4 :(得分:8)
我偶然发现了这一点,并且非常喜欢@ mutate_cond()
。格洛腾迪克,但认为处理新变量可能会派上用场。所以,下面有两个补充:
无关:使用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
设置为list
以初始化具有不同值的多个新变量。在这里,创建了两个新变量,其中使用不同的值初始化排除的行(x
初始化为FALSE
,y
为NA
)
iris %>% mutate_cond(Species == "setosa" & Sepal.Length < 5,
x = TRUE, y = Sepal.Length ^ 2,
new_init = list(FALSE, NA))
答案 5 :(得分:4)
mutate_cond是一个很棒的函数,但如果用于创建条件的列中有NA,则会出错。我觉得条件变异应该只留下这些行。这匹配filter()的行为,它在条件为TRUE时返回行,但省略了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
}
答案 6 :(得分:3)
随着rlang
的创建,Grothendieck 1a示例的略微修改版本成为可能,消除了对envir
参数的需要,因为enquo()
捕获了环境.p
是自动创建的。
mutate_rows <- function(.data, .p, ...) {
.p <- rlang::enquo(.p)
.p_lgl <- rlang::eval_tidy(.p, .data)
.data[.p_lgl, ] <- .data[.p_lgl, ] %>% mutate(...)
.data
}
dt %>% mutate_rows(measure == "exit", qty.exit = qty, cf = 0, delta.watts = 13)
答案 7 :(得分:3)
我实际上没有看到对dplyr
的任何更改,这会使此操作变得容易得多。 case_when
非常适用于一列有多个不同的条件和结果的情况,但是对于要基于一个条件更改多个列的情况没有帮助。同样,如果要在一列中替换多个不同的值,recode
可以节省键入内容,但是一次却不能在多个列中进行替换。最后,mutate_at
等仅将条件应用于列名,而不应用于数据帧中的行。您可能会为mutate_at编写一个函数来执行此操作,但是我无法弄清楚如何使它在不同的列中表现不同。
这就是我要如何使用nest
的{{1}}格式和tidyr
的{{1}}形式来处理它的方法。
map
答案 8 :(得分:2)
您可以拆分数据集,并在TRUE
部分进行常规的mutate调用。
dplyr 0.8 具有group_split
函数,该函数按组划分(并且可以在调用中直接定义组),因此我们将在此处使用它,但是base::split
可以使用也是
library(tidyverse)
df1 %>%
group_split(measure == "exit", keep=FALSE) %>% # or `split(.$measure == "exit")`
modify_at(2,~mutate(.,qty.exit = qty, cf = 0, delta.watts = 13)) %>%
bind_rows()
# site space measure qty qty.exit delta.watts cf
# 1 1 4 led 1 0 73.5 0.246240409
# 2 2 3 cfl 25 0 56.5 0.360315879
# 3 5 4 cfl 3 0 38.5 0.279966850
# 4 5 3 linear 19 0 40.5 0.281439486
# 5 2 3 linear 18 0 82.5 0.007898384
# 6 5 1 linear 29 0 33.5 0.392412729
# 7 5 3 linear 6 0 46.5 0.970848817
# 8 4 1 led 10 0 89.5 0.404447182
# 9 4 1 led 18 0 96.5 0.115594622
# 10 6 3 linear 18 0 15.5 0.017919745
# 11 4 3 led 22 0 54.5 0.901829577
# 12 3 3 led 17 0 79.5 0.063949974
# 13 1 3 led 16 0 86.5 0.551321441
# 14 6 4 cfl 5 0 65.5 0.256845013
# 15 4 2 led 12 0 29.5 0.340603733
# 16 5 3 linear 27 0 63.5 0.895166931
# 17 1 4 led 0 0 47.5 0.173088800
# 18 5 3 linear 20 0 89.5 0.438504370
# 19 2 4 cfl 18 0 45.5 0.031725246
# 20 2 3 led 24 0 94.5 0.456653397
# 21 3 3 cfl 24 0 73.5 0.161274319
# 22 5 3 led 9 0 62.5 0.252212124
# 23 5 1 led 15 0 40.5 0.115608182
# 24 3 3 cfl 3 0 89.5 0.066147321
# 25 6 4 cfl 2 0 35.5 0.007888337
# 26 5 1 linear 7 0 51.5 0.835458916
# 27 2 3 linear 28 0 36.5 0.691483644
# 28 5 4 led 6 0 43.5 0.604847889
# 29 6 1 linear 12 0 59.5 0.918838163
# 30 3 3 linear 7 0 73.5 0.471644760
# 31 4 2 led 5 0 34.5 0.972078100
# 32 1 3 cfl 17 0 80.5 0.457241602
# 33 5 4 linear 3 0 16.5 0.492500255
# 34 3 2 cfl 12 0 44.5 0.804236607
# 35 2 2 cfl 21 0 50.5 0.845094268
# 36 3 2 linear 10 0 23.5 0.637194873
# 37 4 3 led 6 0 69.5 0.161431896
# 38 3 2 exit 19 19 13.0 0.000000000
# 39 6 3 exit 7 7 13.0 0.000000000
# 40 6 2 exit 20 20 13.0 0.000000000
# 41 3 2 exit 1 1 13.0 0.000000000
# 42 2 4 exit 19 19 13.0 0.000000000
# 43 3 1 exit 24 24 13.0 0.000000000
# 44 3 3 exit 16 16 13.0 0.000000000
# 45 5 3 exit 9 9 13.0 0.000000000
# 46 2 3 exit 6 6 13.0 0.000000000
# 47 4 1 exit 1 1 13.0 0.000000000
# 48 1 1 exit 14 14 13.0 0.000000000
# 49 6 3 exit 7 7 13.0 0.000000000
# 50 2 4 exit 3 3 13.0 0.000000000
如果行顺序很重要,请先使用tibble::rowid_to_column
,然后使用dplyr::arrange
上的rowid
,最后选择它。
df1 <- 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),
stringsAsFactors = F)
答案 9 :(得分:1)
以破坏通常的dplyr语法为代价,您可以使用基础中的within
:
dt %>% within(qty.exit[measure == 'exit'] <- qty[measure == 'exit'],
delta.watts[measure == 'exit'] <- 13)
它似乎与管道很好地集成,你可以在其中做任何你想做的事情。
答案 10 :(得分:1)
我认为以前没有提到这个答案。它的运行速度几乎与“默认” data.table
解决方案一样快。.
使用base::replace()
df %>% mutate( qty.exit = replace( qty.exit, measure == 'exit', qty[ measure == 'exit'] ),
cf = replace( cf, measure == 'exit', 0 ),
delta.watts = replace( delta.watts, measure == 'exit', 13 ) )
replace回收替换值,因此,当您希望将列qty
的值输入到列qty.exit
中时,还必须对qty
进行子集处理,因此{{1 }}中的第一个替换项。
现在,您可能不希望一直重新输入qty[ measure == 'exit']
……因此,您将创建一个包含该选择的索引向量,并在上面的函数中使用它。.
measure == 'exit'
基准
#build an index-vector matching the condition
index.v <- which( df$measure == 'exit' )
df %>% mutate( qty.exit = replace( qty.exit, index.v, qty[ index.v] ),
cf = replace( cf, index.v, 0 ),
delta.watts = replace( delta.watts, index.v, 13 ) )
答案 11 :(得分:0)
一种简洁的解决方案是对过滤后的子集进行突变,然后重新添加表的非退出行:
library(dplyr)
dt %>%
filter(measure == 'exit') %>%
mutate(qty.exit = qty, cf = 0, delta.watts = 13) %>%
rbind(dt %>% filter(measure != 'exit'))