Идиома для кодирования типа 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]