首页 文章

dplyr对行的子集进行mutate / replace

提问于
浏览
46

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

在此先感谢您的帮助!

8 回答

  • 2

    以破坏通常的dplyr语法为代价,您可以使用 within from base:

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

    它似乎与管道很好地集成,你可以在里面做任何你想做的事情 .

  • 12

    这些解决方案(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) factor out condition 将条件设为一个额外的列,然后将其删除 . 然后使用 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 '.'")))
    

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

    Note 2: 在dplyr问题13463115181573中讨论了如何轻松指定更新行子集的问题,其中631是主线程,1573是对此答案的回顾 .

  • 4

    您可以使用 magrittr 的双向管道 %<>% 执行此操作:

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

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

  • 8

    这是我喜欢的解决方案:

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

    这是非常易读的 - 尽管它可能没有那么高效 .

  • 0

    正如上面的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
    
  • 17

    我偶然发现了这个,非常喜欢@G的 mutate_cond() . 格洛腾迪克,但认为处理新变量可能会派上用场 . 所以,下面有两个补充:

    不相关:通过使用 filter() ,倒数第二行更多 dplyr

    开头的三个新行获取变量名称以便在 mutate() 中使用,并在 mutate() 发生之前初始化数据框中的任何新变量 . 使用 new_initdata.frame 的剩余部分初始化新变量,默认情况下将其设置为缺失( 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))
    
  • 53

    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
    }
    
  • 8

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

相关问题