我们能否在R中获取因子矩阵?

18

似乎在R中无法获取因子的矩阵。这是真的吗?如果是,为什么?如果不是,我该怎么做?

f <- factor(sample(letters[1:5], 20, rep=TRUE), letters[1:5])
m <- matrix(f,4,5)
is.factor(m) # fail.

m <- factor(m,letters[1:5])
is.factor(m) # oh, yes? 
is.matrix(m) # nope. fail. 

dim(f) <- c(4,5) # aha?
is.factor(f) # yes.. 
is.matrix(f) # yes!

# but then I get a strange behavior
cbind(f,f) # is not a factor anymore
head(f,2) # doesn't give the first 2 rows but the first 2 elements of f
# should I worry about it?

1
为什么需要因子矩阵?你有没有考虑过使用data.frame(支持因子更好)而不是矩阵? - nicola
2
@nicola 我确实考虑过,但是一个 data.frame 对于我所需的东西来说太多了。需要一个由可比较、同质元素组成的矩阵不是很自然吗? - iago-lito
2个回答

27
在这种情况下,它可能像鸭子一样行走,甚至像鸭子一样嘎嘎叫,但是f来自:
f <- factor(sample(letters[1:5], 20, rep=TRUE), letters[1:5])
dim(f) <- c(4,5)

虽然is.matrix()声称f是矩阵,但实际上它并不是矩阵。在is.matrix()的考虑中,只要f是向量并且具有dim属性,它就被视为矩阵。通过将属性添加到f,您可以通过测试。但是,如您所见,一旦您开始将f用作矩阵,它将很快失去使它成为因子的特征(您最终会使用级别或尺寸丢失)。

对于原子向量类型,实际上只有矩阵和数组:

  1. 逻辑型,
  2. 整数型,
  3. 实数,
  4. 复数,
  5. 字符串(或字符),
  6. 原始的

还有,正如@hadley提醒我的那样,您也可以有列表矩阵和数组(通过在列表对象上设置dim属性)。例如,请参见Hadley的书籍Advanced RMatrices & Arrays部分。

除了这些类型之外的任何内容都将通过as.vector()强制转换为某种较低类型。在matrix(f, nrow = 3)中发生这种情况,不是因为f按照is.atomic()的定义是原子的(它返回TRUE,因为它在内部存储为整数,并且typeof(f)返回"integer"),而是因为它具有一个class属性。这将设置f内部表示的OBJECT位,并且任何具有类的东西都应该通过as.vector()被强制转换为原子类型之一:

matrix <- function(data = NA, nrow = 1, ncol = 1, byrow = FALSE,
                   dimnames = NULL) {
    if (is.object(data) || !is.atomic(data)) 
        data <- as.vector(data)
....

使用 dim<-() 添加维度是一种快速创建数组而不会复制对象的方法,但这会绕过 R 通过其他方法将 f 强制转换为矩阵时所做的某些检查和平衡。
matrix(f, nrow = 3) # or
as.matrix(f)

当您尝试使用在矩阵上工作的基本函数或使用方法分派时,会发现这一点。请注意,在将维度分配给f之后,f仍然是"factor"类:

> class(f)
[1] "factor"

这个问题可以通过head()函数来解释;你没有得到head.matrix的结果是因为在S3机制中,f不被视为矩阵:

> debug(head.matrix)
> head(f) # we don't enter the debugger
[1] d c a d b d
Levels: a b c d e
> undebug(head.matrix)

head.default 方法调用了 [,这个方法有一个名为 factor 的方法,因此出现了观察到的行为:

> debugonce(`[.factor`)
> head(f)
debugging in: `[.factor`(x, seq_len(n))
debug: {
    y <- NextMethod("[")
    attr(y, "contrasts") <- attr(x, "contrasts")
    attr(y, "levels") <- attr(x, "levels")
    class(y) <- oldClass(x)
    lev <- levels(x)
    if (drop) 
        factor(y, exclude = if (anyNA(levels(x))) 
            NULL
        else NA)
    else y
}
....
cbind()的行为可以从文档中解释(来自?cbind,强调我的部分):
引用:

cbindrbind函数是S3通用的,...

....

在默认方法中,所有向量/矩阵必须是原子性的(参见vector),或者是列表。不允许使用表达式。语言对象(例如公式和调用)和pairlist将被强制转换为列表:其他对象(例如名称和外部指针)将包含在列表结果的元素中。输入可能具有的任何类都将被丢弃(特别是因子将被替换为其内部代码)。

再次说明,f"factor"类会使您失败,因为默认的cbind方法将被调用,并且它将剥离级别信息并返回内部整数代码,就像您观察到的那样。
在许多方面,您必须忽略或至少不完全信任is.foo函数告诉您的内容,因为它们只是使用简单的测试来判断某个东西是否是foo对象。当涉及到f(带有维度)时,is.matrix()is.atomic()在某种程度上显然是错误的。从特定的角度来看,它们也是正确的。从实现的角度来看,它们的行为可以被理解;我认为is.atomic(f)不正确,但是如果R Core通过"if is of an atomic type"来指代"类型"是由typeof(f)返回的东西,那么is.atomic()就是正确的。一个更严格的测试是is.vector(),而f则失败了:
> is.vector(f)
[1] FALSE

因为它具有超出names属性的属性:

> attributes(f)
$levels
[1] "a" "b" "c" "d" "e"

$class
[1] "factor"

$dim
[1] 4 5

关于如何获得因子矩阵,至少如果你想保留因子信息(水平的标签),那么你是无法获得它的。一个解决方案是使用字符矩阵,它将保留标签:
> fl <- levels(f)
> fm <- matrix(f, ncol = 5)
> fm
     [,1] [,2] [,3] [,4] [,5]
[1,] "c"  "a"  "a"  "c"  "b" 
[2,] "d"  "b"  "d"  "b"  "a" 
[3,] "e"  "e"  "e"  "c"  "e" 
[4,] "a"  "b"  "b"  "a"  "e"

我们存储f的级别,以备将来使用,以防我们在途中丢失矩阵的某些元素。

或者使用内部整数表示:

> (fm2 <- matrix(unclass(f), ncol = 5))
     [,1] [,2] [,3] [,4] [,5]
[1,]    3    1    1    3    2
[2,]    4    2    4    2    1
[3,]    5    5    5    3    5
[4,]    1    2    2    1    5

你可以通过以下方式随时返回到级别/标签:

> fm2[] <- fl[fm2]
> fm2
     [,1] [,2] [,3] [,4] [,5]
[1,] "c"  "a"  "a"  "c"  "b" 
[2,] "d"  "b"  "d"  "b"  "a" 
[3,] "e"  "e"  "e"  "c"  "e" 
[4,] "a"  "b"  "b"  "a"  "e"

使用数据框似乎不是很理想,因为数据框的每个组件都会被视为单独的因子,而您似乎希望将数组作为一个具有一组水平的单个因子进行处理。
如果您真的想做您想要的事情,即拥有一个因子矩阵,您很可能需要创建自己的S3类来实现这一点,并编写所有相关方法。例如,您可以将因子矩阵存储为字符矩阵,但使用类"factorMatrix",并将水平存储为额外的属性。然后,您需要编写[.factorMatrix,它将获取水平,然后在矩阵上使用默认的[方法,最后再次添加水平属性。您还可以编写cbindhead方法。所需方法的列表将很快增长,但简单的实现可能足够,如果您使对象具有类c("factorMatrix", "matrix")(即继承自"matrix"类),则会获取"matrix"类的所有属性/方法(这将删除水平和其他属性),因此您至少可以使用对象并查看您需要添加新方法以填充类行为的位置。

虽然你提到的属性丢弃函数(rbind等)仍然是有道理的。 - BrodieG
@BrodieG 我想补充一下,关于 f-矩阵是否是矩阵,这要看你对“是矩阵”有什么理解。从 is.matrix 的角度来看,它显然是矩阵。但从 S3 类系统的角度来看,它不是(它既不属于 "matrix" 类,也没有继承自它)。从文档定义矩阵的角度来看,它也不是矩阵,因为 f 不是原子向量。最后一点可能很重要;你可能会得到一个看起来像矩阵的对象,但这很快就会被纠正。是的,如果 R 核心团队愿意,R 可以得到改进。 - Gavin Simpson
1
列表也可以是矩阵。 - hadley
1
我认为使用字符矩阵会更好,因为这样就不会意外将值当作数字处理。使用字符串而不是整数几乎没有额外的内存开销,这是由于全局字符串池的存在。 - hadley
1
@BrodieG 如果要有因子矩阵这样的东西,S3需要支持多重继承,但它并不支持。(并不是说在S3系统中“矩阵”真的很好定义,它大多是一个隐式类) - hadley
显示剩余8条评论

7

不幸的是,R中并非所有功能都完全支持因子(factor),因此许多R函数默认将因子视为其内部存储类型integer:

> typeof(factor(letters[1:3]))
[1] "integer  

这是关于 matrixcbind 的情况。它们不知道如何处理因子,但它们知道如何处理整数,所以会将您的因子视为整数进行处理。相反,head 知道如何处理因子,但它从不检查您的因子是否也是矩阵,因此只将其视为普通的无维度因子向量。
如果您想将矩阵操作视为对因子的操作,则最好将其强制转换为字符形式。完成操作后,可以将其恢复为因子形式。您也可以使用整数形式进行此操作,但这样可能会出现奇怪的问题(例如,您可以在整数矩阵上执行矩阵乘法,但这对于因子来说没有意义)。
请注意,如果将类“matrix”添加到您的因子中,则某些(但不是全部)操作将开始起作用。
f <- factor(letters[1:9])
dim(f) <- c(3, 3)
class(f) <- c("factor", "matrix")
head(f, 2)

输出:

     [,1] [,2] [,3]
[1,] a    d    g   
[2,] b    e    h   
Levels: a b c d e f g h i

这并不修复rbind等问题。


那我们来用整数矩阵吧,因为我感觉只需要5或10个级别时,使用字符会很“沉重”..并且尽量不要对我的矩阵进行虚拟操作 ;) - iago-lito
根据Hadley的评论,@lago-lito,字符实际上并不占用太多空间,因为实际的字符字符串只在内存中存储一次。另外,请注意我的答案更新。 - BrodieG
哦,真的吗?我应该在哪里进一步了解矩阵在内存中的存储方式?我必须承认,想到一个整数矩阵和一个字符串矩阵一样轻,感觉有些奇怪。 - iago-lito
@lago-lito,请查看?factor中有关字符向量成本的注释。矩阵只是带有“dim”属性的向量,因此真正重要的是底层向量的存储方式。 - BrodieG
太好了,谢谢!我刚刚还发现了一个函数utils::object.size,可以帮助我测量矩阵的实际大小,非常有趣 ;) - iago-lito

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