为什么要使用as.factor()而不是仅使用factor()?

97

最近我看到Matt Dowle写了一些代码,其中包括as.factor()

for (col in names_factors) set(dt, j=col, value=as.factor(dt[[col]]))

这个回答的评论中

我使用了这个代码片段,但是我需要显式地设置因子级别,以确保级别按照我期望的顺序出现,所以我不得不做出改变。

as.factor(dt[[col]])
factor(dt[[col]], levels = my_levels)

这让我思考:使用as.factor()与仅使用factor()相比,有什么好处(如果有的话)?


5
命名的一致性很重要。几乎所有常见的类都有一个叫做 as.class 的函数。 - Gregor Thomas
1个回答

119

as.factorfactor的一个包装器,但如果输入向量已经是因子,则可以快速返回:

function (x) 
{
    if (is.factor(x)) 
        x
    else if (!is.object(x) && is.integer(x)) {
        levels <- sort(unique.default(x))
        f <- match(x, levels)
        levels(f) <- as.character(levels)
        if (!is.null(nx <- names(x))) 
        names(f) <- nx
        class(f) <- "factor"
        f
    }
else factor(x)
}

来自Frank的评论:这不仅仅是一个包装器,因为这个“快速返回”将保留因子水平,而factor()则不会:

f = factor("a", levels = c("a", "b"))
#[1] a
#Levels: a b

factor(f)
#[1] a
#Levels: a

as.factor(f)
#[1] a
#Levels: a b

两年后的扩展回答,包括以下内容:

  • 手册上说了什么?
  • 性能:当输入为因子时,as.factor > factor
  • 性能:当输入为整数时,as.factor > factor
  • 未使用的水平或NA水平
  • 使用R的分组函数时要小心:注意未使用或NA水平

说明书上说什么?

?factor 的文档提到了以下内容:

‘factor(x, exclude = NULL)’ applied to a factor without ‘NA’s is a
 no-operation unless there are unused levels: in that case, a
 factor with the reduced level set is returned.

 ‘as.factor’ coerces its argument to a factor.  It is an
 abbreviated (sometimes faster) form of ‘factor’.

性能:当输入是因子时,as.factor > factor

“无操作”这个词有点含糊不清。不要把它理解为“什么都不做”;实际上,它的意思是“做了很多事情,但本质上没有改变任何东西”。以下是一个例子:

set.seed(0)
## a randomized long factor with 1e+6 levels, each repeated 10 times
f <- sample(gl(1e+6, 10))

system.time(f1 <- factor(f))  ## default: exclude = NA
#   user  system elapsed 
#  7.640   0.216   7.887 

system.time(f2 <- factor(f, exclude = NULL))
#   user  system elapsed 
#  7.764   0.028   7.791 

system.time(f3 <- as.factor(f))
#   user  system elapsed 
#      0       0       0 

identical(f, f1)
#[1] TRUE

identical(f, f2)
#[1] TRUE

identical(f, f3)
#[1] TRUE

as.factor 可以快速返回结果,但是 factor 并不是真正的“no-op”(不执行操作)。让我们对 factor 进行分析,看看它都做了什么。

Rprof("factor.out")
f1 <- factor(f)
Rprof(NULL)
summaryRprof("factor.out")[c(1, 4)]
#$by.self
#                      self.time self.pct total.time total.pct
#"factor"                   4.70    58.90       7.98    100.00
#"unique.default"           1.30    16.29       4.42     55.39
#"as.character"             1.18    14.79       1.84     23.06
#"as.character.factor"      0.66     8.27       0.66      8.27
#"order"                    0.08     1.00       0.08      1.00
#"unique"                   0.06     0.75       4.54     56.89
#
#$sampling.time
#[1] 7.98

它首先对输入向量 funique 值进行排序,然后将 f 转换为字符向量,最后使用 factor 将字符向量强制转换回因子。这是确认 factor 源代码的方法。
function (x = character(), levels, labels = levels, exclude = NA, 
    ordered = is.ordered(x), nmax = NA) 
{
    if (is.null(x)) 
        x <- character()
    nx <- names(x)
    if (missing(levels)) {
        y <- unique(x, nmax = nmax)
        ind <- sort.list(y)
        levels <- unique(as.character(y)[ind])
    }
    force(ordered)
    if (!is.character(x)) 
        x <- as.character(x)
    levels <- levels[is.na(match(levels, exclude))]
    f <- match(x, levels)
    if (!is.null(nx)) 
        names(f) <- nx
    nl <- length(labels)
    nL <- length(levels)
    if (!any(nl == c(1L, nL))) 
        stop(gettextf("invalid 'labels'; length %d should be 1 or %d", 
            nl, nL), domain = NA)
    levels(f) <- if (nl == nL) 
        as.character(labels)
    else paste0(labels, seq_along(levels))
    class(f) <- c(if (ordered) "ordered", "factor")
    f
}

因此,函数factor的设计是为了处理字符向量,并且它将as.character应用于其输入以确保如此。我们至少可以从上面学到两个与性能相关的问题:

  1. 对于数据框DF,如果许多列已经是因子,则lapply(DF,as.factor)在类型转换时比lapply(DF,factor)快得多。
  2. 函数factor很慢可以解释为什么一些重要的R函数很慢,例如tableR:table函数令人惊讶地慢

性能:as.factor> factor,当输入为整数时

因子变量是整数变量的近亲。

unclass(gl(2, 2, labels = letters[1:2]))
#[1] 1 1 2 2
#attr(,"levels")
#[1] "a" "b"

storage.mode(gl(2, 2, labels = letters[1:2]))
#[1] "integer"

这意味着将整数转换为因子比将数字/字符转换为因子更容易。 as.factor 就可以轻松解决这个问题。
x <- sample.int(1e+6, 1e+7, TRUE)

system.time(as.factor(x))
#   user  system elapsed 
#  4.592   0.252   4.845 

system.time(factor(x))
#   user  system elapsed 
# 22.236   0.264  22.659 

未使用的水平或NA水平

现在让我们通过一些关于factoras.factor对因子水平(如果输入已经是一个因子)的影响的例子。Frank给出了一个未使用的因子水平的例子,我将提供一个包含NA水平的示例。

f <- factor(c(1, NA), exclude = NULL)
#[1] 1    <NA>
#Levels: 1 <NA>

as.factor(f)
#[1] 1    <NA>
#Levels: 1 <NA>

factor(f, exclude = NULL)
#[1] 1    <NA>
#Levels: 1 <NA>

factor(f)
#[1] 1    <NA>
#Levels: 1

有一个(通用的)函数droplevels可用于删除因子中未使用的级别。 但是,默认情况下无法删除NA级别。

## "factor" method of `droplevels`
droplevels.factor
#function (x, exclude = if (anyNA(levels(x))) NULL else NA, ...) 
#factor(x, exclude = exclude)

droplevels(f)
#[1] 1    <NA>
#Levels: 1 <NA>

droplevels(f, exclude = NA)
#[1] 1    <NA>
#Levels: 1

注意使用R的分组函数时要小心:注意未使用或NA水平
R函数执行分组操作,如split,tapply希望我们提供因子变量作为“按”变量。但是,通常我们只提供字符或数字变量。因此,在内部,这些函数需要将它们转换为因子,并且可能大多数函数首先使用as.factor(至少对于split.default和tapply而言)。table函数看起来像一个例外,我发现它内部使用factor而不是as.factor。可能有一些特殊考虑,但不幸的是在我检查其源代码时并不明显。
由于大多数分组R函数使用as.factor,如果它们给出了一个具有未使用或NA级别的因子,则该组将出现在结果中。
x <- c(1, 2)
f <- factor(letters[1:2], levels = letters[1:3])

split(x, f)
#$a
#[1] 1
#
#$b
#[1] 2
#
#$c
#numeric(0)

tapply(x, f, FUN = mean)
# a  b  c 
# 1  2 NA 

有趣的是,尽管 table 不依赖于 as.factor,它仍然保留了那些未使用的水平值:
table(f)
#a b c 
#1 1 0 

有时这种行为可能是不期望的。一个经典的例子是barplot(table(f)):

enter image description here

如果这真的不需要,我们需要手动从因子变量中删除未使用或NA级别,使用droplevelsfactor
提示:
1. split有一个默认为FALSE的参数drop,因此使用as.factor; 通过drop = TRUE函数factor被使用。
2. aggregate依赖于split,因此它也有一个drop参数,默认为TRUE
3. tapply没有drop,尽管它也依赖于split。特别是文档?tapplyas.factor被(始终)使用。

4
答案中函数factor的源代码是基于R 3.4.4版本的。自R 3.5.0版本以来,源代码已经发生了很大变化,但答案中所有结论仍然有效。 - Zheyuan Li

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