Идиома для кодирования типа ifelse для нескольких категорий

Я часто сталкиваюсь с этим настолько, что считаю, что для него должна быть хорошая идиома. Предположим, у меня есть data.frame с кучей атрибутов, включая "продукт". У меня также есть ключ, который переводит продукты на бренд + размер. Код продукта 1-3 - Tylenol, 4-6 - Advil, 7-9 - Bayer, 10-12 - общие.

Какой самый быстрый (с точки зрения человеческого времени) способ кодирования этого?

Я имею тенденцию использовать вложенные ifelse, если есть 3 или меньше категорий, и введите таблицу данных и объедините их, если их больше 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

Можно использовать список как ассоциативный массив для определения отображения brand -> product code, т.е.:

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

После этого вы можете либо инвертировать это, чтобы создать список product code -> brand (может занимать много памяти), либо просто использовать функцию поиска:

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

Решения recode и levels<- очень приятны, но они также значительно медленнее, чем эта (и если у вас есть 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.)

Ответ 2

Вы можете преобразовать свою переменную в коэффициент и изменить ее уровни с помощью функции 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)

Ответ 3

Мне нравится функция recode в пакете car:

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

Ответ 4

Я часто использую следующую технику:

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"  

Ответ 5

"Подход базы данных" заключается в том, чтобы сохранить отдельную таблицу (data.frame) для определений ваших ключей продукта. Это имеет еще больший смысл, поскольку вы говорите, что ваши ключи продукта переходят не только на бренд, но и на размер:

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.

Ответ 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])    

В этом случае хеширование происходит медленно, но если у вас есть больше уровней для перекодирования, то он будет увеличиваться в скорости по сравнению с другими.

Ответ 7

Несколько более читаемый, чем вложенный ifelse:

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

Предостережение: не очень эффективно.

Ответ 8

Я использую эту функцию:

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

Ответ 9

Если у вас есть коды в последовательных группах, как в примере, это может 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

Ответ 10

Там также arules:discretize, но мне он меньше, потому что он заставляет вас отделять метки от диапазона значений:

library(arules)
discretize( dat$product, method = "fixed", categories = c( 1,3,6,9,12 ), labels = c("Tylenol","Advil","Bayer","Generic") )

[1] Generic Generic Generic Generic Bayer   Tylenol Generic Advil   Bayer   Generic Advil   Generic Advil   Advil   Generic Bayer   Generic Advil   Generic Bayer  
Levels: Tylenol Advil Bayer Generic

Ответ 11

Для полноты (и, возможно, самого быстрого и простого решения) можно создать и назвать вектор и использовать его для поиска. Кредит: http://adv-r.had.co.nz/Subsetting.html#applications

product.code <- c(1='Tylenol', 2='Tylenol', 3='Tylenon', 4='Advil', 5 ='Advil', 6='Advil', 7='Bayer', 8='Bayer', 9='Bayer', 10='Generic', 11='Generic', 12='Generic')

Чтобы получить выход

$unname(product.code[dat$product])

Настольная маркировка для скорости с верхними решениями

$microbenchmark(
 named_vector = unname(product.code[dat$product]), 
 find.key = find.key(dat$product, brands),
 levels = 'levels<-'(factor(dat$product),brands))
Unit: microseconds
         expr     min       lq      mean   median       uq     max neval
 named_vector  11.777  20.4810  26.12832  23.0410  28.1610 207.360   100
     find.key  34.305  55.8090  58.75804  59.1370  65.5370 130.049   100
       levels 143.361 224.7685 234.02545 247.5525 255.7445 338.944   100

Это решение очень похоже на решение @kohske, но будет работать для не численного поиска.

Ответ 12

Другая версия, которая будет работать в этом случае:

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