Case Statement Equivalent in R


92

Ho una variabile in un dataframe in cui uno dei campi ha tipicamente 7-8 valori. Voglio collpase loro 3 o 4 nuove categorie all'interno di una nuova variabile all'interno del dataframe. Qual è l'approccio migliore?

Userei un'istruzione CASE se fossi in uno strumento simile a SQL ma non sono sicuro di come attaccarlo in R.

Qualsiasi aiuto che puoi fornire sarà molto apprezzato!


a) Sono interi, numerici, categoriali o stringa? dput()Pubblica un frammento di dati di esempio, utilizzando b) Vuoi una soluzione in base R, dplyr, data.table, tidyverse ...?
smci

Risposte:


39

case_when(), che è stato aggiunto a dplyr nel maggio 2016, risolve questo problema in modo simile a memisc::cases().

Per esempio:

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

A partire da 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"
  )
)

4
Non è necessario il .$davanti a ogni colonna.
kath

1
Sì, a partire da dplyr 0.7.0 (rilasciato il 9 giugno 2017), il file .$non è più necessario. All'epoca in cui questa risposta fu scritta originariamente, lo era.
Evan Cortens

ottima soluzione. se entrambe le affermazioni sono vere. Il secondo sovrascrive il primo?
JdP

1
@JdP Funziona proprio come CASE WHEN in SQL, quindi le istruzioni vengono valutate in ordine e il risultato è la prima istruzione TRUE. (Quindi nell'esempio sopra, ho inserito un VERO alla fine, che funge da valore predefinito.)
Evan Cortens,

Mi piace questa risposta perché, a differenza switch, ti permette di creare una sequenza di espressioni invece di chiavi per i casi.
Dannid

27

Dai un'occhiata alla casesfunzione dal memiscpacchetto. Implementa la funzionalità del case con due diversi modi per usarlo. Dagli esempi nel pacchetto:

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

dove xe ysono due vettori.

Riferimenti: pacchetto memisc , esempio di casi


24

Se lo hai, factorpuoi cambiare i livelli con il metodo 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

Potresti scrivere una semplice funzione come 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"))

2
Bella risposta. Ho dimenticato che potresti usare una lista come argomento per i livelli con il vecchio e il nuovo nome come quello; la mia soluzione dipende da uno che mantiene l'ordine dei livelli dritto, quindi è meglio in questo modo.
Aaron ha lasciato Stack Overflow il

Inoltre, dovrebbe xessere l'ultima riga changelevels?
Aaron ha lasciato Stack Overflow il

22

Ecco un modo per utilizzare la switchdichiarazione:

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

L'unico aspetto negativo di questo è che devi continuare a scrivere il nome della categoria ( animal, ecc.) Per ogni elemento. È sintatticamente più conveniente poter definire le nostre categorie come di seguito (vedi la domanda molto simile Come aggiungere una colonna in un data frame in R )

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

e vogliamo in qualche modo "invertire" questa mappatura. Scrivo la mia funzione invMap:

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

e quindi invertire la mappa sopra come segue:

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

E poi è facile usarlo per aggiungere la typecolonna nel data-frame:

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

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

19

Non vedo alcuna proposta per il "passaggio". Esempio di codice (eseguilo):

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

15

Imho, il codice più semplice e universale:

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

Mi piace questo metodo. Tuttavia, esiste un'implementazione "diversa" poiché in alcune circostanze sarebbe indispensabile
T.Fung,

2
@ T.Fung È possibile modificare la prima riga in y = 'else'. Gli elementi che non soddisfano ulteriori condizioni rimarranno invariati.
Gregory Demin il

7

C'è switchun'affermazione ma non riesco mai a farla funzionare come penso dovrebbe. Poiché non hai fornito un esempio, ne creerò uno utilizzando una variabile fattore:

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

Se specifichi le categorie che desideri in un ordine appropriato alla riassegnazione puoi utilizzare il fattore o le variabili numeriche come indice:

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" ...

In seguito ho appreso che esistono davvero due diverse funzioni dell'interruttore. Non è una funzione generica ma dovresti pensarla come switch.numerico switch.character. Se il tuo primo argomento è un "fattore" R, ottieni un switch.numericcomportamento che probabilmente causerà problemi, poiché la maggior parte delle persone vede i fattori visualizzati come caratteri e presume erroneamente che tutte le funzioni li elaboreranno come tali.


6

Puoi usare recode dal pacchetto dell'auto:

library(ggplot2) #get data
library(car)
daimons$new_var <- recode(diamonds$clarity , "'I1' = 'low';'SI2' = 'low';else = 'high';")[1:10]

11
Non posso supportare una funzione che analizza i suoi parametri dal testo
Hadley

Sì, ma sai se qualcuno ha scritto una versione migliore? sos::findFn("recode")reperti doBy::recodeVar, epicalc::recode, memisc::recode, ma non ho guardato in dettaglio ...
Ben Bolker

5

Non mi piace nessuno di questi, non sono chiari al lettore o al potenziale utente. Uso solo una funzione anonima, la sintassi non è liscia come un'istruzione case, ma la valutazione è simile a un'istruzione case e non è così dolorosa. questo presuppone anche che tu lo stia valutando all'interno di dove sono definite le tue variabili.

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

tutte quelle () sono necessarie per racchiudere e valutare la funzione anonima.


6
1) La parte funzionale non è necessaria; potresti semplicemente fare result <- (if (x==10 | y< 5) 'foo' else if (x==11 & y== 5) 'bar' ). 2) Funziona solo se xe ysono scalari; per i vettori, come nella domanda originale, ifelsesarebbero necessarie istruzioni annidate .
Aaron ha lasciato Stack Overflow il

4

Sto usando in quei casi a cui ti riferisci switch(). Sembra un'istruzione di controllo ma in realtà è una funzione. L'espressione viene valutata e in base a questo valore viene restituito l'elemento corrispondente nell'elenco.

switch funziona in due modi distinti a seconda che il primo argomento restituisca una stringa di caratteri o un numero.

Quello che segue è un semplice esempio di stringa che risolve il tuo problema di comprimere le vecchie categorie in nuove.

Per il formato stringa di caratteri, avere un singolo argomento senza nome come predefinito dopo i valori denominati.

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

3

Se vuoi avere una sintassi simile a sql puoi semplicemente usare sqldfpackage. La funzione da utilizzare è anche nomi sqldfe la sintassi è la seguente

sqldf(<your query in quotation marks>)

2

Una dichiarazione di caso in realtà potrebbe non essere l'approccio giusto qui. Se questo è un fattore, il che è probabile, è sufficiente impostare i livelli del fattore in modo appropriato.

Supponi di avere un fattore con le lettere dalla A alla E, come questo.

> 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

Per unire i livelli B e C e chiamarlo BC, basta cambiare i nomi di quei livelli in 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

Il risultato è quello desiderato.


2

Mixing plyr::mutate e dplyr::case_whenfunziona per me ed è leggibile.

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]

Punti bonus se la colonna può uscire da mutate come fattore invece di char! L'ultima riga dell'istruzione case_when, che cattura tutte le righe senza corrispondenza, è molto 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

2

È possibile utilizzare la basefunzione mergeper le attività di rimappatura in stile case:

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

1

A partire da data.table v1.13.0 è possibile utilizzare la funzione fcase()(fast-case) per eseguire CASEoperazioni simili a SQL (anch'esse simili a 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') ]
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.