Équivalent de déclaration de cas dans R

87

J'ai une variable dans un dataframe où l'un des champs a généralement 7-8 valeurs. Je veux les réduire de 3 ou 4 nouvelles catégories dans une nouvelle variable dans le dataframe. Quelle est la meilleure approche?

J'utiliserais une instruction CASE si j'étais dans un outil de type SQL mais je ne sais pas comment attaquer cela dans R.

Toute aide que vous pouvez fournir sera très appréciée!

Btibert3
la source
a) Sont-ils des nombres entiers, numériques, catégoriels ou des chaînes? Veuillez publier un exemple d'extrait de données, en utilisant dput()b) Voulez-vous une solution dans la base R, dplyr, data.table, tidyverse ...?
smci

Réponses:

38

case_when(), qui a été ajouté à dplyr en mai 2016, résout ce problème d'une manière similaire à memisc::cases().

Par exemple:

library(dplyr)
mtcars %>% 
  mutate(category = case_when(
    .$cyl == 4 & .$disp < median(.$disp) ~ "4 cylinders, small displacement",
    .$cyl == 8 & .$disp > median(.$disp) ~ "8 cylinders, large displacement",
    TRUE ~ "other"
  )
)

À partir de dplyr 0.7.0,

mtcars %>% 
  mutate(category = case_when(
    cyl == 4 & disp < median(disp) ~ "4 cylinders, small displacement",
    cyl == 8 & disp > median(disp) ~ "8 cylinders, large displacement",
    TRUE ~ "other"
  )
)
Evan Cortens
la source
4
Vous n'avez pas besoin du .$devant chaque colonne.
kath
1
Oui, à partir de dplyr 0.7.0 (publié le 9 juin 2017), le .$n'est plus nécessaire. Au moment où cette réponse a été écrite à l'origine, elle l'était.
Evan Cortens
excellente solution. si les deux déclarations sont vraies. Le second écrase-t-il le premier?
JdP
1
@JdP Cela fonctionne comme CASE WHEN en SQL, donc les instructions sont évaluées dans l'ordre et le résultat est la première instruction TRUE. (Donc, dans l'exemple ci-dessus, j'ai mis un TRUE à la fin, qui sert de valeur par défaut.)
Evan Cortens
J'aime cette réponse car, contrairement à switchcela, elle vous permet de créer une séquence d'expressions au lieu de clés pour les cas.
Dannid
27

Jetez un œil à la casesfonction du memiscpackage. Il implémente la fonctionnalité de cas avec deux manières différentes de l'utiliser. À partir des exemples du package:

z1=cases(
    "Condition 1"=x<0,
    "Condition 2"=y<0,# only applies if x >= 0
    "Condition 3"=TRUE
    )

xet ysont deux vecteurs.

Références: package memisc , exemple de cas

Henrico
la source
23

Si c'est le cas, vous factorpouvez changer les niveaux par la méthode standard:

df <- data.frame(name = c('cow','pig','eagle','pigeon'), 
             stringsAsFactors = FALSE)
df$type <- factor(df$name) # First step: copy vector and make it factor
# Change levels:
levels(df$type) <- list(
    animal = c("cow", "pig"),
    bird = c("eagle", "pigeon")
)
df
#     name   type
# 1    cow animal
# 2    pig animal
# 3  eagle   bird
# 4 pigeon   bird

Vous pouvez écrire une fonction simple comme wrapper:

changelevels <- function(f, ...) {
    f <- as.factor(f)
    levels(f) <- list(...)
    f
}

df <- data.frame(name = c('cow','pig','eagle','pigeon'), 
                 stringsAsFactors = TRUE)

df$type <- changelevels(df$name, animal=c("cow", "pig"), bird=c("eagle", "pigeon"))
Marek
la source
1
Bonne réponse. J'ai oublié que vous pouviez utiliser une liste comme argument pour les niveaux avec l'ancien et le nouveau nom comme ça; ma solution dépend de celui qui garde l'ordre des niveaux, donc c'est mieux de cette façon.
Aaron a quitté Stack Overflow le
Aussi, devrait-il être xdans la dernière ligne changelevels?
Aaron a quitté Stack Overflow le
20

Voici un moyen d'utiliser l' switchinstruction:

df <- data.frame(name = c('cow','pig','eagle','pigeon'), 
                 stringsAsFactors = FALSE)
df$type <- sapply(df$name, switch, 
                  cow = 'animal', 
                  pig = 'animal', 
                  eagle = 'bird', 
                  pigeon = 'bird')

> df
    name   type
1    cow animal
2    pig animal
3  eagle   bird
4 pigeon   bird

Le seul inconvénient est que vous devez continuer à écrire le nom de la catégorie ( animal, etc.) pour chaque élément. Il est syntaxiquement plus pratique de pouvoir définir nos catégories comme ci-dessous (voir la question très similaire Comment ajouter une colonne dans un bloc de données dans R )

myMap <- list(animal = c('cow', 'pig'), bird = c('eagle', 'pigeon'))

et nous voulons en quelque sorte "inverser" cette cartographie. J'écris ma propre fonction invMap:

invMap <- function(map) {
  items <- as.character( unlist(map) )
  nams <- unlist(Map(rep, names(map), sapply(map, length)))
  names(nams) <- items
  nams
}

puis inversez la carte ci-dessus comme suit:

> invMap(myMap)
     cow      pig    eagle   pigeon 
"animal" "animal"   "bird"   "bird" 

Et puis il est facile d'utiliser ceci pour ajouter la typecolonne dans la trame de données:

df <- transform(df, type = invMap(myMap)[name])

> df
    name   type
1    cow animal
2    pig animal
3  eagle   bird
4 pigeon   bird
Prasad Chalasani
la source
16

Je ne vois aucune proposition de «changement». Exemple de code (exécutez-le):

x <- "three"
y <- 0
switch(x,
       one = {y <- 5},
       two = {y <- 12},
       three = {y <- 432})
y
adamsss6
la source
14

Imho, le code le plus simple et le plus universel:

dft=data.frame(x = sample(letters[1:8], 20, replace=TRUE))
dft=within(dft,{
    y=NA
    y[x %in% c('a','b','c')]='abc'
    y[x %in% c('d','e','f')]='def'
    y[x %in% 'g']='g'
    y[x %in% 'h']='h'
})
Grégory Demin
la source
J'aime cette méthode. Cependant, y a-t-il une implémentation `` autre '' car dans certaines circonstances, cela serait indispensable
T.Fung
2
@ T.Fung Vous pouvez changer la première ligne en y = 'else'. Les éléments qui ne satisfont à aucune autre condition resteront inchangés.
Gregory Demin
7

Il y a une switchdéclaration, mais je n'arrive jamais à la faire fonctionner comme je pense qu'elle devrait. Puisque vous n'avez pas fourni d'exemple, je vais en créer un en utilisant une variable de facteur:

 dft <-data.frame(x = sample(letters[1:8], 20, replace=TRUE))
 levels(dft$x)
[1] "a" "b" "c" "d" "e" "f" "g" "h"

Si vous spécifiez les catégories souhaitées dans un ordre approprié à la réaffectation, vous pouvez utiliser le facteur ou les variables numériques comme index:

c("abc", "abc", "abc", "def", "def", "def", "g", "h")[dft$x]
 [1] "def" "h"   "g"   "def" "def" "abc" "h"   "h"   "def" "abc" "abc" "abc" "h"   "h"   "abc"
[16] "def" "abc" "abc" "def" "def"

dft$y <- c("abc", "abc", "abc", "def", "def", "def", "g", "h")[dft$x] str(dft)
'data.frame':   20 obs. of  2 variables:
 $ x: Factor w/ 8 levels "a","b","c","d",..: 4 8 7 4 6 1 8 8 5 2 ...
 $ y: chr  "def" "h" "g" "def" ...

J'ai appris plus tard qu'il y avait vraiment deux fonctions de commutation différentes. Ce n'est pas une fonction générique mais vous devriez y penser comme étant soit switch.numericou switch.character. Si votre premier argument est un «facteur» R, vous obtenez un switch.numericcomportement, qui est susceptible de causer des problèmes, car la plupart des gens voient les facteurs affichés comme des caractères et font l'hypothèse erronée que toutes les fonctions les traiteront comme tels.

IRTFM
la source
6

Vous pouvez utiliser recode à partir du package voiture:

library(ggplot2) #get data
library(car)
daimons$new_var <- recode(diamonds$clarity , "'I1' = 'low';'SI2' = 'low';else = 'high';")[1:10]
Boursiers Ian
la source
11
Je ne peux tout simplement pas prendre en charge une fonction qui analyse ses paramètres à partir du texte
hadley
Oui, mais savez-vous si quelqu'un a écrit une meilleure version? sos::findFn("recode")découvertes doBy::recodeVar, epicalc::recode, memisc::recodemais je n'ai pas regardé en détail les ...
Ben Bolker
5

Je n'aime aucun de ceux-ci, ils ne sont pas clairs pour le lecteur ou l'utilisateur potentiel. J'utilise juste une fonction anonyme, la syntaxe n'est pas aussi lisse qu'une instruction de cas, mais l'évaluation est similaire à une instruction de cas et pas si pénible. cela suppose également que vous l'évaluez à l'intérieur de l'endroit où vos variables sont définies.

result <- ( function() { if (x==10 | y< 5) return('foo') 
                         if (x==11 & y== 5) return('bar')
                        })()

tous ces () sont nécessaires pour enfermer et évaluer la fonction anonyme.

jamesM
la source
6
1) La partie fonction n'est pas nécessaire; tu pourrais juste faire result <- (if (x==10 | y< 5) 'foo' else if (x==11 & y== 5) 'bar' ). 2) Cela ne fonctionne que si xet ysont des scalaires; pour les vecteurs, comme dans la question initiale, des ifelsedéclarations imbriquées seraient nécessaires.
Aaron a quitté Stack Overflow le
4

J'utilise dans les cas dont vous parlez switch(). Cela ressemble à une instruction de contrôle, mais en fait, c'est une fonction. L'expression est évaluée et en fonction de cette valeur, l'élément correspondant dans la liste est renvoyé.

switch fonctionne de deux manières distinctes selon que le premier argument correspond à une chaîne de caractères ou à un nombre.

Ce qui suit est un exemple de chaîne simple qui résout votre problème pour réduire les anciennes catégories en nouvelles.

Pour le formulaire chaîne de caractères, utilisez un seul argument sans nom par défaut après les valeurs nommées.

newCat <- switch(EXPR = category,
       cat1   = catX,
       cat2   = catX,
       cat3   = catY,
       cat4   = catY,
       cat5   = catZ,
       cat6   = catZ,
       "not available")
Petzi
la source
3

Si vous voulez avoir une syntaxe de type SQL, vous pouvez simplement utiliser sqldfpackage. La fonction à utiliser est également des noms sqldfet la syntaxe est la suivante

sqldf(<your query in quotation marks>)
kuba
la source
2

Une déclaration de cas n'est peut-être pas la bonne approche ici. S'il s'agit d'un facteur, ce qui est probablement le cas, définissez simplement les niveaux du facteur de manière appropriée.

Disons que vous avez un facteur avec les lettres A à E, comme ceci.

> a <- factor(rep(LETTERS[1:5],2))
> a
 [1] A B C D E A B C D E
Levels: A B C D E

Pour joindre les niveaux B et C et le nommer BC, changez simplement les noms de ces niveaux en BC.

> levels(a) <- c("A","BC","BC","D","E")
> a
 [1] A  BC BC D  E  A  BC BC D  E 
Levels: A BC D E

Le résultat est comme souhaité.

Aaron a quitté le débordement de pile
la source
2

Mixage plyr::mutate et dplyr::case_whenfonctionne pour moi et est lisible.

iris %>%
plyr::mutate(coolness =
     dplyr::case_when(Species  == "setosa"     ~ "not cool",
                      Species  == "versicolor" ~ "not cool",
                      Species  == "virginica"  ~ "super awesome",
                      TRUE                     ~ "undetermined"
       )) -> testIris
head(testIris)
levels(testIris$coolness)  ## NULL
testIris$coolness <- as.factor(testIris$coolness)
levels(testIris$coolness)  ## ok now
testIris[97:103,4:6]

Des points bonus si la colonne peut sortir de la mutation en tant que facteur au lieu de caractère! La dernière ligne de l'instruction case_when, qui capture toutes les lignes sans correspondance, est très importante.

     Petal.Width    Species      coolness
 97         1.3  versicolor      not cool
 98         1.3  versicolor      not cool  
 99         1.1  versicolor      not cool
100         1.3  versicolor      not cool
101         2.5  virginica     super awesome
102         1.9  virginica     super awesome
103         2.1  virginica     super awesome
בנימן הגלילי
la source
2

Vous pouvez utiliser la basefonction mergepour les tâches de remappage de type casse:

df <- data.frame(name = c('cow','pig','eagle','pigeon','cow','eagle'), 
                 stringsAsFactors = FALSE)

mapping <- data.frame(
  name=c('cow','pig','eagle','pigeon'),
  category=c('mammal','mammal','bird','bird')
)

merge(df,mapping)
# name category
# 1    cow   mammal
# 2    cow   mammal
# 3  eagle     bird
# 4  eagle     bird
# 5    pig   mammal
# 6 pigeon     bird
patrickmdnet
la source
1

Depuis data.table v1.13.0, vous pouvez utiliser la fonction fcase()(fast-case) pour effectuer des CASEopérations de type SQL (également similaires à dplyr::case_when()):

require(data.table)

dt <- data.table(name = c('cow','pig','eagle','pigeon','cow','eagle'))
dt[ , category := fcase(name %in% c('cow', 'pig'), 'mammal',
                        name %in% c('eagle', 'pigeon'), 'bird') ]
andschar
la source