多类别ifelse风格重编码的成语

17
我经常遇到这种情况,所以我认为一定有一个很好的习语来描述它。假设我有一个数据框架,其中包括“产品”等多个属性。我还有一个键,将产品转换为品牌+尺寸。产品代码1-3是泰诺,4-6是阿德维尔,7-9是拜耳,10-12是通用药品。
编写此代码的最快(从人类时间的角度)方法是什么?
如果分类少于3个,我倾向于使用嵌套的ifelse,并在分类超过3个时键入数据表并进行合并。还有更好的想法吗?Stata有一个recode命令,非常适合这种情况,尽管我认为它会促进数据-代码混合得太多。
dat <- structure(list(product = c(11L, 11L, 9L, 9L, 6L, 1L, 11L, 5L, 
7L, 11L, 5L, 11L, 4L, 3L, 10L, 7L, 10L, 5L, 9L, 8L)), .Names = "product", row.names = c(NA, 
-20L), class = "data.frame")

1
这里展示了许多SO的创意。很难选择一个答案。 - Ari B. Friedman
13个回答

19

你可以将变量转换为因子,并使用levels<-函数更改其水平。一条命令可以写成:

`levels<-`(
    factor(dat$product),
    list(Tylenol=1:3, Advil=4:6, Bayer=7:9, Generic=10:12)
)

步骤如下:

brands <- factor(dat$product)
levels(brands) <- list(Tylenol=1:3, Advil=4:6, Bayer=7:9, Generic=10:12)

这绝对是最简单的方法,尽管你第一次调用levels<-可能会让很多人感到困惑。 :) - Joshua Ulrich
2
好的快捷方式!我在这里找到了它的解释:[链接](https://dev59.com/Xmkv5IYBdhLWcg3wpCbJ) - nassimhddd

14

可以使用列表作为关联数组来定义 品牌 -> 产品代码 映射,例如:

brands <- list(Tylenol=1:3, Advil=4:6, Bayer=7:9, Generic=10:12)

有了这个之后,你可以将其反转以创建一个 产品代码 -> 品牌 列表(可能会占用大量内存),或者只需使用搜索功能:

find.key <- function(x, li, default=NA) {
    ret <- rep.int(default, length(x))
    for (key in names(li)) {
        ret[x %in% li[[key]]] <- key
    }
    return(ret)
}

我相信有更好的编写此函数的方式(for循环让我感到困扰!),但至少它是矢量化的,因此仅需要一次通过列表。

使用它应该像这样:

> dat$brand <- find.key(dat$product, brands)
> dat
   product   brand
1       11 Generic
2       11 Generic
3        9   Bayer
4        9   Bayer
5        6   Advil
6        1 Tylenol
7       11 Generic
8        5   Advil
9        7   Bayer
10      11 Generic
11       5   Advil
12      11 Generic
13       4   Advil
14       3 Tylenol
15      10 Generic
16       7   Bayer
17      10 Generic
18       5   Advil
19       9   Bayer
20       8   Bayer

recodelevels<- 的解决方案非常好,但它们的速度也比这个解决方案慢得多(一旦你有了find.key,这比recode更容易阅读,并且与levels<-相当):

> microbenchmark(
     recode=recode(dat$product,recodes="1:3='Tylenol';4:6='Advil';7:9='Bayer';10:12='Generic'"), 
     find.key=find.key(dat$product, brands),
     levels=`levels<-`(factor(dat$product),brands))
Unit: microseconds
      expr      min        lq    median        uq      max
1 find.key   64.325   69.9815   76.8950   83.8445  221.748
2   levels  240.535  248.1470  274.7565  306.8490 1477.707
3   recode 1636.039 1683.4275 1730.8170 1855.8320 3095.938

(我无法正确对switch版本进行基准测试,但它似乎比上述所有解决方案都更快,尽管它甚至比recode解决方案更不利于人类。)


为什么不呢?find.key是一个通用函数,你可以将其复制粘贴到你的代码中并使用。 - huon
新版本看起来非常易于使用。而旧版本则不是这样的:cbind(dat,dat$brand brand=find<- find.key(dat$product, brands))。但是现在我看一眼,发现它也不复杂。早上的糊涂 :-) - Ari B. Friedman
我仍然喜欢这个函数,但我刚刚注意到它会用 NA 替换掉不在 li 列表中的任何内容。 - Ari B. Friedman
@AriB.Friedman,这就是default参数的控制:find.key(c(1,13), brands, default="missing") => [1] "Tylenol" "missing" - huon
@dbaupp 我在想像这样 default=dat$product,它会插入没有标签的特定产品代码。但现在我想想,最好返回 NA 并在事后用代码填充缺失值。 - Ari B. Friedman
显示剩余4条评论

13

我喜欢car包中的recode函数:

library(car)

dat$brand <- recode(dat$product,
  recodes="1:3='Tylenol';4:6='Advil';7:9='Bayer';10:12='Generic'")

# > dat
#    product   brand
# 1       11 Generic
# 2       11 Generic
# 3        9   Bayer
# 4        9   Bayer
# 5        6   Advil
# 6        1 Tylenol
# 7       11 Generic
# 8        5   Advil
# 9        7   Bayer
# 10      11 Generic
# 11       5   Advil
# 12      11 Generic
# 13       4   Advil
# 14       3 Tylenol
# 15      10 Generic
# 16       7   Bayer
# 17      10 Generic
# 18       5   Advil
# 19       9   Bayer
# 20       8   Bayer

9
“recode” 的唯一问题在于它是通过处理字符串来实现的,因此如果您的代码/数据中恰好含有分号和等号,那么这将会是一个大难题... - Ben Bolker

8
我经常使用以下技巧:

我经常使用以下技巧:

key <- c()
key[1:3] <- "Tylenol"
key[4:6] <- "Advil"
key[7:9] <- "Bayer"
key[10:12] <- "Generic"

那么,

> key[dat$product]
 [1] "Generic" "Generic" "Bayer"   "Bayer"   "Advil"   "Tylenol" "Generic" "Advil"   "Bayer"   "Generic"
[11] "Advil"   "Generic" "Advil"   "Tylenol" "Generic" "Bayer"   "Generic" "Advil"   "Bayer"   "Bayer"  

7
"数据库方法"是将产品密钥的定义保存在单独的表格(数据框架)中。这样做更有意义,因为您说产品密钥不仅可以转换成品牌,还可以转换成大小。"
product.keys <- read.table(textConnection("

product brand   size
1       Tylenol small
2       Tylenol medium
3       Tylenol large
4       Advil   small
5       Advil   medium
6       Advil   large
7       Bayer   small
8       Bayer   medium
9       Bayer   large
10      Generic small
11      Generic medium
12      Generic large

"), header = TRUE)

然后,您可以使用merge将数据合并:

merge(dat, product.keys, by = "product")
#    product   brand   size
# 1        1 Tylenol  small
# 2        3 Tylenol  large
# 3        4   Advil  small
# 4        5   Advil medium
# 5        5   Advil medium
# 6        5   Advil medium
# 7        6   Advil  large
# 8        7   Bayer  small
# 9        7   Bayer  small
# 10       8   Bayer medium
# 11       9   Bayer  large
# 12       9   Bayer  large
# 13       9   Bayer  large
# 14      10 Generic  small
# 15      10 Generic  small
# 16      11 Generic medium
# 17      11 Generic medium
# 18      11 Generic medium
# 19      11 Generic medium
# 20      11 Generic medium

注意,merge函数无法保留行的顺序。如果这是个问题,可以使用plyr包中的join函数来保留顺序:

library(plyr)
join(dat, product.keys, by = "product")
#    product   brand   size
# 1       11 Generic medium
# 2       11 Generic medium
# 3        9   Bayer  large
# 4        9   Bayer  large
# 5        6   Advil  large
# 6        1 Tylenol  small
# 7       11 Generic medium
# 8        5   Advil medium
# 9        7   Bayer  small
# 10      11 Generic medium
# 11       5   Advil medium
# 12      11 Generic medium
# 13       4   Advil  small
# 14       3 Tylenol  large
# 15      10 Generic  small
# 16       7   Bayer  small
# 17      10 Generic  small
# 18       5   Advil medium
# 19       9   Bayer  large
# 20       8   Bayer medium

如果您的表格很大而且速度很重要,考虑使用data.tables(来自data.table包)而不是data.frames。


merge函数中没有一个,sort=FALSE选项可以保留行的顺序吗? - Ari B. Friedman

6

如果您确实有一个庞大的数据集,那么这个可能是比较好的方法,虽然需要输入一些内容。Bryangoodrich和Dason在talkstats.com教给了我这个方法。它使用哈希表或创建包含查找表的环境。我实际上会将此函数(哈希函数)保留在我的.Rprofile文件中,用于字典查找。

我复制了您的数据1000次,使其变得更大一些。

#################################################
# THE HASH FUNCTION (CREATES A ENW ENVIRONMENT) #
#################################################
hash <- function(x, type = "character") {
    e <- new.env(hash = TRUE, size = nrow(x), parent = emptyenv())
    char <- function(col) assign(col[1], as.character(col[2]), envir = e)
    num <- function(col) assign(col[1], as.numeric(col[2]), envir = e)
    FUN <- if(type=="character") char else num
    apply(x, 1, FUN)
    return(e)
}
###################################
# YOUR DATA REPLICATED 1000 TIMES #
###################################
dat <- dat <- structure(list(product = c(11L, 11L, 9L, 9L, 6L, 1L, 11L, 5L, 
    7L, 11L, 5L, 11L, 4L, 3L, 10L, 7L, 10L, 5L, 9L, 8L)), .Names = "product", row.names = c(NA, 
    -20L), class = "data.frame")
dat <- dat[rep(seq_len(nrow(dat)), 1000), , drop=FALSE]
rownames(dat) <-NULL
dat
#########################
# CREATE A LOOKUP TABLE #
#########################
med.lookup <- data.frame(val=as.character(1:12), 
    med=rep(c('Tylenol', 'Advil', 'Bayer', 'Generic'), each=3))  

########################################
# USE hash TO CREATE A ENW ENVIRONMENT #
########################################  
meds <- hash(med.lookup)  

##############################
# CREATE A RECODING FUNCTION #
##############################          
recoder <- function(x){
    x <- as.character(x) #turn the numbers to character
    rc <- function(x){
       if(exists(x, env = meds))get(x, e = meds) else NA 
    }  
    sapply(x, rc, USE.NAMES = FALSE) 
}
#############
# HASH AWAY #
#############
recoder(dat[, 1])    

在这种情况下,哈希速度较慢,但如果您需要更多的重新编码级别,则速度会超过其他方法。


3

比嵌套的ifelse更易读:

unlist(lapply(as.character(dat$product), switch,
              `1`=,`2`=,`3`='tylenol',
              `4`=,`5`=,`6`='advil',
              `7`=,`8`=,`9`='bayer',
              `10`=,`11`=,`12`='generic'))

注意:不是很高效。


2
我倾向于使用这个函数:
recoder <- function (x, from = c(), to = c()) {
  missing.levels <- unique(x)
  missing.levels <- missing.levels[!missing.levels %in% from]
  if (length(missing.levels) > 0) {
    from <- append(x = from, values = missing.levels)
    to <- append(x = to, values = missing.levels)
  }
  to[match(x, from)]
}

就像这样:

recoder(x = dat$product, from = 1:12, to = c(rep("Product1", 3), rep("Product2", 3), rep("Product3", 3), rep("Product4", 3)))

1
如果您的代码以顺序组的形式存在,就像示例中一样,那么这可能会“切中要害”:cut
cut(dat$product,seq(0,12,by=3),labels=c("Tylenol","Advil","Bayer","Generic"))
 [1] Generic Generic Bayer   Bayer   Advil   Tylenol Generic Advil   Bayer  
[10] Generic Advil   Generic Advil   Tylenol Generic Bayer   Generic Advil  
[19] Bayer   Bayer  
Levels: Tylenol Advil Bayer Generic

0

另一种在这种情况下可行的版本:

c("Tylenol","Advil","Bayer","Generic")[(dat$product %/% 3.1) + 1]

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