R中的Case语句等效语句

106

我在一个数据框中有一个变量,其中一个字段通常有7-8个值。我想将它们合并为3或4个新类别,并将其放入数据框中的一个新变量中。什么是最好的方法?

如果我在像SQL这样的工具中,我会使用CASE语句,但不确定如何在R中解决这个问题。

非常感谢您能提供的任何帮助!

17个回答

58

case_when() 函数是在 2016 年 5 月加入到 dplyr 中的,它解决了一个类似于 memisc::cases() 的问题。

例如,在 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"
  )
)

原始回答

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

5
每列前面不需要加上 .$ - kath
2
是的,从dplyr 0.7.0版本开始(发布于2017年6月9日),不再需要使用.$。在本回答最初编写时,这是必需的。 - Evan Cortens
很棒的解决方案。如果两个语句都为真,第二个语句会覆盖第一个吗? - JdP
1
@JdP 它的工作原理就像SQL中的CASE WHEN一样,因此语句按顺序评估,结果是第一个TRUE语句。(因此在上面的示例中,我在最后放了一个TRUE,它作为默认值。) - Evan Cortens
我喜欢这个答案,因为与 switch 不同,它允许你创建一个表达式序列而不是键来作为各种情况的条件。 - Dannid

31

下面是使用 switch 语句的方法:

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

这种方法的唯一缺点是您需要为每个项目都编写类别名称(animal等)。从语法上讲,更方便的方法是将我们的类别定义如下(请参见非常相似的问题如何在R中向数据框添加一列)

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

我们希望以某种方式"反转"这个映射。我编写了自己的invMap函数:

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

然后将上面的映射反转,方法如下:

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

然后就可以轻松使用这个方法在数据框中添加type列:

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

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

这真的很简单和直接,谢谢你。 - Juano

31

请查看memisc包中的cases函数,它提供了两种不同的用法实现case功能。 从该包的示例中:

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

其中xy是两个向量。

参考文献:memisc包cases示例


30

我没有看到“switch”的建议。以下是一个代码示例(可运行):

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

26

如果你有factor,那么你可以通过标准方法改变水平:

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
您可以编写一个简单的函数作为包装器:
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
好答案。我忘了你可以像这样使用列表作为levels的参数,包括旧名称和新名称;我的解决方案依赖于保持levels顺序的正确性,所以从这个角度来看,这种方法更好。 - Aaron left Stack Overflow
还有,最后一行的 x 应该改为 changelevels 吗? - Aaron left Stack Overflow

17

在我看来,最简单通用的代码:

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

我喜欢这种方法。但是,在某些情况下,是否有“否则”实现是不可或缺的? - Sweepy Dodo
3
你可以将第一行改为y = 'else'。不符合任何进一步条件的元素将保持不变。 - Gregory Demin

10

有一个switch语句,但我似乎从来无法让它按照我想的方式工作。由于您未提供示例,我将使用因子变量创建一个示例:

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

如果你按照重新指定的适当顺序指定要使用的类别,那么可以将因子或数值变量用作索引:

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

后来我了解到,其实有两个不同的switch函数。它不是通用函数,而是应该将其视为switch.numericswitch.character。如果您的第一个参数是R '因子',则会得到switch.numeric行为,这很可能会导致问题,因为大多数人看到因子显示为字符并错误地假设所有函数都会将它们处理为字符。


8
我在这些情况下使用你所提到的switch()。它看起来像一个控制语句,但实际上它是一个函数。表达式被评估并基于这个值,返回列表中对应的项。

switch有两种不同的方式,取决于第一个参数是字符串还是数字。

下面是一个简单的字符串示例,解决了将旧类别折叠到新类别的问题。

对于字符串形式,在命名值之后有一个未命名的默认参数。

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

6
您可以使用car软件包中的recode功能:
library(ggplot2) #get data
library(car)
daimons$new_var <- recode(diamonds$clarity , "'I1' = 'low';'SI2' = 'low';else = 'high';")[1:10]

11
我无法支持一个从文本中解析其参数的函数。 - hadley
是的,但你知道是否有人写了更好的版本吗?sos::findFn("recode")可以找到doBy::recodeVarepicalc::recodememisc::recode,但我还没有详细查看它们... - Ben Bolker

5
我不喜欢这些选项,它们对读者或潜在用户来说都不够清晰。我只是使用匿名函数,语法不像case语句那样流畅,但计算方式类似于case语句,并且并不痛苦。这也假设您在定义变量的范围内进行评估。
result <- ( function() { if (x==10 | y< 5) return('foo') 
                         if (x==11 & y== 5) return('bar')
                        })()

所有这些()都是必要的,用于封装和评估匿名函数。

7
1)“function”部分是不必要的;您可以这样写:result <- (if (x==10 | y< 5) 'foo' else if (x==11 & y== 5) 'bar' )。 2)这仅适用于 xy 是标量;对于向量,就像在原始问题中一样,需要嵌套 ifelse 语句。 - Aaron left Stack Overflow

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接