在矩阵中使用 vctrs

Using vctrs in matrices

我正在试验 vctrs 包。我的实际用例在相关方面类似于 vctrs 主页上有用的 S3 vectors 文章中实施的 rational class,因为它使用 rcrd对于配对数据。为了清楚起见,我将使用它作为我的代表。 (编辑:但是,我对有理数并不特别感兴趣。)让我先粘贴相关部分:

library(vctrs)
library(zeallot)

new_rational <- function(n = integer(), d = integer()) {
  vec_assert(n, ptype = integer())
  vec_assert(d, ptype = integer())

  new_rcrd(list(n = n, d = d), class = "vctrs_rational")
}

rational <- function(n, d) {
  c(n, d) %<-% vec_cast_common(n, d, .to = integer())
  c(n, d) %<-% vec_recycle_common(n, d)

  new_rational(n, d)
}

format.vctrs_rational <- function(x, ...) {
  n <- field(x, "n")
  d <- field(x, "d")

  out <- paste0(n, "/", d)
  out[is.na(n) | is.na(d)] <- NA

  out
}

vec_ptype_abbr.vctrs_rational <- function(x, ...) "rtnl"
vec_ptype_full.vctrs_rational <- function(x, ...) "rational"

一个使用这个的例子:

(x <- rational(1, 1:15))
#> <rational[15]>
#>  [1] 1/1  1/2  1/3  1/4  1/5  1/6  1/7  1/8  1/9  1/10 1/11 1/12 1/13 1/14 1/15

我的问题出现在尝试在 matrix 中使用这样的 class 时:

matrix(x, ncol = 5, nrow = 3)
#> Warning in matrix(x, ncol = 5, nrow = 3): data length [2] is not a sub-multiple
#> or multiple of the number of rows [3]
#>      [,1]       [,2]       [,3]       [,4]       [,5]      
#> [1,] Integer,15 Integer,15 Integer,15 Integer,15 Integer,15
#> [2,] Integer,15 Integer,15 Integer,15 Integer,15 Integer,15
#> [3,] Integer,15 Integer,15 Integer,15 Integer,15 Integer,15

reprex package (v0.3.0)

于 2020 年 6 月 5 日创建

我希望得到一个 3×5 矩阵,每个单元格包含一个来自 x 的值,如果 x 是一个 "normal" 向量就会发生这种情况。相反,我得到一个 3×5 列表矩阵,其中 vctrs 尝试使交替行分别包含 nd 值。

因此,我的问题是 是否有可能让 vctrs 在这种情况下以 "expected" 的方式处理矩阵,如果可以,怎么做? 通过试验,我觉得这可能与实施 dim.rational`dim<-.rational` 有关,但我无法让它发挥作用。

编辑:如果所需的矩阵不清楚(如评论中所建议的),我想要一个有点类似于以下内容的矩阵对象,我已经编辑了手:

(m <- matrix(x, ncol = 5, nrow = 3))
#> <rational[15]>
#>      [,1] [,2] [,3] [,4]  [,5]   
#> [1,] 1/1  1/4  1/7  1/10  1/13
#> [2,] 1/2  1/5  1/8  1/11  1/14
#> [3,] 1/3  1/6  1/9  1/12  1/15

这样正常的矩阵运算就可以在 m 上运行,例如

m[1,]
#> <rational[5]>
#> 1/1  1/4  1/7  1/10  1/13

rational class 的整个设计似乎建立在保持其类型安全和对用户隐藏实现的基础上,我认为这是使其始终如一地工作所必需的,但是意味着您不能指望它能很好地与 R 的默认 S3 方法配合使用。

vctrs 的帮助文件具体说

  • dims(), dims<-, dimnames(), dimnames<-, levels(), and levels<- methods throw errors.

这表明 vctrs 的作者认为它不是构建矩阵方法的良好基础。

在任何情况下,我都不会急于尝试将其放入矩阵中,因为一旦它存在就无法对其进行任何操作:您没有可用的算术方法:

x + 2
#> Error: <rational> + <double> is not permitted
#> Run `rlang::last_error()` to see where the error occurred.
x * 2
#> Error: <rational> * <double> is not permitted
#> Run `rlang::last_error()` to see where the error occurred.
x + x
#> Error: <rational> + <rational> is not permitted
#> Run `rlang::last_error()` to see where the error occurred.

所以你需要先定义算术方法。在你这样做之前,你需要 $ 分子和分母的访问器,一个 is.rational 函数来在尝试算术之前检查类型,一个函数来找到最大公分母,以及一个函数来简化你的基于它的理性。

`$.vctrs_rational` <- function(vec, symb) unclass(vec)[[as.character(symb)]]

is.rational <- function(num) class(num)[1] == "vctrs_rational"

gcd <- function(x, y) ifelse(x %% y, gcd(y, x %% y), y)

simplify <- function(num) {
  common <- gcd(num$n, num$d)
  rational(num$n / common, num$d/common)
}

所以现在你可以做

x$n
#>  [1] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
x$d
#>  [1]  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15
is.rational(x)
#> [1] TRUE

现在写算术函数。例如,这里是涵盖数字和有理数类型的基本算术的实现:

Ops.vctrs_rational <- function(vec, num)
{
  if(!is.rational(vec)) {tmp <- vec; vec <- num; num <- tmp; }
  if(.Generic == '*'){
    if(is.rational(num)) return(simplify(rational(vec$n * num$n, vec$d * num$d)))
    else return(simplify(rational(vec$n * 2, vec$d)))
  }

  else if (.Generic == '/'){
    if(is.rational(num)) return(vec * rational(num$d, num$n))
    else return(vec * rational(1, num))
  }

  else if (.Generic == '+'){
    if(is.rational(num)){ 
      new_n <- vec$n * (vec$d * num$d)/vec$d + num$n * (vec$d * num$d)/num$d
      return(simplify(rational(new_n, vec$d * num$d)))
    }
    else return(simplify(rational(num * vec$d + vec$n, vec$d)))
  }

  else if (.Generic == '-'){
    if(is.rational(num)) return(vec + rational(-num$n, num$d)) 
    else return(vec + (-num)) 
  }

  else if (.Generic == '^'){
    if(is.rational(num) | num < 0) stop("fractional and negative powers not supported")
    return(simplify(rational(vec$n ^ num, vec$d ^ num)))
  }
}

现在您可以这样做,例如:

x * 3
#> <rational[15]>
#>  [1] 3/1  3/2  1/1  3/4  3/5  1/2  3/7  3/8  1/3  3/10 3/11 1/4  3/13 3/14 1/5

x + x
#> <rational[15]>
#> [1] 2/1  1/1  2/3  1/2  2/5  1/3  2/7  1/4  2/9  1/5  2/11 1/6  2/13 1/7  2/15

(2 + x)^2 / (3 * x + 1)
#> <rational[15]>
#>  [1] 3/1     25/8    49/15   27/8    121/35  169/48  25/7    289/80 
#>  [9] 361/99  147/40  529/143 625/168 243/65  841/224 961/255

尝试直接使用 matrix() 本身可能行不通,因为 matrix 通过转换为基本向量然后调用 C 代码来工作。这会删除 class 信息。

这意味着您需要定义一个单独的 rational_matrix class,这反过来又会受益于支持 rational_vector class。然后我们可以定义具体的格式和打印方式:

as.vector.vctrs_rational <- function(x, ...) {
  n <- x$n/x$d
  attr(n, "denom") <- x$d
  attr(n, "numerator") <- x$n
  class(n) <- "rational_attr"
  n
}

rational_matrix <- function(data, nrow = 1, ncol = 1, 
                            byrow = FALSE, dimnames = NULL){
  d <- as.vector(data)
  m <- .Internal(matrix(d, nrow, ncol, byrow, dimnames, missing(nrow), 
                        missing(ncol)))
  m_dim <- dim(m)
  attributes(m) <- attributes(d)
  dim(m) <- rev(m_dim)
  class(m) <- c("rational_matrix", "matrix")
  m
}

format.rational_matrix <- function(x) {
 return(paste0(attr(x, "numerator"), "/", attr(x, "denom")))
}

print.rational_matrix <- function(x)
{
  print(matrix(format(x), nrow = dim(x)[2]), quote = FALSE)
}

最后,您需要覆盖 matrix() 以使其成为 S3 方法,确保您首先将函数复制为 matrix.default

matrix.default <- matrix
matrix <- function(data = NA, ...) UseMethod("matrix")
matrix.vctrs_rational <- function(data, ...) rational_matrix(data, ...)

现在你可以做:

matrix(x, nrow = 5)
#>      [,1] [,2] [,3] [,4] [,5]
#> [1,] 1/1  1/4  1/7  1/10 1/13
#> [2,] 1/2  1/5  1/8  1/11 1/14
#> [3,] 1/3  1/6  1/9  1/12 1/15

rational_matrix(x + 5, nrow = 3)
#>      [,1] [,2] [,3] [,4]  [,5] 
#> [1,] 6/1  21/4 36/7 51/10 66/13
#> [2,] 11/2 26/5 41/8 56/11 71/14
#> [3,] 16/3 31/6 46/9 61/12 76/15

rational_matrix(x + x, nrow = 5)
#>      [,1] [,2] [,3]
#> [1,] 2/1  1/3  2/11
#> [2,] 1/1  2/7  1/6 
#> [3,] 2/3  1/4  2/13
#> [4,] 1/2  2/9  1/7 
#> [5,] 2/5  1/5  2/15

然而,为了让它工作,我们不得不添加额外的 classes 属性,所以我的感觉是,如果你想要一个与矩阵等一起工作的有理 class,你应该在本机 S3 或 R 中可用的其他面向对象方法之一中执行此操作,而不是使用 vctrs 包。

同样值得一提的是,上面的 class 远未准备好生产,因为您需要添加测试相等/不等的方法、描述矩阵运算的方法、转换为的能力小数点、绘图方法等