【问题标题】:dplyr mutate/replace several columns on a subset of rowsdplyr 变异/替换行子集上的几列
【发布时间】:2016-03-09 20:20:34
【问题描述】:

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

提前感谢您的帮助!

【问题讨论】:

    标签: r data.table dplyr


    【解决方案1】:

    这些解决方案(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 我们可以通过管道中的 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 '.'")))
    

    4) row_case_when 还可以查看定义的row_case_when Returning a tibble: how to vectorize with 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 是对这里答案的评论。

    【讨论】:

    • 很好的答案,谢谢!您的 mutate_cond 和@Kevin Ushey 的 mutate_when 都是解决此问题的好方法。我认为我对 mutate_when 的可读性/灵活性稍有偏好,但我会给这个答案“检查”以确保彻底性。
    • 我真的很喜欢 mutate_cond 方法。我似乎也喜欢这个功能或非常接近它的东西值得包含在 dplyr 中,并且对于人们在这里考虑的用例来说,这将是比 VectorizedSwitch(在github.com/hadley/dplyr/issues/1573 中讨论)更好的解决方案......
    • 我喜欢 mutate_cond。各种选项应该是单独的答案。
    • 已经有几年了,github 问题似乎已经关闭和锁定。这个问题有官方解决方案吗?
    • 这是问题中示例的一个很好的解决方案,但它不能在 DF 中创建新行。由于 .data[condition, ] &lt;- .data[condition, ] %&gt;% mutate(...) 中的行数较少,因此会引发错误 Can't recycle input of size *x* to size *(&lt;x)*. 如果该列尚不存在,则将不满足条件的行设为 NA 会很好
    【解决方案2】:

    你可以用magrittr的双向管道%&lt;&gt;%做到这一点:

    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,它将不起作用。
    • @UrsusFrost 添加一个只是数据集子集的新列对我来说似乎很奇怪。您将 NA 添加到未子集的行?
    • @Baraliuh 是的,我很感激。它是循环的一部分,在该循环中,我在日期列表上增加和附加数据。前几个日期的处理方式必须与后续日期不同,因为它正在复制现实世界的业务流程。在进一步的迭代中,根据日期的条件,数据的计算方式不同。由于条件限制,我不想无意中更改data.frame 中的先前日期。 FWIW,我只是回到使用data.table 而不是dplyr 因为它的i 表达式可以轻松处理这个问题——而且整个循环运行得更快。
    【解决方案3】:

    这是我喜欢的解决方案:

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

    它的可读性很强——尽管它的性能可能不如预期的那么好。

    【讨论】:

      【解决方案4】:

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

      【讨论】:

        【解决方案5】:

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

        无关:倒数第二行使用dplyr 增加了一点filter()

        开头的三个新行获取在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))
        

        【讨论】:

        • 您的 mutate_cond 函数在我的数据集上引发了错误,而 Grothendiecks 的函数则没有。 Error: incorrect length (4700), expecting: 168好像和过滤功能有关。
        • 您是否将其放入库或将其形式化为函数?这似乎很容易,尤其是在所有改进之后。
        • 没有。我认为此时使用 dplyr 的最佳方法是将 mutate 与 if_elsecase_when 结合使用。
        • 你能提供一个例子(或链接)到这种方法吗?
        【解决方案6】:

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

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

        【讨论】:

        • 哇,这很好。我从来不知道 rbind 可以嵌套另一个过滤器!
        • 事实上 rbind() 并没有真正做那里的工作。 rbind 调用中的表达式只是被评估为另一个 tibble。因此,这种对数据进行小操作或过滤的模式在传递给任何函数时都有效。如果您有较大的操作,它会变得特别难看,因此如果您对数据框进行重大转换以将其存储为中间名称,通常更可取。
        【解决方案7】:

        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
        }
        

        【讨论】:

        • 感谢马格努斯!我正在使用它来更新包含构成动画的所有对象的动作和时间的表。我遇到了 NA 问题,因为数据如此多样,以至于某些操作对某些对象没有意义,所以我在这些单元格中有 NA。上面的其他 mutate_cond 崩溃了,但你的解决方案就像一个魅力。
        • 如果这对你有用,这个功能可以在我写的一个小包中使用,“zulutils”。它不在 CRAN 上,但您可以使用 remotes::install_github("torfason/zulutils") 安装它
        【解决方案8】:

        我实际上没有看到对dplyr 的任何更改,这会使这变得更容易。 case_when 非常适合一列有多个不同条件和结果的情况,但对于您想根据一个条件更改多个列的情况没有帮助。同样,recode 如果您要替换一列中的多个不同值,则可以节省输入,但对于同时在多列中这样做没有帮助。最后,mutate_at 等仅将条件应用于列名而不是数据框中的行。您可能会为 mutate_at 编写一个可以执行此操作的函数,但我无法弄清楚您将如何使其对不同列的行为有所不同。

        这就是我将如何使用nest 形式tidyrmap 来自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()
        

        【讨论】:

        • 我唯一建议的是使用nest(-measure) 来避免group_by
        • 编辑以反映@DaveGruenewald 的建议
        【解决方案9】:

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

        【讨论】:

          【解决方案10】:

          您可以拆分数据集并对TRUE 部分进行常规变异调用。

          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,然后在rowid 上使用dplyr::arrange,最后选择它。

          数据

          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)
          

          【讨论】:

            【解决方案11】:

            我认为这个答案之前没有提到过。它的运行速度几乎与“默认”data.table-solution..

            使用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 进行子集化...因此第一个替换中的 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
            

            【讨论】:

              【解决方案12】:

              以打破通常的 dplyr 语法为代价,您可以使用来自 base 的within

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

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

              【讨论】:

              • 这不像写的那样工作,因为第二个任务实际上并没有发生。但如果你这样做 dt %&gt;% within({ delta.watts[measure == 'exit'] &lt;- 13 ; qty.exit[measure == 'exit'] &lt;- qty[measure == 'exit'] ; cf[measure == 'exit'] &lt;- 0 }) 那么它确实有效
              猜你喜欢
              • 1970-01-01
              • 2021-12-13
              • 2016-11-06
              • 1970-01-01
              • 2015-04-29
              • 2018-08-24
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多