【问题标题】:get lhs object name when piping with dplyr使用 dplyr 管道时获取 lhs 对象名称
【发布时间】:2015-07-15 10:35:26
【问题描述】:

我想要一个可以使用从 dplyr 导出的管道运算符的函数。我没有使用 magrittr。

df %>% my_function

如何获取 df 名称?如果我尝试

my_function <- function(tbl){print(deparse(substitute(tbl)))}

返回

[1] "."

虽然我想拥有 [1] "df"

有什么建议吗?

提前谢谢你,
尼古拉

【问题讨论】:

  • 虽然我认为这是一个有趣的问题,但您为什么要这样做?
  • @eddi: my_func 获取一些 dplyr 操作的输出并执行一些额外的操作来格式化数据以进行打印和导出。我想将对象中的数据导出到与对象同名的csv文件中..
  • 好吧,df %&gt;% stuff %&gt;% my_func 不可能做到这一点,因为到了 my_func 时,它很可能完全是一个不同的对象,关于最左边对象的信息将会丢失,所以我的建议是简单地做my_func(df)
  • 这个答案似乎有效:stackoverflow.com/a/42561430/5028841 或者我错过了什么?

标签: r pipe dplyr chain


【解决方案1】:

cmets 中 JBGruber links to 的 SO 答案主要解决了这个问题。它通过在执行环境中向上移动直到找到某个变量来工作,然后从该环境返回lhs。唯一缺少的是函数输出原始数据帧的名称​​和操作数据的要求——我从 OP 的一个 cmets 中收集了后一个要求。为此,我们只需要输出一个包含这些内容的列表,我们可以通过修改 MrFlick 的答案来做到这一点:

get_orig_name <- function(df){
    i <- 1
    while(!("chain_parts" %in% ls(envir=parent.frame(i))) && i < sys.nframe()) {
        i <- i+1
    }
    list(name = deparse(parent.frame(i)$lhs), output = df)
}

现在我们可以将get_orig_name 运行到任何管道的末尾,以在列表中获取操作数据和原始数据框的名称。我们使用$ 访问两者:

mtcars %>% summarize_all(mean) %>% get_orig_name

#### OUTPUT ####

$name
[1] "mtcars"

$output
       mpg    cyl     disp       hp     drat      wt     qsec     vs      am   gear   carb
1 20.09062 6.1875 230.7219 146.6875 3.596563 3.21725 17.84875 0.4375 0.40625 3.6875 2.8125

我还应该提到,虽然我认为这个策略的细节很有趣,但我也认为它是不必要的复杂。听起来 OP 的目标是操作数据,然后将其写入与原始未操作数据帧同名的文件,这可以使用更直接的方法轻松完成。例如,如果我们正在处理多个数据帧,我们可以执行以下操作:

df_list <- list(mtcars = mtcars, iris = iris)

for(name in names(df_list)){
    df_list[[name]] %>% 
        group_by_if(is.factor) %>%
        summarise_all(mean) %>% 
        write.csv(paste0(name, ".csv"))
}

【讨论】:

  • 我考虑过发布这个,但决定反对它,因为这个问题已经存在了几年,并且在链接功能中没有太多可以改进的地方。我建议的两件事是废弃这部分 &amp;&amp; i &lt; sys.nframe(),因为它似乎永远不会变成 FALSE,并使用 message(deparse(parent.frame(i)$lhs))return(df) 之前打印名称。我不会返回列表,因为进一步的管道会更复杂。
  • @JBGruber,是的,在我注意到它是从 2015 年开始之前,我几乎回答完了。我有点生气,但我已经承诺了,所以我还是决定发帖。不确定来自 OP 以外的其他人的赏金是如何工作的。我预计get_orig_name 只会在管道末端使用,因此进一步的管道不应该成为问题。如果您想在获取数据框名称后继续处理数据,也很容易执行… %&gt;% get_orig_name %&gt;% `$`(output) %&gt;% … 之类的操作。我想我会保持现状,直到这篇文章得到更多关注。
  • 至少我在工作的过程中学到了一些东西。 +1 用于解释函数的工作原理,原始答案中没有。
【解决方案2】:

这是一种很老套的方法,我敢肯定它会在很多极端情况下被打破:

library(data.table) # for the address function
                    # or parse .Internal(inspect if you feel masochistic

fn = function(tbl) {
  objs = ls(parent.env(environment()))
  objs[sapply(objs,
          function(x) address(get(x, env = parent.env(environment()))) == address(tbl))]
}

df = data.frame(a = 1:10)
df %>% fn
#[1] "df"

【讨论】:

    【解决方案3】:

    虽然这个问题是一个老问题,并且已经获得了赏金,但我想扩展 gersht 的出色答案,它非常适合获取最左侧的对象名称。但是,除了在管道的最后一步使用这种方法之外,还没有解决在 dplyr 工作流程中集成此功能的问题。

    由于我经常使用 dplyr,我围绕常见的 dplyr 动词创建了一组自定义包装函数,我称之为 metadplyr(我仍在使用该功能,这就是为什么我没有上传到 github 上)。

    本质上,这些函数在 tibble 之上创建了一个名为 meta_tbl 的新类,并在该对象的属性中写入了某些内容。应用于 OP 的问题,我提供了一个简单的示例 filter,但该过程也适用于任何其他 dplyr 动词。

    在我的原始函数系列中,我使用的名称与 dplyr 略有不同,但该方法在“覆盖”原始 dplyr 动词时也有效。

    下面是一个新的过滤器函数,它将数据框或 tibble 转换为 meta_tbl,并将 lhs 对象的原始名称写入属性.name。在这里,我使用的是 gersht 方法的简短版本。

    library(dplyr)
    
     filter <- function(.data, ...) {
    
        if(!("meta_tbl" %in% class(.data))) {
    
          .data2 <- as_tibble(.data)
    
          # add new class 'meta_tbl' to data.frame  
          attr(.data2, "class") <- c(attr(.data2, "class"), "meta_tbl")
    
          # write lhs original name into attributes
          i <- 1
          while(!("chain_parts" %in% ls(envir=parent.frame(i)))) {
            i <- i+1
          }
          attr(.data2, ".name") <- deparse(parent.frame(i)$lhs)
    
        }
    
        dplyr::filter(.data2, ...)
    
    }
    

    为方便起见,最好有一些辅助函数来轻松地从属性中提取原始名称。

    .name <- function(.data) {
      if("meta_tbl" %in% class(.data)) {
      attr(.data, ".name")
      } else stop("this function only work on objects of class 'meta_tbl'")
    
    }
    

    这两个函数都可以通过以下方式在工作流中使用:

    mtcars %>% 
      filter(gear == 4) %>% 
      write.csv(paste0(.name(.), ".csv"))
    

    这可能是一个不好的例子,因为管道不会继续,但理论上,我们可以使用包含原始名称的管道并将其用于进一步的函数调用。

    【讨论】:

      【解决方案4】:

      灵感来自 gersht 提到的link

      你可以倒退5代得到名字

      df %>% {parent.frame(5)$lhs}
      

      示例如下:

      library(dplyr)
      
      a <- 1
      
      df1 <- data.frame(a = 1:10)
      
      df2 <- data.frame(a = 1:10)
      
      a %>% {parent.frame(5)$lhs}
      
      df1 %>% {parent.frame(5)$lhs}
      
      df2 %>% {parent.frame(5)$lhs}
      

      【讨论】:

        【解决方案5】:

        如果不向您的my_function 添加额外的参数,我认为这是不可能的。当使用dplyr 链接函数时,它会自动将df 转换为tbl_df 对象,因此dplyr 范围内的新名称"." 使管道更简单。

        以下是 dplyr 的一种非常 hacky 的方式,它只是添加了一个附加参数来返回原始 data.frame 的名称

        my_function <- function(tbl, orig.df){print(deparse(substitute(orig.df)))}
        df %>% my_function(df)
        [1] "df"
        

        请注意,您不能只将 df 与原始函数一起传递,因为 tbl_df 对象会自动传递给所有后续函数。

        【讨论】:

        • 我认为这比my_fn=function(tbl, origin.df) print(origin.df); df %&gt;% my_fn("df") 更糟糕,这显然毫无意义。
        • @eddi 我完全同意,我只想提供 OP 要求的内容,而不使用额外的包。正如你所说,整件事毫无意义。
        猜你喜欢
        • 1970-01-01
        • 2018-06-17
        • 1970-01-01
        • 2018-03-29
        • 2022-11-26
        • 2016-04-22
        • 2017-01-08
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多