dplyr更改/替换行子集上的几列


85

我正在尝试一个基于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,因为我不想多次键入条件-这是一个简化的示例,但是有时基于一个条件会有很多分配。

先谢谢您的帮助!

Answers:


81

这些解决方案(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)分解条件通过将其作为额外的列来分解条件,然后再将其删除。然后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我们可以update通过流水线中的sqldf包使用SQL来存储数据帧(除非对数据表进行转换,否则就不能使用它-这可能表示dplyr中的错误。请参见dplyr第1579版)。似乎由于存在,我们正在不希望地修改此代码中的输入,update但实际上,它对update临时生成的数据库中的输入副本起作用,而不是对实际输入起作用。

library(sqldf)

DF %>% 
   do(sqldf(c("update '.' 
                 set 'qty.exit' = qty, cf = 0, 'delta.watts' = 13 
                 where measure = 'exit'", 
              "select * from '.'")))

4)row_case_when还要签出返回小标题中row_case_when定义的内容 :如何使用case_when向量化?。它使用类似于case_when但适用于行的语法。

library(dplyr)

DF %>%
  row_case_when(
    measure == "exit" ~ data.frame(qty.exit = qty, cf = 0, delta.watts = 13),
    TRUE ~ data.frame(qty.exit, cf, delta.watts)
  )

注意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问题讨论13463115181573631是主线程和1573是这里的答案进行了审查。


1
很好的答案,谢谢!您的mutate_cond和@Kevin Ushey的mutate_when都是解决此问题的好方法。我认为我对mutate_when的可读性/灵活性稍有偏爱,但我将为这个答案提供“检查”以确保完整性。
克里斯·牛顿

我真的很喜欢mutate_cond方法。似乎我也喜欢这个功能或与其非常接近的功能,应该包含在dplyr中,对于人们正在思考的用例,它比VectorizedSwitch(在github.com/hadley/dplyr/issues/1573中进行了讨论)是一个更好的解决方案。关于这里...
Magnus

我喜欢mutate_cond。各种选项应该是单独的答案。
霍尔格·布兰德尔

已经有几年了,github问题似乎已经关闭并锁定了。这个问题有官方解决方案吗?
static_rtti

27

您可以使用magrittr的双向管道来做到这一点%<>%

library(dplyr)
library(magrittr)

dt[dt$measure=="exit",] %<>% mutate(qty.exit = qty,
                                    cf = 0,  
                                    delta.watts = 13)

这样可以减少打字量,但仍然比慢得多data.table


实际上,既然我有机会对此进行了测试,那么我宁愿选择一种解决方案,该解决方案避免使用dt [dt $ measure =='exit',]表示法进行子集化,因为随着时间的推移,这样做可能会变得笨拙dt名称。
克里斯·牛顿

只是一个仅供参考,但是仅当data.frame/tibble已经包含定义的列时,此解决方案才有效mutate。如果您尝试添加新列(例如,第一次运行循环并修改),则将无法使用data.frame
Ursus Frost

@UrsusFrost添加仅是数据集子集的新列对我来说很奇怪。您是否将NA添加到没有子集的行?
Baraliuh18年

@Baraliuh是的,我很感激。这是循环的一部分,在该循环中,我将数据增加并追加到日期列表中。前几个日期必须与后续日期区别对待,因为它复制了真实的业务流程。在进一步的迭代中,根据日期条件,数据的计算方式有所不同。由于有条件限制,我不想无意中更改中的先前日期data.frame。FWIW,我只是回到使用上,data.table而不是dplyr因为它的i表达式可以轻松地处理它-而且整个循环运行得更快。
Ursus Frost

18

这是我喜欢的解决方案:

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

可读性很强-尽管性能可能不尽如人意。


14

如上eipi10所示,在dplyr中没有简单的方法来进行子集替换,因为DT使用按引用传递语义而不是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

10

我只是偶然发现了这个,真的很喜欢mutate_cond()@G。Grothendieck,但认为处理新变量可能会派上用场。因此,下面有两个补充:

无关:倒数第二行dplyr使用filter()

开头的三行新行获取在中使用的变量名mutate(),并在mutate()出现之前初始化数据帧中的所有新变量。在data.frameusing的其余部分初始化新变量new_initNA默认情况下将其设置为missing()。

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

iris %>% mutate_cond(Species == "setosa" & Sepal.Length < 5,
                  x = TRUE, y = Sepal.Length ^ 2,
                  new_init = list(FALSE, NA))

您的mutate_cond函数在我的数据集上引发错误,而Grothendiecks的函数则没有。Error: incorrect length (4700), expecting: 168似乎与过滤功能有关。
RHA

您是否已将其放入库中或作为函数形式化了?似乎毫无疑问,特别是在所有改进方面。
荨麻

1
否。我认为目前使用dplyr的最佳方法是将mutate与if_else或结合case_when
西蒙·杰克逊

您可以提供这种方法的示例(或链接)吗?
荨麻

6

mutate_cond是一个很棒的函数,但是如果用于创建条件的列中不存在NA,则会产生错误。我觉得有条件的mutation应该只留下这样的行。这与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
}

谢谢马格努斯!我正在使用它来更新一个表格,其中包含组成动画的所有对象的动作和时间。我遇到了NA问题,因为数据是如此多变,以至于某些对象的某些操作毫无意义,因此这些单元格中都包含NA。上面的另一个mutate_cond崩溃了,但是您的解决方案像一个魔术一样工作。
Phil van Kleur

如果这对您有用,则可以在我写的一个小包装“ zulutils”中使用此功能。它不在CRAN上,但是您可以使用remotes :: install_github(“ torfason / zulutils”)
Magnus,

大!非常感谢。我还在用
Phil van Kleur

4

我实际上看不到有任何更改dplyr可以使此操作变得容易得多。case_when当一列有多个不同的条件和结果时,该选项非常有用,但对于要基于一个条件更改多个列的情况,这无济于事。同样,recode如果要在一列中替换多个不同的值,但是一次又一次在多个列中执行替换操作,则可以节省键入内容。最后,mutate_at等等仅将条件应用于列名,而不应用于数据帧中的行。您可能会为mutate_at编写一个函数来执行此操作,但我无法弄清楚如何使它在不同的列中表现不同。

就是说,这就是我将如何使用nestformtidyrmapfrom来处理它purrr

library(data.table)
library(dplyr)
library(tidyr)
library(purrr)

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

dt2 <- dt %>% 
  nest(-measure) %>% 
  mutate(data = if_else(
    measure == "exit", 
    map(data, function(x) mutate(x, qty.exit = qty, cf = 0, delta.watts = 13)),
    data
  )) %>%
  unnest()

1
我唯一建议的是nest(-measure)避免使用group_by
Dave Gruenewald '18

编辑以反映@DaveGruenewald的建议

4

一种简洁的解决方案是对过滤后的子集进行突变,然后重新添加表的非退出行:

library(dplyr)

dt %>% 
    filter(measure == 'exit') %>%
    mutate(qty.exit = qty, cf = 0, delta.watts = 13) %>%
    rbind(dt %>% filter(measure != 'exit'))

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)

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::arrangerowid,并最终选择出来。

数据

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)

2

我认为这个答案以前没有提到过。它的运行速度几乎与“默认”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输入到colums中时qty.exit,还必须对其进行子集化qty …因此是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 ) )

基准

# Unit: milliseconds
#         expr      min       lq     mean   median       uq      max neval
# data.table   1.005018 1.053370 1.137456 1.112871 1.186228 1.690996   100
# wimpel       1.061052 1.079128 1.218183 1.105037 1.137272 7.390613   100
# wimpel.index 1.043881 1.064818 1.131675 1.085304 1.108502 4.192995   100

1

以牺牲常规dplyr语法为代价,可以使用withinfrom base:

dt %>% within(qty.exit[measure == 'exit'] <- qty[measure == 'exit'],
              delta.watts[measure == 'exit'] <- 13)

它似乎可以与管道很好地集成在一起,并且您可以在管道中执行几乎任何您想做的事情。


这不能按书面要求工作,因为第二次分配实际上并未发生。但是,如果这样做,dt %>% within({ delta.watts[measure == 'exit'] <- 13 ; qty.exit[measure == 'exit'] <- qty[measure == 'exit'] ; cf[measure == 'exit'] <- 0 })它确实起作用了
参阅
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.