Níveis de fator de queda em um quadro de dados subconjuntos

543

Eu tenho um quadro de dados contendo um factor. Quando eu crio um subconjunto desse quadro de dados usando subsetou outra função de indexação, um novo quadro de dados é criado. No entanto, a factorvariável mantém todos os seus níveis originais, mesmo quando / se eles não existirem no novo quadro de dados.

Isso causa problemas ao fazer plotagens facetadas ou ao usar funções que dependem de níveis de fatores.

Qual é a maneira mais sucinta de remover níveis de um fator no novo quadro de dados?

Aqui está um exemplo:

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
fonte

Respostas:

420

Tudo o que você deve fazer é aplicar o fator () à sua variável novamente após o 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

No exemplo da página de fatores:

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

Para eliminar níveis de todas as colunas de fatores em um quadro de dados, você pode usar:

subdf <- subset(df, numbers <= 3)
subdf[] <- lapply(subdf, function(x) if(is.factor(x)) factor(x) else x)
hatmatrix
fonte
22
Isso é bom para um caso único, mas em um data.frame com um grande número de colunas, você faz isso em cada coluna que é um fator ... levando à necessidade de uma função como drop.levels () de gdata.
Dirk Eddelbuettel 29/07/2009
6
Entendo ... mas do ponto de vista do usuário, é rápido escrever algo como subdf [] <- lapply (subdf, função (x) if (is.factor (x)) factor (x) else x) ... drop.levels () muito mais eficiente em termos computacionais ou melhor com grandes conjuntos de dados? (Um teria que reescrever a linha acima em um loop for para um quadro de dados enorme, eu suponho.)
hatmatrix
1
Obrigado Stephen & Dirk - Eu estou dando a este um sinal de positivo para o caes de um fator, mas espero que as pessoas leiam esses comentários para suas sugestões sobre como limpar todo um quadro de dados.
medriscoll
9
Como efeito colateral, a função converte o quadro de dados em uma lista; portanto, mydf <- droplevels(mydf)é preferível a solução sugerida por Roman Luštrik e Tommy O'Dell abaixo.
Johan
1
Também: este método faz preservar a ordenação da variável.
Webel
492

Desde a versão 2.12 do R, há uma droplevels()função.

levels(droplevels(subdf$letters))
Roman Luštrik
fonte
7
Uma vantagem desse método em relação ao uso factor()é que não é necessário modificar o quadro de dados original ou criar um novo quadro de dados persistente. Posso envolver droplevelsum quadro de dados subconjunto e usá-lo como argumento de dados para uma função de treliça, e os grupos serão tratados corretamente.
Mars
Percebi que, se eu tenho um nível de NA em meu fator (um nível genuíno de NA), ele diminui os níveis, mesmo se os NAs estiverem presentes.
Meep
46

Se você não deseja esse comportamento, não use fatores, use vetores de caracteres. Eu acho que isso faz mais sentido do que consertar as coisas depois. Tente o seguinte antes de carregar seus dados com read.tableou read.csv:

options(stringsAsFactors = FALSE)

A desvantagem é que você está restrito à ordem alfabética. (reordenar é seu amigo para terrenos)

Hadley
fonte
38

É um problema conhecido, e um possível remédio é fornecido drop.levels()no pacote gdata em que seu exemplo se torna

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

Há também a dropUnusedLevelsfunção no pacote Hmisc . No entanto, ele funciona apenas alterando o operador de subconjunto [e não é aplicável aqui.

Como corolário, uma abordagem direta por coluna é simples 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
fonte
5
O reorderparâmetro da drop.levelsfunção é vale a pena mencionar: se você tem que preservar a ordem original de seus fatores, usá-lo com FALSEvalor.
daroczig
O uso do gdata apenas para drop.levels gera "suporte ao gdata: read.xls para arquivos 'XLS' (Excel 97-2004) HABILITADOS." msgstr "gdata: Não foi possível carregar as bibliotecas perl necessárias para read.xls ()" "gdata: para suportar arquivos 'XLSX' (Excel 2007+)." "gdata: Execute a função 'installXLSXsupport ()'" "gdata: para baixar e instalar automaticamente o perl". Use droplevels da baseR ( stackoverflow.com/a/17218028/9295807 )
Vrokipal
As coisas acontecem com o tempo. Você está comentando uma resposta que escrevi há nove anos. Então, tomemos isso como uma dica para preferir geralmente as soluções R básicas, pois essas são as que usam funcionalidades que ainda estarão em torno de N anos a partir de agora.
Dirk Eddelbuettel
25

Outra maneira de fazer o mesmo, mas com dplyr

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

Editar:

Também funciona! Graças a agenis

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

Por uma questão de completude, agora também existe fct_dropno forcatspacote http://forcats.tidyverse.org/reference/fct_drop.html .

Difere da droplevelsmaneira como lida com 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
fonte
15

Aqui está outra maneira, que acredito ser equivalente à factor(..)abordagem:

> 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
fonte
Ha, depois de todos esses anos eu não sabia que é um `[.factor`método que tem um dropargumento e você postou isso em 2009 ...
David Arenburg
8

Isso é desagradável. É assim que eu costumo fazer isso, para evitar carregar outros pacotes:

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

que você recebe:

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

Observe que os novos níveis substituirão o que ocupa seu índice nos níveis antigos (subdf $ letters), então algo como:

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

não vai funcionar.

Obviamente, isso não é ideal quando você tem muitos níveis, mas, para alguns, é rápido e fácil.

Matt Parker
fonte
8

Observando o código dos droplevelsmétodos na fonte R, você pode vê- lo factorfuncionar. Isso significa que você pode basicamente recriar a coluna com a factorfunção
Abaixo da maneira data.table para reduzir os níveis de todas as colunas de fatores.

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
fonte
1
Eu acho que o data.tablecaminho seria algo comofor (j in names(DT)[sapply(DT, is.factor)]) set(DT, j = j, value = factor(DT[[j]]))
David Arenburg
1
@DavidArenburg isso não muda muito aqui como chamamos [.data.tableapenas uma vez
jangorecki
7

aqui está uma maneira de fazer isso

varFactor <- factor(letters[1:15])
varFactor <- varFactor[1:5]
varFactor <- varFactor[drop=T]
Diogo
fonte
2
Este é um engano desta resposta, publicada 5 anos antes.
David Arenburg
6

Eu escrevi funções utilitárias para fazer isso. Agora que eu sei sobre o drop.levels do gdata, parece bastante semelhante. Aqui estão eles ( daqui ):

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
fonte
4

Tópico muito interessante, gostei especialmente da ideia de fatorar novamente a subseleção. Eu tive o problema semelhante antes e acabei de converter em caractere e depois voltar ao fator.

   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
fonte
Quero dizer, factor(as.chracter(...))funciona, mas apenas de forma menos eficiente e sucinta do que factor(...). Parece estritamente pior que as outras respostas.
Gregor Thomas
1

Infelizmente, factor () parece não funcionar ao usar o rxDataStep do RevoScaleR. Eu faço isso em duas etapas: 1) Converta em caractere e armazene no quadro de dados externo temporário (.xdf). 2) Converta de volta ao fator e armazene no quadro de dados externo definitivo. Isso elimina qualquer nível de fator não utilizado, sem carregar todos os dados na memória.

# 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
fonte
1

Tentei a maioria dos exemplos aqui, se não todos, mas nenhum parece estar funcionando no meu caso. Depois de lutar por algum tempo, tentei usar as.character () na coluna de fator para alterá-lo para um col com strings que parece estar funcionando bem.

Não tenho certeza sobre problemas de desempenho.

Naga Pakalapati
fonte