recodificar - factores en r
Por qué usar as.factor() en lugar de simplemente factor() (1)
Hace poco vi a Matt Dowle escribir un código con as.factor()
, específicamente
for (col in names_factors) set(dt, j=col, value=as.factor(dt[[col]]))
en un comentario a esta respuesta .
Usé este fragmento, pero necesitaba establecer explícitamente los niveles de los factores para asegurarme de que los niveles aparecían en el orden deseado, así que tuve que cambiar
as.factor(dt[[col]])
a
factor(dt[[col]], levels = my_levels)
Esto me hizo pensar: ¿qué (si hay alguno) es el beneficio de usar as.factor()
versus solo factor()
?
as.factor
es un contenedor para factor
, pero permite un retorno rápido si el vector de entrada ya es un factor:
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)
}
Comentario de : no es una mera envoltura, ya que este "retorno rápido" dejará los niveles de factores tal como están, mientras que factor()
no:
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
Respuesta expandida dos años después, que incluye lo siguiente:
- ¿Qué dice el manual?
- Rendimiento:
as.factor
> cuando la entrada es un factor - Rendimiento:
as.factor
> cuando la entrada es un número entero - Niveles no utilizados o niveles de NA
- Precaución al usar las funciones de agrupamiento por R: observe los niveles de NA o no utilizados
¿Qué dice el manual?
La documentación para ?factor
menciona lo siguiente:
‘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’.
Rendimiento: as.factor
> cuando la entrada es un factor
La palabra "no-operación" es un poco ambigua. No lo tome como "no hacer nada"; de hecho, significa "hacer muchas cosas pero básicamente no cambiar nada". Aquí hay un ejemplo:
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
da un retorno rápido, pero el factor
no es un verdadero "no-op". Vamos a factor
perfil para ver lo que ha hecho.
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
Primero sort
los valores unique
del vector de entrada f
, luego convierte f
en un vector de caracteres, finalmente usa el factor
para forzar al vector de caracteres a volver a un factor. Aquí está el código fuente del factor
de confirmación.
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
}
Por lo tanto, el factor
función está realmente diseñado para funcionar con un vector de caracteres y aplica as.character
a su entrada para garantizar eso. Al menos podemos aprender dos problemas relacionados con el rendimiento desde arriba:
- Para un marco de datos
DF
,lapply(DF, as.factor)
es mucho más rápido quelapply(DF, factor)
para la conversión de tipo, si muchas columnas son factores fácilmente. - Ese
factor
función es lento puede explicar por qué algunas funciones R importantes son lentas, por ejemplo,table
: R: función de tabla sorprendentemente lenta
Rendimiento: as.factor
> cuando la entrada es un número entero
Una variable de factor es el pariente más cercano de una variable entera.
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"
Esto significa que convertir un número entero a un factor es más fácil que convertir un número / carácter a un factor. as.factor
simplemente se ocupa de esto.
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
Niveles no utilizados o niveles de NA
Ahora veamos algunos ejemplos sobre el factor
y la influencia del factor en los niveles de los factores (si la entrada ya es un factor). ha dado uno con nivel de factor no utilizado, le proporcionaré uno con nivel de 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
Hay una función (genérica) droplevels
que se puede utilizar para soltar niveles no utilizados de un factor. Pero los niveles de NA
no se pueden eliminar por defecto.
## "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
Precaución al usar las funciones de agrupamiento por R: observe los niveles de NA o no utilizados
Las funciones R que realizan operaciones de agrupamiento por split
, como split
, esperan que proporcionemos variables de factor como variables "por". Pero a menudo simplemente proporcionamos caracteres o variables numéricas. Así que internamente, estas funciones necesitan convertirlas en factores y probablemente la mayoría de ellas usaría como as.factor
(al menos esto es así para split.default
y tapply
). La función de table
ve como una excepción y yo veo factor
lugar de as.factor
factor
en el interior. Puede haber alguna consideración especial que desafortunadamente no sea obvia para mí cuando inspecciono su código fuente.
Como la mayoría de las funciones R as.factor
utilizan como as.factor
, si se les asigna un factor con niveles NA
o no utilizados, dicho grupo aparecerá en el resultado.
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
Curiosamente, aunque la table
no se basa en as.factor
, también conserva esos niveles no utilizados:
table(f)
#a b c
#1 1 0
Algunas veces este tipo de comportamiento puede ser no deseado. Un ejemplo clásico es barplot(table(f))
:
Si esto no es realmente deseado, tenemos que eliminar manualmente los niveles no utilizados o de NA
de nuestra variable de factor, utilizando droplevels
o factor
.
Insinuación:
-
split
tiene unadrop
argumento que por defecto esFALSE
poras.factor
tanto,as.factor
se usa elas.factor
; pordrop = TRUE
factor
funcióndrop = TRUE
se utiliza en su lugar. -
aggregate
depende desplit
, por lo que también tiene un argumentodrop
y su valor predeterminado esTRUE
. -
tapply
no tienedrop
aunque también se basa ensplit
. En particular, la documentación?tapply
dice queas.factor
es (siempre) utilizado.