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"

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)

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))

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)


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"

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)

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

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"

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.


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"

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]

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
}

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))

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)

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.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.