Baisse des niveaux de facteur dans une trame de données sous-réglée

543

J'ai un bloc de données contenant un factor. Lorsque je crée un sous-ensemble de cette trame de données à l'aide de subsetou d'une autre fonction d'indexation, une nouvelle trame de données est créée. Cependant, lefactor variable conserve tous ses niveaux d'origine, même si / s'ils n'existent pas dans la nouvelle trame de données.

Cela pose des problèmes lors du traçage à facettes ou de l'utilisation de fonctions qui dépendent des niveaux de facteur.

Quelle est la manière la plus succincte de supprimer des niveaux d'un facteur dans la nouvelle trame de données?

Voici un exemple:

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
la source

Réponses:

420

Tout ce que vous devez faire est d'appliquer à nouveau le facteur () à votre variable après le sous-ensemble:

> 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

ÉDITER

Dans l'exemple de la page factorielle:

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

Pour supprimer des niveaux de toutes les colonnes de facteurs dans une trame de données, vous pouvez utiliser:

subdf <- subset(df, numbers <= 3)
subdf[] <- lapply(subdf, function(x) if(is.factor(x)) factor(x) else x)
hatmatrix
la source
22
C'est bien pour une seule fois, mais dans un data.frame avec un grand nombre de colonnes, vous pouvez le faire sur chaque colonne qui est un facteur ... conduisant au besoin d'une fonction telle que drop.levels () de gdata.
Dirk Eddelbuettel
6
Je vois ... mais du point de vue de l'utilisateur, il est rapide d'écrire quelque chose comme subdf [] <- lapply (subdf, function (x) if (is.factor (x)) factor (x) else x) ... Is drop.levels () beaucoup plus efficace sur le plan du calcul ou mieux avec de grands ensembles de données? (Il faudrait réécrire la ligne ci-dessus dans une boucle for pour une énorme trame de données, je suppose.)
hatmatrix
1
Merci Stephen et Dirk - je donne à celui-ci le pouce levé pour les caes d'un facteur, mais j'espère que les gens liront ces commentaires pour vos suggestions sur le nettoyage d'une base de données entière de facteurs.
medriscoll
9
Comme effet secondaire, la fonction convertit le mydf <- droplevels(mydf)bloc de données en une liste, donc la solution suggérée par Roman Luštrik et Tommy O'Dell ci-dessous est préférable.
Johan
1
En outre: cette méthode ne conserver l'ordre de la variable.
webelo
492

Depuis R version 2.12, il existe une droplevels()fonction.

levels(droplevels(subdf$letters))
Roman Luštrik
la source
7
Un avantage de cette méthode sur l'utilisation factor()est qu'il n'est pas nécessaire de modifier la trame de données d'origine ou de créer une nouvelle trame de données persistante. Je peux droplevelsencapsuler une trame de données sous-définie et l'utiliser comme argument de données pour une fonction de réseau, et les groupes seront traités correctement.
Mars
J'ai remarqué que si j'ai un niveau NA dans mon facteur (un véritable niveau NA), il est baissé par niveaux baissés, même si les NA sont présents.
Meep
46

Si vous ne voulez pas de ce comportement, n'utilisez pas de facteurs, utilisez plutôt des vecteurs de caractères. Je pense que cela a plus de sens que de rafistoler les choses par la suite. Essayez ce qui suit avant de charger vos données avec read.tableou read.csv:

options(stringsAsFactors = FALSE)

L'inconvénient est que vous êtes limité à l'ordre alphabétique. (réorganiser est votre ami pour les parcelles)

hadley
la source
38

Il s'agit d'un problème connu, et un remède possible est fourni par drop.levels()dans le package gdata où votre exemple devient

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

Il y a aussi la dropUnusedLevelsfonction dans le package Hmisc . Cependant, cela ne fonctionne qu'en modifiant l'opérateur de sous-ensemble [et n'est pas applicable ici.

En corollaire, une approche directe colonne par colonne est 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
la source
5
Le reorderparamètre de la drop.levelsfonction mérite d'être mentionné: si vous devez conserver l'ordre d'origine de vos facteurs, utilisez-le avec FALSEvaleur.
daroczig
L'utilisation de gdata pour juste drop.levels produit "gdata: le support read.xls pour les fichiers 'XLS' (Excel 97-2004) est ACTIVÉ." "gdata: impossible de charger les bibliothèques perl nécessaires à read.xls ()" "gdata: pour prendre en charge les fichiers 'XLSX' (Excel 2007+)." "gdata: Exécutez la fonction 'installXLSXsupport ()'" "gdata: pour télécharger et installer automatiquement le perl". Utilisez des droplevels de baseR ( stackoverflow.com/a/17218028/9295807 )
Vrokipal
Les choses arrivent avec le temps. Vous êtes formulez des commentaires sur une réponse que j'ai écrit il y a neuf ans. Prenons donc cela comme un indice pour préférer généralement les solutions de base R car ce sont celles qui utilisent des fonctionnalités qui vont encore être dans environ N ans.
Dirk Eddelbuettel
25

Une autre façon de faire la même chose mais avec dplyr

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

Éditer:

Fonctionne également! Merci à agenis

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

Par souci d'exhaustivité, il y a maintenant également fct_dropdans le forcatspackage http://forcats.tidyverse.org/reference/fct_drop.html .

Il diffère de droplevelsla manière dont il traite 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
la source
15

Voici une autre façon, qui je crois est équivalente à l' factor(..)approche:

> 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
la source
Ha, après toutes ces années, je ne savais pas qu'il existe une `[.factor`méthode qui a un dropargument et vous l'avez posté en 2009 ...
David Arenburg
8

C'est désagréable. Voici comment je le fais habituellement, pour éviter de charger d'autres packages:

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

ce qui vous donne:

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

Notez que les nouveaux niveaux remplaceront tout ce qui occupe leur index dans les anciens niveaux (lettres subdf $), donc quelque chose comme:

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

ne fonctionnera pas.

Ce n'est évidemment pas idéal lorsque vous avez beaucoup de niveaux, mais pour certains, c'est rapide et facile.

Matt Parker
la source
8

En regardant le code desdroplevels méthodes dans la source R, vous pouvez voir qu'il factorfonctionne correctement . Cela signifie que vous pouvez recréer la colonne avec une factorfonction.
Sous le moyen data.table pour supprimer les niveaux de toutes les colonnes de facteurs.

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
la source
1
Je pense que le data.tablechemin serait quelque chose commefor (j in names(DT)[sapply(DT, is.factor)]) set(DT, j = j, value = factor(DT[[j]]))
David Arenburg
1
@DavidArenburg cela ne change pas grand-chose ici car nous [.data.table
n'appelons
7

voici une façon de le faire

varFactor <- factor(letters[1:15])
varFactor <- varFactor[1:5]
varFactor <- varFactor[drop=T]
Diogo
la source
2
Ceci est une dupe de cette réponse qui a été publiée 5 ans plus tôt.
David Arenburg
6

J'ai écrit des fonctions utilitaires pour ce faire. Maintenant que je connais les drop.levels de gdata, cela semble assez similaire. Les voici (d' ici ):

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
la source
4

Fil très intéressant, j'ai particulièrement aimé l'idée de simplement factoriser à nouveau la sous-sélection. J'ai eu le même problème avant et je viens de convertir en personnage, puis de nouveau en facteur.

   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
la source
Je veux dire, factor(as.chracter(...))fonctionne, mais juste moins efficacement et succinctement que factor(...). Semble strictement pire que les autres réponses.
Gregor Thomas
1

Malheureusement, factor () ne semble pas fonctionner lors de l'utilisation de rxDataStep de RevoScaleR. Je le fais en deux étapes: 1) Convertir en caractère et stocker dans un cadre de données externe temporaire (.xdf). 2) Convertissez à nouveau en facteur et stockez dans une trame de données externe définitive. Cela élimine tous les niveaux de facteur inutilisés, sans charger toutes les données en mémoire.

# 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
la source
1

J'ai essayé la plupart des exemples ici, sinon tous, mais aucun ne semble fonctionner dans mon cas. Après avoir lutté pendant un certain temps, j'ai essayé d'utiliser as.character () sur la colonne factor pour le changer en un col avec des chaînes qui semble fonctionner très bien.

Pas sûr pour les problèmes de performances.

Naga Pakalapati
la source