将 Tibble 转换为参数列表

Convert Tibble to Parameter List

我正在尝试将 Tibble 转换为函数调用的参数列表。我这样做的原因是因为我想创建一个简单的文件规范 Tibble 来读取多个具有不同列的固定宽度文件。这样我只需要使用 pull 和 select 指定文件中的列,然后我就可以自动加载和解析文件。但是,我 运行 遇到了使用 cols 对象指定列格式的问题。

对于这个例子,假设我有一个格式的 Tibble:

> (filespec <- tibble(ID = c("Title", "Date", "ATTR"), Length = c(23, 8, 6), Type = c("col_character()", "col_date()", "col_factor(levels=c(123456,654321)")))
# A tibble: 3 x 3
     ID Length                               Type
  <chr>  <dbl>                              <chr>
1 Title     23                    col_character()
2  Date      8                         col_date()
3  ATTR      6 col_factor(levels=c(123456,654321)

我想以以下格式的 cols 对象结束:

> (cols(Title = col_character(), Date = col_date(), ATTR=col_factor(levels=c(123456,654321))))
cols(
  Title = col_character(),
  Date = col_date(format = ""),
  ATTR = col_factor(levels = c(123456, 654321), ordered = FALSE)
)

根据我读过的其他问题,我知道这可以用 do.call 来完成。但我无法弄清楚如何以自动方式将列 ID 和 Type 转换为 cols 对象。这是我尝试过的示例...

> do.call(cols, select(filespec,ID, Type))
Error in switch(x, `_` = , `-` = col_skip(), `?` = col_guess(), c = col_character(),  : 
  EXPR must be a length 1 vector

我假设 select 需要用另一个执行行到参数映射的函数包装,这是怎么做到的?

我可能会以不同的方式处理这个问题,并将文件规范存储在一个简单的列表中:

library(purrr)
library(readr)
filespec <- list(Title = list(Length = 23,
                              Type = col_character()),
                 Date = list(Length = 8,
                             Type = col_date()),
                 ATTR = list(Length = 6,
                             Type = col_factor(levels = 123456,654321)))

a <- at_depth(.x = filespec,.depth = 1,.f = "Type")
> invoke(.f = cols,.x = a)

cols(
  Title = col_character(),
  Date = col_date(format = ""),
  ATTR = col_factor(levels = 123456, ordered = 654321, include_na = FALSE)
)

或者,

> invoke(.f = cols,.x = a[c('Title','ATTR')])
cols(
  Title = col_character(),
  ATTR = col_factor(levels = 123456, ordered = 654321, include_na = FALSE)
)

tl;dr: There are many things that make this more complex than it seems. But it’s feasible, and the resulting code (provided at the end) isn’t complicated, once the individual parts are understood.

正如评论中所讨论的,我从根本上更喜欢 Joran 的方法。事实上,每当您发现自己将代码表达式存储在字符串中时,就应该敲响警钟:这是一个 anti-pattern,也称为 stringly typed code (a riff on, and quite the opposite of, strongly typed code)。不幸的是,R 充满了字符串类型的代码。

也就是说,你的use-case(file-based配置)本身就是一个好主意。我会考虑以不同于 R 代码片段的格式存储信息。但是,好吧,它确实有效。那么让我们来探讨一下为什么 您的代码 不起作用。

第一个问题是:您将小标题传递给 do.call。 Tibbles 是列的列表,因此 do.call 允许这样做。但是,在内部,您的呼叫被转换为等同于:

cols(
    ID = c("Title", "Date", "ATTR"),
    Type = c("col_character()", "col_date()", "col_factor(levels=c(123456,654321))")
)

—但这根本不是我们想要的代码!

我们需要在这里解决两件事:

  1. 我们需要使用 Type 列作为参数 valuesID 列作为参数 姓名。我们可以通过创建一个以 ID 作为名称和 Type 作为值的新列表来做到这一点:setNames(Type, ID).

  2. cols 不知道如何处理字符串参数。它需要列规范 — objects 类型 Collector

    换句话说,写"col_date()"col_date()差别很大。

为了解决这个问题,我们需要做一些相当复杂的事情:我们需要将 Type 列解析为 R 代码,并且我们需要评估生成的解析表达式。 R 提供了两个方便的函数(分别为 parseeval)来完成此操作。但是不要被两个简单函数的存在所蒙蔽:这是一个极其复杂的操作。 R 本质上需要 运行 对您的代码片段进行完整的解析器和解释器。如果代码不是您所期望的,它会变得更加毛茸茸。例如,文本可能包含代码 unlink('/', recursive = TRUE) 而不是 col_date()。然后 R 会愉快地擦除你的硬盘。

这只是 parse/eval 复杂且通常被避免的原因之一。其他原因包括:如果代码中存在解析错误会发生什么(事实上,您的代码确实包含缺少右括号……)?

但是我们开始吧。现在我们把所有的部分都放在一起了,我们可以相对容易地加入它们:

filespec %>%
    mutate(Parsed = lapply(Type, function (x) parse(text = x, encoding = 'UTF-8'))) %>%
    mutate(ColSpec = lapply(Parsed, eval)) %>%
    with(setNames(ColSpec, ID)) %>%
    do.call(cols, .)

一段一段地执行这段代码,看看它做了什么,并说服自己它工作正常。