Declaração de Caso Equivalente em R

87

Eu tenho uma variável em um dataframe onde um dos campos normalmente tem 7 a 8 valores. Quero agrupar 3 ou 4 novas categorias em uma nova variável dentro do dataframe. Qual é a melhor abordagem?

Eu usaria uma instrução CASE se estivesse em uma ferramenta semelhante a SQL, mas não tivesse certeza de como atacar isso em R.

Qualquer ajuda que você puder fornecer será muito apreciada!

Btibert3
fonte
a) São inteiros, numéricos, categóricos ou string? Por favor, poste fragmento de dados de exemplo, usando dput()b) Você quer uma solução em base R, dplyr, data.table, tidyverse ...?
smci

Respostas:

38

case_when(), que foi adicionado ao dplyr em maio de 2016, resolve esse problema de maneira semelhante a memisc::cases().

Por exemplo:

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

Desde 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
fonte
4
Você não precisa do .$na frente de cada coluna.
kath
1
Sim, a partir do dplyr 0.7.0 (lançado em 9 de junho de 2017), o .$não é mais necessário. Na época em que essa resposta foi escrita originalmente, ela estava.
Evan Cortens
ótima solução. se ambas as afirmações forem verdadeiras. O segundo está substituindo o primeiro?
JdP de
1
@JdP Funciona exatamente como CASE WHEN em SQL, portanto, as instruções são avaliadas em ordem e o resultado é a primeira instrução TRUE. (Portanto, no exemplo acima, coloquei um TRUE no final, que serve como um valor padrão.)
Evan Cortens
Gosto desta resposta porque, ao contrário switch, permite criar uma sequência de expressões em vez de chaves para os casos.
Dannid
27

Dê uma olhada na casesfunção do memiscpacote. Ele implementa a funcionalidade de caso com duas maneiras diferentes de usá-la. Dos exemplos do pacote:

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

onde xe ysão dois vetores.

Referências: pacote memisc , exemplo de casos

Henrico
fonte
23

Se você conseguiu factor, você pode alterar os níveis pelo método padrão:

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

Você pode escrever uma função simples como um 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
fonte
1
Boa resposta. Esqueci que você poderia usar uma lista como argumento para nivelar o antigo e o novo nomes assim; minha solução depende de manter a ordem dos níveis em linha reta, então é melhor assim.
Aaron saiu do Stack Overflow de
Além disso, deve estar xna última linha changelevels?
Aaron saiu do Stack Overflow de
20

Esta é uma maneira de usar a switchinstrução:

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

A única desvantagem disso é que você precisa continuar escrevendo o nome da categoria ( animaletc.) para cada item. É sintaticamente mais conveniente ser capaz de definir nossas categorias como a seguir (veja a pergunta muito semelhante Como adicionar uma coluna em um quadro de dados em R )

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

e queremos de alguma forma "inverter" esse mapeamento. Eu escrevo minha própria função invMap:

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

e, em seguida, inverta o mapa acima da seguinte maneira:

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

E então é fácil usar isso para adicionar a typecoluna no data-frame:

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

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

Não vejo nenhuma proposta para 'troca'. Exemplo de código (execute-o):

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

Imho, código mais simples e universal:

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'
})
Gregory Demin
fonte
Eu gosto desse método. No entanto, existe uma implementação 'else', pois em algumas circunstâncias isso seria indispensável
T.Fung
2
@ T.Fung Você pode alterar a primeira linha para y = 'else'. Os elementos que não atendem a nenhuma outra condição permanecerão inalterados.
Gregory Demin
7

Existe uma switchdeclaração, mas nunca consigo fazê-la funcionar da maneira que acho que deveria. Como você não forneceu um exemplo, farei um usando uma variável de fator:

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

Se você especificar as categorias que deseja em uma ordem apropriada para a reatribuição, você pode usar o fator ou as variáveis ​​numéricas como um índice:

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

Mais tarde, aprendi que realmente existem duas funções de switch diferentes. Não é uma função genérica, mas você deve pensar nela como switch.numericou switch.character. Se seu primeiro argumento for um 'fator' R, você obterá um switch.numericcomportamento, que provavelmente causará problemas, uma vez que a maioria das pessoas vê os fatores exibidos como caracteres e faz a suposição incorreta de que todas as funções os processarão como tais.

IRTFM
fonte
6

Você pode usar recode do pacote do carro:

library(ggplot2) #get data
library(car)
daimons$new_var <- recode(diamonds$clarity , "'I1' = 'low';'SI2' = 'low';else = 'high';")[1:10]
Ian Fellows
fonte
11
Eu simplesmente não consigo suportar uma função que analisa seus parâmetros de texto
hadley
Sim, mas você sabe se alguém escreveu uma versão melhor? sos::findFn("recode")achados doBy::recodeVar, epicalc::recode, memisc::recode, mas eu não olhei para eles em detalhes ...
Ben Bolker
5

Eu não gosto de nenhum desses, eles não são claros para o leitor ou o usuário potencial. Eu apenas uso uma função anônima, a sintaxe não é tão engenhosa quanto uma declaração de caso, mas a avaliação é semelhante a uma declaração de caso e não é tão dolorosa. isso também pressupõe que você o avalie dentro de onde suas variáveis ​​são definidas.

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

todos esses () são necessários para incluir e avaliar a função anônima.

JamesM
fonte
6
1) A parte da função é desnecessária; você poderia apenas fazer result <- (if (x==10 | y< 5) 'foo' else if (x==11 & y== 5) 'bar' ). 2) Isso só funciona se xe yforem escalares; para vetores, como na pergunta original, ifelseinstruções aninhadas seriam necessárias.
Aaron saiu do Stack Overflow de
4

Estou usando nesses casos que você está se referindo switch(). Parece uma instrução de controle, mas na verdade, é uma função. A expressão é avaliada e com base neste valor, o item correspondente na lista é retornado.

switch funciona de duas maneiras distintas, dependendo se o primeiro argumento é avaliado como uma string de caracteres ou um número.

O que se segue é um exemplo de string simples que resolve seu problema de transformar categorias antigas em novas.

Para a forma de sequência de caracteres, tenha um único argumento sem nome como padrão após os valores nomeados.

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

Se você quiser ter uma sintaxe semelhante ao sql, você pode apenas usar o sqldfpacote. A função a ser usada também é nomes sqldfe a sintaxe é a seguinte

sqldf(<your query in quotation marks>)
Kuba
fonte
2

Uma declaração de caso, na verdade, pode não ser a abordagem certa aqui. Se este for um fator, o que provavelmente é, apenas defina os níveis do fator de forma adequada.

Digamos que você tenha um fator com as letras de A a E, assim.

> 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

Para ingressar nos níveis B e C e nomeá-lo BC, basta alterar os nomes desses níveis para 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

O resultado é o desejado.

Aaron saiu do Stack Overflow
fonte
2

Mistura plyr::mutate e dplyr::case_whenfunciona para mim e é legível.

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]

Pontos de bônus se a coluna puder sair da mutação como um fator ao invés de char! A última linha da instrução case_when, que captura todas as linhas não correspondidas, é muito 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
בנימן הגלילי
fonte
2

Você pode usar a basefunção mergepara tarefas de remapeamento de estilo de caso:

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

A partir de data.table v1.13.0, você pode usar a função fcase()(caso rápido) para fazer CASEoperações semelhantes a SQL (também semelhantes 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') ]
andschar
fonte