zoukankan      html  css  js  c++  java
  • [R]在dplyr函数的基础上编写函数(3)tidyeval

    dplyr的优点很明显,数据框操作简洁,如filter(df, x == 1, y == 2, z == 3)等于df[df$x == 1 & df$y ==2 & df$z == 3, ]。然而优点也是缺点,因为它的的参数不是透明的,这意味着你不能用一个看似等价的对象代替一个在别处定义的值。

    df <- tibble(x = 1:3, y = 3:1)
    filter(df, x == 1)
    
    #错误
    my_var <- x
    filter(df, my_var == 1)
    
    #同样错误
    my_var <- "x"
    filter(df, my_var == 1)
    

    从上我们可以看出在针对dplyr编写函数时,传参并非我们想象的那么容易。

    dplyr代码不明确,取决于在什么地方定义了什么变量。

    filter(df, x == y)
    
    #等价于以下任意代码:
    df[df$x == df$y, ]
    df[df$x == y, ]
    df[x == df$y, ]
    df[x == y, ]
    

    预热一下

    greet <- function(name) {
      "How do you do, name?"
    }
    greet("Hadley") 
    [1] "How do you do, name?"
    

    传参失败,因为引号把参数括起来,没有对输入的东西进行解释,它仅仅将输入作为一个字符串进行存储。一种解决的办法是使用paste函数将字符串粘连起来。

    greet <- function(name) {
      paste0("How do you do, ", name, "?")
    }
    greet("Hadley")
    ## [1] "How do you do, Hadley?"
    

    另一个方法是使用glue包:“unquote”一个字符串内容(就是取消引号),用R表达式的值替换字符串。这就优雅地实现我们的函数,因为{name}被替换为name参数的值。

    greet <- function(name) {
      glue::glue("How do you do, {name}?")
    }
    greet("Hadley")
    ## How do you do, Hadley?
    

    开始编程

    1.对不同数据集编写函数

    dplyr的第一个参数data是透明的,这个参数没有做任何特殊的处理。

    mutate(df1, y = a + x)
    mutate(df2, y = a + x)
    mutate(df3, y = a + x)
    mutate(df4, y = a + x)
    

    我们想对以上数据编写一个函数来避免重复。

    mutate_y <- function(df) {
      mutate(df, y = a + x)
    }
    

    但这个函数存在一个缺点:如果其中一个变量不存在于数据框中,但存在于全局环境时,则有可能出错。

    df1 <- tibble(x = 1:3)
    a <- 10  #来自全局环境的变量
    mutate_y(df1)
    

    我们可以通过使用.data代词(pronoun)更明确地指定,来修正这种不确定性。这时如果变量不存在,这会抛出一个信息错误。

    mutate_y <- function(df) {
      mutate(df, y = .data$a + .data$x)
    }
    mutate_y(df1)
    ## Error in mutate_impl(.data, dots): Evaluation error: Column `a`: not found in data.
    

    2.对不同表达式编写函数

    如果我们想要函数的一个参数是变量名(如x)或者一个表达式(如x + y)是非常困难的,因此dplyr自动将输入括起来了(“quote”),因此它们都不是透明的。

    比如我们想要创建一个可变分组用于数据汇总。

    df <- tibble(
      g1 = c(1, 1, 2, 2, 2),
      g2 = c(1, 2, 1, 2, 1),
      a = sample(5),
      b = sample(5)
    )
    
    df %>%
      group_by(g1) %>%
      summarise(a = mean(a))
    ## # A tibble: 2 x 2
    ##      g1     a
    ##   <dbl> <dbl>
    ## 1    1.  2.50
    ## 2    2.  3.33
    
    df %>%
      group_by(g2) %>%
      summarise(a = mean(a))
    ## # A tibble: 2 x 2
    ##      g2     a
    ##   <dbl> <dbl>
    ## 1    1.  2.00
    ## 2    2.  4.50
    

    自然想到编写类似下面的函数:

    my_summarise <- function(df, group_var) {
      df %>%
        group_by(group_var) %>%
        summarise(a = mean(a))
    }
    
    my_summarise(df, g1)
    ## Error in grouped_df_impl(data, unname(vars), drop): Column `group_var` is unknown
    

    报错了。将变量名换成字符串:

    my_summarise(df, "g2")
    ## Error in grouped_df_impl(data, unname(vars), drop): Column `group_var` is unknown
    

    仍然报错。

    我们看到这两次报错是一样的。group_by()函数似乎自带引号功能:它不会评估输入,不管是啥,它都先将其括起来。

    因此想要以上函数工作,我们需要做两件事:一是自己手动把输入括起来(这样上面编写的my_summarise()函数像group_by()一样可以输入一个裸的变量名);二是告诉group_by()不要再quote它的输入(因为我们已经做过了)。

    那么,要怎样才能quote输入呢?我们不能使用"",因为它返回一个字符串。我们需要的是一个能够捕捉表达式及其环境的函数。base R中的函数quote()以及操作符~貌似可以做,但它们都不是我们真正想要的。这里,引入一个新的函数:quo(),它将输入括起来但不执行

    quo(g1)
    ## <quosure>
    ##   expr: ^g1
    ##   env:  global
    quo(a + b + c)
    ## <quosure>
    ##   expr: ^a + b + c
    ##   env:  global
    quo("a")
    ## <quosure>
    ##   expr: ^"a"
    ##   env:  empty
    

    quo() 返回的是一个quosure,这是一种特殊类型的公式。后续会讲。

    现在我们已经捕捉到了这个表达式,怎么在group_by中使用它呢?如果直接使用这个函数的结果作为我们创建函数的输入不会起作用:

    my_summarise(df, quo(g1))
    ## Error in grouped_df_impl(data, unname(vars), drop): Column `group_var` is unknown
    

    错误还是一样。因为我们还没有告诉group_by()已经处理过quote的问题,因此这里需要unquote(去掉括起)group_var变量。

    在dplyr(和通用的tidyeval)中,可以使用!!告诉动词函数你想要unquote输入从而让它执行,而不是括起来。

    联合上面操作:

    my_summarise <- function(df, group_var) {
      df %>%
        group_by(!! group_var) %>%
        summarise(a = mean(a))
    }
    
    my_summarise(df, quo(g1))
    ## # A tibble: 2 x 2
    ##      g1     a
    ##   <dbl> <dbl>
    ## 1    1.  2.50
    ## 2    2.  3.33
    

    虽然功能是实现了,但还是不够优雅,我们想要实现像group_by(df,g1)一样方便使用。因此可以将括起改到函数中:

    my_summarise <- function(df, group_var) {
      quo_group_var <- quo(group_var)
      print(quo_group_var)  #为查看错误
    
      df %>%
        group_by(!! quo_group_var) %>%
        summarise(a = mean(a))
    }
    
    my_summarise(df, g1)
    ## <quosure>
    ##   expr: ^group_var
    ##   env:  000000001DF8CAC8
    ## Error in grouped_df_impl(data, unname(vars), drop): Column `group_var` is unknown
    

    但是又报错了。这里的问题是:quo(group_var)总是返回~group_var,而我们想将它替换为~g1

    类似于字符串我们不用"",而是用一些可以将参数变成字符串的函数,enquo()就是这样的函数,它通过查看用户键入值,然后将该值返回为quosure(技术上来说,这是可以实现的,因为函数的参数都使用一种特殊的数据结构promise进行执行)。

    my_summarise <- function(df, group_var) {
      group_var <- enquo(group_var)
      print(group_var)
    
      df %>%
        group_by(!! group_var) %>%
        summarise(a = mean(a))
    }
    
    my_summarise(df, g1)
    ## <quosure>
    ##   expr: ^g1
    ##   env:  global
    ## # A tibble: 2 x 2
    ##      g1     a
    ##   <dbl> <dbl>
    ## 1    1.  2.50
    ## 2    2.  3.33
    

    对应于我们第二节讲到的base R中的quote()和substitute()函数,quo()等价于quote(),而enquo()等价于substitute()

    如果是处理多个分组变量呢?这种情况我们也更常见,接着往下看。

    3.对不同的输入变量编写函数

    summarise(df, mean = mean(a), sum = sum(a), n = n())
    ## # A tibble: 1 x 3
    ##    mean   sum     n
    ##   <dbl> <int> <int>
    ## 1    3.    15     5
    summarise(df, mean = mean(a * b), sum = sum(a * b), n = n())
    ## # A tibble: 1 x 3
    ##    mean   sum     n
    ##   <dbl> <int> <int>
    ## 1  9.60    48     5
    

    我们要对以上两项处理自定义编写一个函数,汇总三个变量。先试写一下:

    my_var <- quo(a)
    summarise(df, mean = mean(!! my_var), sum = sum(!! my_var), n = n())
    ## # A tibble: 1 x 3
    ##    mean   sum     n
    ##   <dbl> <int> <int>
    ## 1    3.    15     5
    

    我们可以直接用quo作用于dplyr函数,这是调试很好的方法:

    quo(summarise(df,
      mean = mean(!! my_var),
      sum = sum(!! my_var),
      n = n()
    ))
    ## <quosure>
    ##   expr: ^summarise(df, mean = mean(^a), sum = sum(^a), n = n())
    ##   env:  global
    

    enquo --> !!的方法我们已经了解了。下面正式编写函数:

    my_summarise2 <- function(df, expr) {
      expr <- enquo(expr)
    
      summarise(df,
        mean = mean(!! expr),
        sum = sum(!! expr),
        n = n()
      )
    }
    my_summarise2(df, a)
    ## # A tibble: 1 x 3
    ##    mean   sum     n
    ##   <dbl> <int> <int>
    ## 1    3.    15     5
    my_summarise2(df, a * b)
    ## # A tibble: 1 x 3
    ##    mean   sum     n
    ##   <dbl> <int> <int>
    ## 1  9.60    48     5
    

    发现对不同的变量/表达式也是可以的。

    4.对不同输入和输出变量编写函数

    mutate(df, mean_a = mean(a), sum_a = sum(a))
    ## # A tibble: 5 x 6
    ##      g1    g2     a     b mean_a sum_a
    ##   <dbl> <dbl> <int> <int>  <dbl> <int>
    ## 1    1.    1.     1     3     3.    15
    ## 2    1.    2.     4     2     3.    15
    ## 3    2.    1.     2     1     3.    15
    ## 4    2.    2.     5     4     3.    15
    ## # ... with 1 more row
    mutate(df, mean_b = mean(b), sum_b = sum(b))
    ## # A tibble: 5 x 6
    ##      g1    g2     a     b mean_b sum_b
    ##   <dbl> <dbl> <int> <int>  <dbl> <int>
    ## 1    1.    1.     1     3     3.    15
    ## 2    1.    2.     4     2     3.    15
    ## 3    2.    1.     2     1     3.    15
    ## 4    2.    2.     5     4     3.    15
    ## # ... with 1 more row
    

    要对以上处理编写函数,看起来和前面的例子比较相似,但是有两个新的问题:

    一是要将字符串连在一起创建新的变量名。因此我们需要quo_name()将输入表达式转换为字符串。
    二是!! mean_name = mean(!! expr) 不是合法的R代码。我们要使用由rlang提供的:=帮助函数。

    my_mutate <- function(df, expr) {
      expr <- enquo(expr)
      mean_name <- paste0("mean_", quo_name(expr))
      sum_name <- paste0("sum_", quo_name(expr))
    
      mutate(df,
        !! mean_name := mean(!! expr),
        !! sum_name := sum(!! expr)
      )
    }
    
    my_mutate(df, a)
    ## # A tibble: 5 x 6
    ##      g1    g2     a     b mean_a sum_a
    ##   <dbl> <dbl> <int> <int>  <dbl> <int>
    ## 1    1.    1.     1     3     3.    15
    ## 2    1.    2.     4     2     3.    15
    ## 3    2.    1.     2     1     3.    15
    ## 4    2.    2.     5     4     3.    15
    ## # ... with 1 more row
    

    5.捕获多个变量

    这里我们要将my_summarise()扩展到可以接收任何数目的分组变量。需要3个改变:

    • 一是在函数定义中使用...以便于我们的函数能够接收任意数目的参数。
    • 二是使用quos()去捕获所有的...作为公式列表。
    • 三是使用!!!替换!!将参数一个个切进group_by()
    my_summarise <- function(df, ...) {
      group_var <- quos(...)
    
      df %>%
        group_by(!!! group_var) %>%
        summarise(a = mean(a))
    }
    
    my_summarise(df, g1, g2)
    ## # A tibble: 4 x 3
    ## # Groups:   g1 [?]
    ##      g1    g2     a
    ##   <dbl> <dbl> <dbl>
    ## 1    1.    1.  1.00
    ## 2    1.    2.  4.00
    ## 3    2.    1.  2.50
    ## 4    2.    2.  5.00
    

    !!!将元素列表作为参数并把它们切开放入当前的函数调用。

    args <- list(na.rm = TRUE, trim = 0.25)
    quo(mean(x, !!! args))
    ## <quosure>
    ##   expr: ^mean(x, na.rm = TRUE, trim = 0.25)
    ##   env:  global
    
    args <- list(quo(x), na.rm = TRUE, trim = 0.25)
    quo(mean(!!! args))
    ## <quosure>
    ##   expr: ^mean(^x, na.rm = TRUE, trim = 0.25)
    ##   env:  global
    

    以上是tidyeval的一些基础,下一节继续深入理论,以应对编写函数新的情况。

    Ref: https://github.com/tidyverse/dplyr/blob/master/vignettes/programming.Rmd
    https://www.jianshu.com/p/5eca388205d4

  • 相关阅读:
    Javaweb中的监听器
    Maven
    Ajax
    jQuery
    Spring入门
    spring boot实战——微信点餐系统02:接口信息,分页工具,gson, 注解信息,表单验证,字段返回(时间处理,null处理)
    Nginx
    Spring Data JPA
    spring boot实战——微信点餐系统01:开始代码
    spring boot实战——微信点餐系统00:项目设计,环境搭建
  • 原文地址:https://www.cnblogs.com/jessepeng/p/11443869.html
Copyright © 2011-2022 走看看