将Tibble转换为参数列表

4
我正在尝试将一个 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语句包装在另一个函数中,以执行行到参数的映射,这应该如何完成?

1
可能可以使用do.call来完成这个任务,但是你的代码目前根本没有实现你想要的功能——在使用它之前,你需要先了解do.call的实际作用。 - Konrad Rudolph
我对R是新手,所以这都是学习经验。我认为我理解了do.call的作用,它使用其他参数作为参数调用函数。根据我在下面答案的评论,我认为我不明白的是如何自动创建一个命名列表。我不想手动输入所有字段=类型参数,我将它们放在两列中,我只想让R为我创建命名列表。 - RandomString
是的,你在问题描述上实际上是非常准确的。从你的问题中看不出来你理解这一点。但是使用 setNames 可以很容易地解决问题的这一部分。另一个更大的问题是你的参数是字符串,而不是代码。因此,你需要先评估它们,虽然这是可能的(通过 parse/eval),但是这样做会很混乱,也可能不是一个好主意(嗯;在你的情况下可能是)。Joran 的方法更优秀。 - Konrad Rudolph
2个回答

1

我可能会有点不同的方法,将文件规格存储在一个简单的列表中:

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

我喜欢这个解决方案,它很有效!我使用 tibble 的主要原因是最终可能会有 50-60 列,并在源文件中维护该列表可能会很麻烦,所以我希望通过 csv 读取来完成它。是否有一种简单的方法可以将我需要的两列从 tibble 中提取出来并将它们转换为列表?我对 R 还很陌生,我认为自动创建一个命名列表的方法正在逃避我。 - RandomString

1

tl;dr: 有许多因素使得这个问题比看起来更加复杂。但是,只要理解了各个部分,它是可行的,并且生成的代码(在结尾处提供)并不复杂。

正如评论中所讨论的那样,我基本上更喜欢Joran的方法。实际上,每当你发现自己在字符字符串中存储代码表达式时,这应该引起警报:这是一种反模式,称为stringly typed code(对strongly typed code的一个玩笑,而且完全相反)。不幸的是,R充满了stringly typed code。

话虽如此,您的用例(基于文件的配置)本身是一个好主意。我会考虑将信息存储在与R代码片段不同的格式中。但是,嗯,它确实有效。因此,让我们探讨为什么您的代码不起作用。

第一个问题是:当你将一个tibble传递给do.call时,它是由列组成的列表,因此do.call允许这样做。然而,在内部,你的调用被转换为等效于以下内容的形式:
cols(
    ID = c("Title", "Date", "ATTR"),
    Type = c("col_character()", "col_date()", "col_factor(levels=c(123456,654321))")
)

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

我们需要解决两个问题:

  1. 我们需要使用 Type 列作为参数 ,并将 ID 列作为参数 名称。我们可以通过创建一个新列表,将 ID 作为名称和 Type 作为值来实现: setNames(Type, ID)

  2. cols 不知道如何处理字符字符串参数。它需要列规范 - 类型为 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, .)

逐行执行这段代码,观察其功能并确保它能正确运行。


1
setNames / with 部分正是我所需要的。我知道 eval 问题,但很可能会在以后通过简单的类型字符串映射 -> S3 对象来解决它。 - RandomString

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接