【问题标题】:Create R function using dplyr::filter problem使用 dplyr::filter 问题创建 R 函数
【发布时间】:2019-03-04 18:04:27
【问题描述】:

我查看了其他答案,但找不到以下代码工作的解决方案。基本上,我正在创建一个函数,该函数将 inner_join 两个数据框和 filter 基于函数中输入的列。

问题是函数的filter 部分不起作用。但是,如果我从函数中删除过滤器并将其附加为 mydiff("a") %>% filter(a.x != a.y)

任何建议都是有帮助的。

注意我是函数输入引号

library(dplyr)

# fake data
df1<- tibble(id = seq(4,19,2), 
             a = c("a","b","c","d","e","f","g","h"), 
             b = c(rep("foo",3), rep("bar",5)))
df2<- tibble(id = seq(10, 20, 1), 
             a = c("d","a", "e","f","k","m","g","i","h", "a", "b"),
             b = c(rep("bar", 7), rep("foo",4)))

# What I am trying to do
dplyr::inner_join(df1, df2, by = "id") %>% select(id, b.x, b.y) %>% filter(b.x!=b.y)

#> # A tibble: 1 x 3
#>      id b.x   b.y  
#>   <dbl> <chr> <chr>
#> 1    18 bar   foo

# creating a function so that I can filter by difference in column if I have more columns
mydiff <- function(filteron, df_1 = df1, df_2 = df2){
  require(dplyr, warn.conflicts = F)
  col_1 = paste0(quo_name(filteron), "x")
  col_2 = paste0(quo_name(filteron), "y")
  my_df<- inner_join(df_1, df_2, by = "id", suffix = c("x", "y"))
  my_df %>% select(id, col_1, col_2) %>% filter(col_1 != col_2)
}

# the filter part is not working as expected. 
# There is no difference whether i pipe filter or leave it out
mydiff("a")

#> # A tibble: 5 x 3
#>      id ax    ay   
#>   <dbl> <chr> <chr>
#> 1    10 d     d    
#> 2    12 e     e    
#> 3    14 f     k    
#> 4    16 g     g    
#> 5    18 h     h

【问题讨论】:

    标签: r filter dplyr rlang tidyeval


    【解决方案1】:

    它在您的原始函数中不起作用的原因是 col_1stringdplyr::filter() 预期 LHS 的“未引用”输入变量。因此,您需要首先使用sym()col_1 转换为变量,然后使用!!(砰砰)在filter 中取消引用它。

    rlang 具有非常好的功能 qq_show 来显示引用/取消引用实际发生的情况(请参阅下面的输出)

    另见类似question

    library(rlang)
    library(dplyr)
    
    # creating a function that can take either string or symbol as input
    mydiff <- function(filteron, df_1 = df1, df_2 = df2) {
    
      col_1 <- paste0(quo_name(enquo(filteron)), "x")
      col_2 <- paste0(quo_name(enquo(filteron)), "y")
    
      my_df <- inner_join(df_1, df_2, by = "id", suffix = c("x", "y"))
    
      cat('\nwithout sym and unquote\n')
      qq_show(col_1 != col_2)
    
      cat('\nwith sym and unquote\n')
      qq_show(!!sym(col_1) != !!sym(col_2))
      cat('\n')
    
      my_df %>% 
        select(id, col_1, col_2) %>% 
        filter(!!sym(col_1) != !!sym(col_2))
    }
    
    ### testing: filteron as a string
    mydiff("a")
    #> 
    #> without sym and unquote
    #> col_1 != col_2
    #> 
    #> with sym and unquote
    #> ax != ay
    #> 
    #> # A tibble: 1 x 3
    #>      id ax    ay   
    #>   <dbl> <chr> <chr>
    #> 1    14 f     k
    
    ### testing: filteron as a symbol
    mydiff(a)
    #> 
    #> without sym and unquote
    #> col_1 != col_2
    #> 
    #> with sym and unquote
    #> ax != ay
    #>  
    #> # A tibble: 1 x 3
    #>      id ax    ay   
    #>   <dbl> <chr> <chr>
    #> 1    14 f     k
    

    reprex package (v0.2.1.9000) 于 2018 年 9 月 28 日创建

    【讨论】:

    • 这正是我想要的。我不知道我必须先sym 然后!! 才能工作。很高兴知道我快到了!
    • @x85ms16: 如果您将quo_nameenquo 一起使用,您可以使函数更加灵活,因为它现在可以使用字符串和符号作为输入
    【解决方案2】:

    来自https://dplyr.tidyverse.org/articles/programming.html

    大多数 dplyr 函数使用非标准评估 (NSE)。这是一个包罗万象的术语,意味着它们不遵循通常的 R 评估规则。

    这有时会在尝试将它们包装在函数中时产生一些问题。 这是您创建的函数的基本版本。

    mydiff<- function(filteron, df_1=df1, df_2 = df2){
    
                     col_1 = paste0(filteron,"x")
                     col_2 = paste0(filteron, "y")
    
                     my_df <- merge(df1, df2, by="id", suffixes = c("x","y"))
    
                     my_df[my_df[, col_1] != my_df[, col_2], c("id", col_1, col_2)]  
             }
    
    > mydiff("a")
      id ax ay
    3 14  f  k
    > mydiff("b")
      id  bx  by
    5 18 bar foo
    

    这将解决您的问题,并且很可能在现在和将来都能按预期工作。通过减少对外部包的依赖,您可以减少这些问题和其他随着包作者发展其工作而可能在未来发展的怪癖。

    【讨论】:

    • 我认为关于放弃 dplyr 的建议需要与这样做的不利因素相平衡,主要是您失去了代码对不同数据源的可移植性。
    • 有趣的观点。但也许放弃 dplyr 扩展了代码的可移植性,因为没有它编写函数变得更简单、更可预测和更一致。由于函数是包的构建块,并且包仍然是将 R 代码发送给其他人的黄金标准,因此与 dplyr 相比,基本代码更具可移植性并且可以访问更广泛的数据源。
    • @lionel 如何不使用 dplyr 影响代码的可移植性?
    • 我说的是 dplyr 后端。
    • @Justin 这是仅使用基本 R 来解决我的问题的绝佳选择。谢谢
    【解决方案3】:

    对我来说似乎是一个评估问题。试试这个修改后的mydiff 函数,使用lazyeval 包:

    mydiff <- function(filteron, df_1 = df1, df_2 = df2){
      require(dplyr, warn.conflicts = F)
      col_1 <- paste0(quo_name(filteron), "x")
      col_2 <- paste0(quo_name(filteron), "y")
      criteria <- lazyeval::interp(~ x != y, .values = list(x = as.name(col_1), y = as.name(col_2)))
      my_df <- inner_join(df_1, df_2, by = "id", suffix = c("x", "y"))
      my_df %>% select(id, col_1, col_2) %>% filter_(criteria)
    }
    

    您可以查看 Hadley Wickham 的书 Advanced R 中的 Functions chapter 了解更多信息。

    【讨论】:

      【解决方案4】:

      将基本 R 用于简单函数的建议很好,但是它不能扩展到更复杂的 tidyverse 函数,并且您失去了对 dplyr 后端(如数据库)的可移植性。如果你想围绕 tidyverse 管道创建函数,你必须学习一些关于 R 表达式和取消引用运算符 !! 的知识。我建议浏览https://tidyeval.tidyverse.org 的第一部分,以大致了解此处使用的概念。

      由于您要创建的函数采用裸列名称并且不涉及复杂的表达式(例如您将传递给mutate()summarise()),因此我们不需要像quosures 这样的花哨的东西。我们可以使用符号。要创建符号,请使用 as.name()rlang::sym()

      as.name("mycolumn")
      #> mycolumn
      
      rlang::sym("mycolumn")
      #> mycolumn
      

      后者的优势是属于更大的函数家族:ensym(),以及复数变体syms()ensyms()。我们将使用ensym() 来捕获列名,即延迟列的执行,以便在经过几次转换后将其传递给 dplyr。延迟执行称为“引用”。

      我对你的函数界面做了一些改动:

      • 首先获取数据帧以与 dplyr 函数保持一致

      • 不要为数据框提供默认值。这些默认设置做了太多假设。

      • 使 bysuffix 用户可配置,并具有合理的默认值。

      这是代码,内嵌解释:

      mydiff <- function(df1, df2, var, by = "id", suffix = c(".x", ".y")) {
        stopifnot(is.character(suffix), length(suffix) == 2)
      
        # Let's start by the easy task, joining the data frames
        df <- dplyr::inner_join(df1, df2, by = by, suffix = suffix)
      
        # Now onto dealing with the diff variable. `ensym()` takes a column
        # name and delays its execution:
        var <- rlang::ensym(var)
      
        # A delayed column name is not a string, it's a symbol. So we need
        # to transform it to a string in order to work with paste() etc.
        # `quo_name()` works in this case but is generally only for
        # providing default names.
        #
        # Better use base::as.character() or rlang::as_string() (the latter
        # works a bit better on Windows with foreign UTF-8 characters):
        var_string <- rlang::as_string(var)
      
        # Now let's add the suffix to the name:
        col1_string <- paste0(var_string, suffix[[1]])
        col2_string <- paste0(var_string, suffix[[2]])
      
        # dplyr::select() supports column names as strings but it is an
        # exception in the dplyr API. Generally, dplyr functions take bare
        # column names, i.e. symbols. So let's transform the strings back to
        # symbols:
        col1 <- rlang::sym(col1_string)
        col2 <- rlang::sym(col2_string)
      
        # The delayed column names now need to be inserted back into the
        # dplyr code. This is accomplished by unquoting with the !!
        # operator:
        df %>%
          dplyr::select(id, !!col1, !!col2) %>%
          dplyr::filter(!!col1 != !!col2)
      }
      
      mydiff(df1, df2, b)
      #> # A tibble: 1 x 3
      #>      id b.x   b.y
      #>   <dbl> <chr> <chr>
      #> 1    18 bar   foo
      
      mydiff(df1, df2, "a")
      #> # A tibble: 1 x 3
      #>      id a.x   a.y
      #>   <dbl> <chr> <chr>
      #> 1    14 f     k
      

      您还可以通过使用字符串而不是裸列名称来简化函数。在这个版本中,我将使用syms() 创建一个符号列表,并使用!!! 将其一次性传递给select()

      mydiff2 <- function(df1, df2, var, by = "id", suffix = c(".x", ".y")) {
        stopifnot(
          is.character(suffix), length(suffix) == 2,
          is.character(var), length(var) == 1
        )
      
        # Create a list of symbols from a character vector:
        cols <- rlang::syms(paste0(var, suffix))
      
        df <- dplyr::inner_join(df1, df2, by = by, suffix = suffix)
      
        # Unquote the whole list as once with the big bang !!!
        df %>%
          dplyr::select(id, !!!cols) %>%
          dplyr::filter(!!cols[[1]] != !!cols[[2]])
      }
      
      mydiff2(df1, df2, "a")
      #> # A tibble: 1 x 3
      #>      id a.x   a.y
      #>   <dbl> <chr> <chr>
      #> 1    14 f     k
      

      【讨论】:

      • 很好的答案!不知道你为什么被否决。我们可以使用quo_name(enquo(var)) 使函数mydiff2 更灵活,即可以接受字符串或符号形式的输入吗?
      • 在这种情况下它不起作用,因为输入必须遵循选择语法,因为它们被转发到select()。同时他们不能使用选择助手,因为他们也被转发到filter()。所以共同点只是裸列名。这点很好,我会把它添加到 bookdown 中。
      • 在这种情况下,我更喜欢mydiff() 而不是mydiff2(),因为它更灵活:)
      • @lionel 这非常有帮助。谢谢你详细解释事情。现在,当我编写更复杂的 dplyr 函数时,我可以参考这一点。您关于参数和引用的提示非常有帮助。一个问题,每次使用参数中的列时,是否需要取消引用!!!!!
      • 是的,围绕 dplyr 管道创建函数的主要模式是“引用和取消引用”。你用enquo()ensym()(或它们的复数变体)引用,用!!!!!取消引用。
      【解决方案5】:

      首先查找col_1 != col_2 的索引可能足以解决此问题。

      mydiff <- function(filteron, df_1 = df1, df_2 = df2){
        require(dplyr, warn.conflicts = F)
        col_1 <- paste0(quo_name(filteron), "x")
        col_2 <- paste0(quo_name(filteron), "y")
        my_df <-
          inner_join(df_1, df_2, by = "id", suffix = c("x", "y")) %>%
          select(id, col_1, col_2)
        # find indices of different columns
        same <- my_df[, col_1] != my_df[, col_2]
        # return for the rows
        my_df[same, ]
      }
      my_diff("a")
      #> # A tibble: 1 x 3
      #>      id ax    ay   
      #>   <dbl> <chr> <chr>
      #> 1    14 f     k
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2023-01-29
        • 2015-04-04
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2022-07-19
        • 1970-01-01
        相关资源
        最近更新 更多