Caída de niveles de factores en un marco de datos subconjunto

543

Tengo un marco de datos que contiene a factor. Cuando creo un subconjunto de este marco de datos usando subsetu otra función de indexación, se crea un nuevo marco de datos. Sin embargo, la factorvariable conserva todos sus niveles originales, incluso cuando / si no existen en el nuevo marco de datos.

Esto causa problemas al hacer trazados facetados o al usar funciones que dependen de niveles de factores.

¿Cuál es la forma más sucinta de eliminar niveles de un factor en el nuevo marco de datos?

Aquí hay un ejemplo:

df <- data.frame(letters=letters[1:5],
                    numbers=seq(1:5))

levels(df$letters)
## [1] "a" "b" "c" "d" "e"

subdf <- subset(df, numbers <= 3)
##   letters numbers
## 1       a       1
## 2       b       2
## 3       c       3    

# all levels are still there!
levels(subdf$letters)
## [1] "a" "b" "c" "d" "e"
medriscoll
fuente

Respuestas:

420

Todo lo que debe hacer es aplicar factor () a su variable nuevamente después de subconjunto:

> subdf$letters
[1] a b c
Levels: a b c d e
subdf$letters <- factor(subdf$letters)
> subdf$letters
[1] a b c
Levels: a b c

EDITAR

Del ejemplo de la página de factores:

factor(ff)      # drops the levels that do not occur

Para eliminar niveles de todas las columnas de factores en un marco de datos, puede usar:

subdf <- subset(df, numbers <= 3)
subdf[] <- lapply(subdf, function(x) if(is.factor(x)) factor(x) else x)
hatmatrix
fuente
22
Eso está bien para una sola vez, pero en un data.frame con una gran cantidad de columnas, puedes hacer eso en cada columna que sea un factor ... lo que lleva a la necesidad de una función como drop.levels () de gdata.
Dirk Eddelbuettel
66
Ya veo ... pero desde la perspectiva del usuario, es rápido escribir algo como subdf [] <- lapply (subdf, function (x) if (is.factor (x)) factor (x) else x) ... Is drop.levels () mucho más eficiente computacionalmente o mejor con grandes conjuntos de datos? (Supongo que habría que reescribir la línea de arriba en un bucle for para un marco de datos enorme)
Hatmatrix
1
Gracias Stephen y Dirk: le doy el visto bueno a los factores de un factor, pero espero que la gente lea estos comentarios para sus sugerencias sobre la limpieza de un marco completo de factores.
medriscoll
99
Como efecto secundario, la función convierte el marco de datos en una lista, por lo mydf <- droplevels(mydf)que es preferible la solución sugerida por Roman Luštrik y Tommy O'Dell a continuación.
Johan
1
También: este método hace preservar el orden de la variable.
webelo 01 de
492

Desde R versión 2.12, hay una droplevels()función.

levels(droplevels(subdf$letters))
Roman Luštrik
fuente
77
Una ventaja de este método sobre el uso factor()es que no es necesario modificar el marco de datos original o crear un nuevo marco de datos persistente. Puedo ajustar droplevelsun marco de datos subconjunto y usarlo como argumento de datos para una función de red, y los grupos se manejarán correctamente.
Marte
Me he dado cuenta de que si tengo un nivel de NA en mi factor (un nivel de NA genuino), se reduce por niveles de caída, incluso si los NA están presentes.
Meep
46

Si no desea este comportamiento, no use factores, use vectores de caracteres en su lugar. Creo que esto tiene más sentido que arreglar las cosas después. Pruebe lo siguiente antes de cargar sus datos con read.tableo read.csv:

options(stringsAsFactors = FALSE)

La desventaja es que está restringido al orden alfabético. (reordenar es tu amigo para las parcelas)

Hadley
fuente
38

Es un problema conocido, y drop.levels()el paquete gdata proporciona un posible remedio donde su ejemplo se convierte

> drop.levels(subdf)
  letters numbers
1       a       1
2       b       2
3       c       3
> levels(drop.levels(subdf)$letters)
[1] "a" "b" "c"

También existe la dropUnusedLevelsfunción en el paquete Hmisc . Sin embargo, solo funciona alterando el operador del subconjunto [y no es aplicable aquí.

Como corolario, un enfoque directo por columna es simple as.factor(as.character(data)):

> levels(subdf$letters)
[1] "a" "b" "c" "d" "e"
> subdf$letters <- as.factor(as.character(subdf$letters))
> levels(subdf$letters)
[1] "a" "b" "c"
Dirk Eddelbuettel
fuente
55
El reorderparámetro de la drop.levelsfunción de la pena mencionar: si tiene que preservar el orden original de los factores, usarlo con FALSEvalor.
daroczig
El uso de gdata solo para drop.levels produce "soporte gdata: read.xls para archivos 'XLS' (Excel 97-2004) HABILITADO". "gdata: no se pueden cargar las bibliotecas Perl que necesita read.xls ()" "gdata: para admitir archivos 'XLSX' (Excel 2007+)". "gdata: ejecute la función 'installXLSXsupport ()'" "gdata: para descargar e instalar automáticamente el perl". Use droplevels de baseR ( stackoverflow.com/a/17218028/9295807 )
Vrokipal
Las cosas pasan con el tiempo. Usted está comentando una respuesta que escribí hace nueve años. Así que tomemos esto como una pista para preferir generalmente las soluciones base R, ya que esas son las que usan la funcionalidad que todavía estará dentro de N años a partir de ahora.
Dirk Eddelbuettel
25

Otra forma de hacer lo mismo pero con dplyr

library(dplyr)
subdf <- df %>% filter(numbers <= 3) %>% droplevels()
str(subdf)

Editar:

También funciona! Gracias a agenis

subdf <- df %>% filter(numbers <= 3) %>% droplevels
levels(subdf$letters)
Prradep
fuente
17

En aras de la exhaustividad, ahora también hay fct_dropen el forcatspaquete http://forcats.tidyverse.org/reference/fct_drop.html .

Se diferencia de droplevelsla forma en que trata NA:

f <- factor(c("a", "b", NA), exclude = NULL)

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

forcats::fct_drop(f)
# [1] a    b    <NA>
# Levels: a b
Aurèle
fuente
15

Aquí hay otra forma, que creo que es equivalente al factor(..)enfoque:

> df <- data.frame(let=letters[1:5], num=1:5)
> subdf <- df[df$num <= 3, ]

> subdf$let <- subdf$let[ , drop=TRUE]

> levels(subdf$let)
[1] "a" "b" "c"
ars
fuente
Ha, después de todos estos años que no saben que hay un `[.factor`método que tiene un dropargumento y que ha publicado esto en 2009 ...
David Arenburg
8

Esto es desagradable. Así es como lo hago generalmente, para evitar cargar otros paquetes:

levels(subdf$letters)<-c("a","b","c",NA,NA)

que te lleva a:

> subdf$letters
[1] a b c
Levels: a b c

Tenga en cuenta que los nuevos niveles reemplazarán todo lo que ocupe su índice en los niveles anteriores (subdf $ letras), así que algo como:

levels(subdf$letters)<-c(NA,"a","c",NA,"b")

no funciona

Obviamente, esto no es ideal cuando tienes muchos niveles, pero para unos pocos, es rápido y fácil.

Matt Parker
fuente
8

Mirando el código dedroplevels métodos en la fuente R, puede ver que se ajusta para factorfuncionar. Eso significa que básicamente puede recrear la columna con la factorfunción.
Debajo de la forma data.table de eliminar niveles de todas las columnas de factores.

library(data.table)
dt = data.table(letters=factor(letters[1:5]), numbers=seq(1:5))
levels(dt$letters)
#[1] "a" "b" "c" "d" "e"
subdt = dt[numbers <= 3]
levels(subdt$letters)
#[1] "a" "b" "c" "d" "e"

upd.cols = sapply(subdt, is.factor)
subdt[, names(subdt)[upd.cols] := lapply(.SD, factor), .SDcols = upd.cols]
levels(subdt$letters)
#[1] "a" "b" "c"
jangorecki
fuente
1
Creo que el data.tablecamino sería algo asífor (j in names(DT)[sapply(DT, is.factor)]) set(DT, j = j, value = factor(DT[[j]]))
David Arenburg el
1
@DavidArenburg no cambia mucho aquí, ya que llamamos [.data.tablesolo una vez
Jangorecki
7

aquí hay una manera de hacerlo

varFactor <- factor(letters[1:15])
varFactor <- varFactor[1:5]
varFactor <- varFactor[drop=T]
Diogo
fuente
2
Esta es una copia de esta respuesta que se publicó 5 años antes.
David Arenburg
6

Escribí funciones de utilidad para hacer esto. Ahora que sé sobre los niveles de caída de gdata, se ve bastante similar. Aquí están (desde aquí ):

present_levels <- function(x) intersect(levels(x), x)

trim_levels <- function(...) UseMethod("trim_levels")

trim_levels.factor <- function(x)  factor(x, levels=present_levels(x))

trim_levels.data.frame <- function(x) {
  for (n in names(x))
    if (is.factor(x[,n]))
      x[,n] = trim_levels(x[,n])
  x
}
Brendan OConnor
fuente
4

Hilo muy interesante, me gustó especialmente la idea de factorizar nuevamente la subselección. Tuve el problema similar antes y simplemente me convertí en personaje y luego volví a factorizar.

   df <- data.frame(letters=letters[1:5],numbers=seq(1:5))
   levels(df$letters)
   ## [1] "a" "b" "c" "d" "e"
   subdf <- df[df$numbers <= 3]
   subdf$letters<-factor(as.character(subdf$letters))
DfAC
fuente
Quiero decir, factor(as.chracter(...))funciona, pero de manera menos eficiente y sucinta que factor(...). Parece estrictamente peor que las otras respuestas.
Gregor Thomas
1

Desafortunadamente, el factor () no parece funcionar cuando se usa rxDataStep de RevoScaleR. Lo hago en dos pasos: 1) Convertir a carácter y almacenar en un marco de datos externo temporal (.xdf). 2) Convertir de nuevo a factor y almacenar en un marco de datos externo definitivo. Esto elimina los niveles de factores no utilizados, sin cargar todos los datos en la memoria.

# Step 1) Converts to character, in temporary xdf file:
rxDataStep(inData = "input.xdf", outFile = "temp.xdf", transforms = list(VAR_X = as.character(VAR_X)), overwrite = T)
# Step 2) Converts back to factor:
rxDataStep(inData = "temp.xdf", outFile = "output.xdf", transforms = list(VAR_X = as.factor(VAR_X)), overwrite = T)
Jerome Smith
fuente
1

He probado la mayoría de los ejemplos aquí, si no todos, pero ninguno parece estar funcionando en mi caso. Después de luchar durante bastante tiempo, he intentado usar as.character () en la columna de factores para cambiarlo a una columna con cadenas que parece funcionar bien.

No estoy seguro de los problemas de rendimiento.

Naga Pakalapati
fuente