对 R 因子进行算术运算

7

我有一个R数据框,想要从一个列中减去另一个列。我使用$运算符提取列,但是这些列的类别是“因子”,而R不会对因子执行算术运算。是否有特殊的函数可以处理这个问题?


2
在R中,因子通常用于分类(或有序)数据。那么,如何为分类数据定义算术运算? - Andrie
4个回答

22

如果你真的希望使用因子的水平,那么要么你正在做一些非常错误的事情,要么就是太聪明了而且自己玩过头了。

如果你拥有的是一个包含存储在因子水平中的数字的因子,那么你首先需要使用 as.numeric(as.character(...)) 强制将其转换为数值类型:

dat <- data.frame(f=as.character(runif(10)))
你可以在这里看到访问因子索引和分配因子内容之间的差异:
> as.numeric(dat$f)
 [1]  9  7  2  1  4  6  5  3 10  8
> as.numeric(as.character(dat$f))
 [1] 0.6369432 0.4455214 0.1204000 0.0336245 0.2731787 0.4219241 0.2910194
 [8] 0.1868443 0.9443593 0.5784658

对于非每个元素都独立的情况,时间复杂度与仅在级别上进行转换的替代方法相比表现得更快:

dat <- data.frame( f = sample(as.character(runif(10)),10^4,replace=TRUE) )
library(microbenchmark)
microbenchmark(
  as.numeric(as.character(dat$f)),
  as.numeric( levels(dat$f) )[dat$f] ,
  as.numeric( levels(dat$f)[dat$f] ),
  times=50
  )

                              expr     min      lq  median      uq     max
1  as.numeric(as.character(dat$f)) 7835865 7869228 7919699 7998399 9576694
2 as.numeric(levels(dat$f))[dat$f]  237814  242947  255778  270321  371263
3 as.numeric(levels(dat$f)[dat$f]) 7817045 7905156 7964610 8121583 9297819

因此,如果 length(levels(dat$f)) < length(dat$f),使用 as.numeric(levels(dat$f))[dat$f] 可以大幅提高速度。

如果 length(levels(dat$f)) 大约等于 length(dat$f),则不会有速度提升:

dat <- data.frame( f = as.character(runif(10^4) ) )
library(microbenchmark)
microbenchmark(
  as.numeric(as.character(dat$f)),
  as.numeric( levels(dat$f) )[dat$f] ,
  as.numeric( levels(dat$f)[dat$f] ),
  times=50
  )

                              expr     min      lq  median      uq      max
1  as.numeric(as.character(dat$f)) 7986423 8036895 8101480 8202850 12522842
2 as.numeric(levels(dat$f))[dat$f] 7815335 7866661 7949640 8102764 15809456
3 as.numeric(levels(dat$f)[dat$f]) 7989845 8040316 8122012 8330312 10420161

虽然R在因子化之前会自动进行排序,所以如果它们是整数,这个问题就不相关了。 - Brandon Bertelsen
2
@Brandon:除非有人使用了relevel或整数序列不连续。假设级别索引与级别内容相同似乎是一个危险的假设。 - Ari B. Friedman
一个提示:使用rbenchmark而不是microbenchmark可以获得更易读的输出和相对速度。 - Joris Meys
@Joris:我喜欢rbenchmark的输出,但是我认为microbenchmark更准确,因为它不包括system.time()引起的一些调用开销... - Ari B. Friedman
准确性在这里是一个相对的概念。重新进行三次分析,每次得到不同的数字。毫秒级的准确性是一件好事,但超过这个范围就会进入随机性... - Joris Meys
我会说期望上更少的偏见。由于mb减去了基准测试本身的开销时间估计,即使对于两者结果都是随机的,它们在mb与rb中也是无偏的(除非估计有偏)。但这有点挑剔,因为我们谈论的数字非常小。在大多数使用情况下,良好的演示胜过精确的计时,我真的希望微基准测试有更好的输出(print.microbenchmarkplot.microbenchmark都需要大幅改进)。 - Ari B. Friedman

3
你可以定义自己的运算符来实现这一点,参见? Arith。如果没有泛型组,则可以定义自己的二元运算符%operator%:
%-% <- function (factor1, factor2){
  # put in the code here to calculate difference 
  # of two factors (e.g. facor1 level cat - factor2 level mouse = ?)
}

3

首先,你应该仔细检查数据的提取方式。如果这些列确实是数字列,R会识别它们(Excel有时会出错)。但无论如何,由于列中可能存在其他不良因素,它可能被强制转换为因子。到目前为止,你收到的回复都没有提到as.numeric()只返回级别数字。这意味着你不会对已转换为因子的实际数字执行操作,而是对与每个因子相关联的级别数字执行操作。


1
你需要将因子转换成数值数组。
a <- factor(c(5,6,5))
b <- factor(c(3,2,1))
df <- data.frame(a, b)

# WRONG: Factors can't be subtracted.
df$a - df$b

# CORRECT: Get the levels and substract
as.numeric(levels(df$a)[df$a]) - as.numeric(levels(df$b)[df$b])

1
-1 这假设 a) 您的因子已排序,b) 数据是区间比例尺的。如果是这种情况,则数据一开始就不应该是一个因子。 - Andrie
+1是一种比其他解决方案中提供的as.numeric(as.character())更好的转换因子的方法。 - Joris Meys
安德里:如果向量没有排序(尽管可能想要进行集合交集),减法是否有意义的解释?我怀疑数据导入存在问题,导致数据首先被因子化。这在我的几次经历中都发生过。然后,当然,正确的方法是去除数据的因子并修复导入。 - Janne Peltola
@Joris:这不是正确的方法,但它看起来类似于正确的方法。如果你希望获得效率提升,调用as.numeric应该只包装级别。请参见我的答案以获取基准测试结果。 - Ari B. Friedman
@gsk3:谢谢,我之前不知道这会涉及到性能问题。当然,你的方法更高效。 - Janne Peltola

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