如何创建一个在 data.frame 中运行良好的新类型?

pie*_*ito 4 types r dataframe r6 tibble

我想有几种方法可以做到这一点。因此,这个问题的答案即使不是主观的,也可能是主观的。因此,我将尝试缩小问题范围,并向您提供我已经完成的操作的详细信息。

\n\n

语境

\n\n

我正在使用该R6包,并且创建了一个IntervalNumeric\nR6Class,它有两个字段lower_boundupper_bound

\n\n
require(R6)\nNumericInterval <-\n  R6Class(\n        "NumericInterval",\n        public = list(\n          lower_bound = NA,\n          upper_bound = NA,\n          initialize = function(low, up) {\n            self$lower_bound <- low\n            self$upper_bound <- up\n          },\n          as_character = function() {\n            paste0("[", self$lower_bound, ", ",\n                        self$upper_bound, "]")}))\n
Run Code Online (Sandbox Code Playgroud)\n\n

我还使用S3通用方法系统来获取\n类型的as.characterand :printNumericInterval

\n\n
as.character.NumericInterval <- function(x, ...) {\n  x$as_character()}\nprint.NumericInterval <- function(x, ...) {\n  x$as_character()}\n
Run Code Online (Sandbox Code Playgroud)\n\n

现在我可以这样做(与 相同print):

\n\n
> as.character(NumericInterval$new(0, pi))\n\n[1] "[0, 3.14159265358979]"\n
Run Code Online (Sandbox Code Playgroud)\n\n
\n\n

问题:

\n\n

现在需要做什么才能使用这个新类型作为data.frame列类型?

\n\n

例如我希望能够做到这一点:

\n\n
(df <- data.frame(\n   X = c("I1", "I2", "I3"),\n   Y = c(NumericInterval$new(0,1),\n         NumericInterval$new(1,2),\n         NumericInterval$new(2,3)))\n
Run Code Online (Sandbox Code Playgroud)\n\n

并得到:

\n\n
   X      Y\n1 I1 [0, 1]\n2 I2 [1, 2]\n3 I3 [2, 3]\n
Run Code Online (Sandbox Code Playgroud)\n\n

但我得到:

\n\n
Error in as.data.frame.default(x[[i]], optional = TRUE) :\n  cannot coerce class \xe2\x80\x98c("NumericInterval", "R6")\xe2\x80\x99 to a data.frame\n
Run Code Online (Sandbox Code Playgroud)\n\n

当然,我还希望能够访问对象并执行以下操作:

\n\n
df[2, 2]$lower_bound <- 0\n
Run Code Online (Sandbox Code Playgroud)\n\n
\n\n

tibbles似乎是一个解决方案

\n\n
(df <- tibble(\nX = c("I1", "I2", "I3"),\nY = c(NumericInterval$new(0,1),\nNumericInterval$new(1,2),\nNumericInterval$new(2,3))))\n
Run Code Online (Sandbox Code Playgroud)\n\n

产生:

\n\n
# A tibble: 3 x 2\n  X     Y\n  <chr> <list>\n1 I1    <NmrcIntr>\n2 I2    <NmrcIntr>\n3 I3    <NmrcIntr>\n
Run Code Online (Sandbox Code Playgroud)\n\n

每个都NumericInterval按预期放置,例如:

\n\n
> require(dplyr)\n> df[2,1][[1]] %>% pull\n\n\n[[1]]\n<NumericInterval>\n  Public:\n    as_character: function ()\n    clone: function (deep = FALSE)\n    initialize: function (low, up)\n    lower_bound: 0\n    upper_bound: 1\n
Run Code Online (Sandbox Code Playgroud)\n\n

但 tibble 的输出和访问对象的方式并不是我所期望的。

\n

All*_*ron 5

在将 R6 对象强制到数据帧中之前,您需要做出一些设计决策。也许最重要的是您希望矢量化发生在什么级别。

在您的示例中,您将“原子”NumericInterval放入向量中,这当然有一些优点,但主要缺点是,当您尝试在集合上使用基本 R 向量函数时NumericInterval,R 会将对象视为环境(这就是 R6 对象)。这意味着您不会获得您正在寻找的行为,因为您希望 R 处理这些环境向量的方式与通常处理环境向量的方式不同。换句话说,要处理这种工作方式,您需要定义另一个类,其中包含管理向量操作的方法。这是可能的,但看起来复杂、混乱且效率低下。

在我看来,最好将向量化保留在单个 R6 对象内 - 也就是说,在单个 R6 对象内包含lower_bounds和 的向量。upper_boundsR6 类可以处理打印、格式化和子集设置,并且可以充当数据帧本身中的整个列。

为此,您首先需要定义一些泛型函数的 R6 特化:

`[.R6` <- function(x, ...) x$`[`(...) 
`[<-.R6` <- function(x, ...) x$`[<-`(...)
length.R6 <- function(x) x$length()
format.R6 <- function(x) x$format()
as.data.frame.R6 <- function(x, ...) x$as.data.frame()
Run Code Online (Sandbox Code Playgroud)

将它们作为.R6而不是NumericInterval允许您在多个不同的类中使用它们。

现在我们可以用我们需要的专业来定义我们的类:

NumericInterval <- R6Class("NumericInterval",
        public = list(
          lower_bound = NA,
          upper_bound = NA,
          initialize = function(low, up) {
            self$lower_bound <- low
            self$upper_bound <- up
          },
          `[` = function(n){
            return(NumericInterval$new(self$lower_bound[n], self$upper_bound[n]))
          },
          `[<-` = function(n, m){
            self$lower_bound[n] <- m[1]
            self$upper_bound[n] <- m[2]
            invisible(self)
          },
          length = function() length(self$lower_bound), 
          as.data.frame = function(...) {
            structure(
              list(interval = structure(a)), 
              class = "data.frame", 
              row.names = seq_along(self$lower_bound))
          },
          as_character = function() {
            paste0("[", self$lower_bound, ", ",
                        self$upper_bound, "]")},
          format = function(...) self$as_character(),
          print = function() {
            print(self$as_character(), quote = FALSE)
            invisible(self)}))
Run Code Online (Sandbox Code Playgroud)

这会产生以下行为:

a <- NumericInterval$new(1:3, 4:6)
a
#> [1] [1, 4] [2, 5] [3, 6]

as.data.frame(a)
#>   interval
#> 1   [1, 4]
#> 2   [2, 5]
#> 3   [3, 6]

df <- data.frame(id = LETTERS[1:3], interval = a)
df
#>   id interval
#> 1  A   [1, 4]
#> 2  B   [2, 5]
#> 3  C   [3, 6]

df[1,]
#>   id interval
#> 1  A   [1, 4]

df$interval[1]$lower_bound
#> [1] 1
Run Code Online (Sandbox Code Playgroud)

这当然不是生产级代码。特别是,您需要包括错误处理以确保上限和下限具有相同的长度,并且都是数字。