dplyr mutate /替换行子集上的多个列

时间:2015-12-04 19:39:12

标签: r data.table dplyr

我正在尝试基于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,因为我不想多次输入条件 - 这是一个简化的例子,但有时很多基于单个条件的赋值。

提前感谢您的帮助!

12 个答案:

答案 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)因子输出条件通过使条件成为额外的列(后来被删除)来排除条件。然后使用ifelsereplace或算术与逻辑,如图所示。这也适用于数据表。

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问题1346311518和{}中还讨论了如何轻松指定更新行子集的问题。 {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.framenew_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)

与上述相同,但也在条件中未包含的行中创建新变量xNA)。以前不可能。

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初始化为FALSEyNA

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