recodificar - Idioma para la recodificación de estilo ifelse para múltiples categorías
transformar variables en r (12)
Me encuentro con esto con la frecuencia suficiente para darme cuenta de que tiene que haber un buen lenguaje para ello. Supongamos que tengo un data.frame con un montón de atributos, incluido "producto". También tengo una llave que traduce los productos a la marca + tamaño. Los códigos de producto 1-3 son Tylenol, 4-6 son Advil, 7-9 son Bayer, 10-12 son genéricos.
¿Cuál es la forma más rápida (en términos de tiempo humano) de codificar esto?
Tiendo a usar los ifelse
anidados si hay 3 o menos categorías, y escribo la tabla de datos y la fusiono si hay más de 3. ¿Alguna idea mejor? Stata tiene un comando de recode
que es bastante ingenioso para este tipo de cosas, aunque creo que promueve la mezcla de códigos de datos un poco demasiado.
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")
A menudo utilizo la técnica a continuación:
key <- c()
key[1:3] <- "Tylenol"
key[4:6] <- "Advil"
key[7:9] <- "Bayer"
key[10:12] <- "Generic"
Entonces,
> 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"
Algo más legible que los ifelse
anidados:
unlist(lapply(as.character(dat$product), switch,
`1`=,`2`=,`3`=''tylenol'',
`4`=,`5`=,`6`=''advil'',
`7`=,`8`=,`9`=''bayer'',
`10`=,`11`=,`12`=''generic''))
Advertencia: no es muy eficiente.
El "enfoque de base de datos" es mantener una tabla separada (un data.frame) para las definiciones de las claves de su producto. Tiene más sentido ya que dice que las claves de sus productos se traducen no solo en una marca, sino también en un tamaño:
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)
Luego, puedes unir tus datos usando 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
Como puede observar, el orden de las filas no se conserva mediante la merge
. Si esto es un problema, el paquete plyr
tiene una función de join
que mantiene el orden:
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
Finalmente, si sus tablas son grandes y la velocidad es un problema, considere usar data.tables (del paquete data.table
) en lugar de data.frames.
Este requiere algunos tipos de escritura, pero si realmente tiene un gran conjunto de datos, este puede ser el camino a seguir. Bryangoodrich y Dason en talkstats.com me enseñaron este. Está utilizando una tabla hash o creando un entorno que contiene una tabla de consulta. De hecho, mantengo este en mi. Perfil (la función hash que es) para consultas de tipo de diccionario.
Repliqué tus datos 1000 veces para hacerlos un poco más grandes.
#################################################
# 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])
En este caso, el hashing es lento, pero si tiene más niveles para recodificar, aumentará la velocidad sobre otros.
Me gusta la función de recode
en el paquete de 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
Otra versión, que funcionaría en este caso:
c("Tylenol","Advil","Bayer","Generic")[(dat$product %/% 3.1) + 1]
Para completar (y, probablemente, la solución más rápida y sencilla), se puede crear un vector denominado y usarlo para la búsqueda. Crédito: 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'')
Para obtener la salida
$unname(product.code[dat$product])
Bench-mark para velocidad con las mejores soluciones.
$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
Esta solución es muy similar a la solución de @Kohske pero funcionaría para búsquedas no numéricas.
Podría convertir su variable en un factor y cambiar sus niveles por levels<-
función. En una orden podría ser como:
`levels<-`(
factor(dat$product),
list(Tylenol=1:3, Advil=4:6, Bayer=7:9, Generic=10:12)
)
En pasos:
brands <- factor(dat$product)
levels(brands) <- list(Tylenol=1:3, Advil=4:6, Bayer=7:9, Generic=10:12)
Si tiene códigos en grupos secuenciales como en el ejemplo, esto puede cut
la mostaza:
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
También hay arules:discretize
, pero me gusta menos porque te hace separar las etiquetas del rango de valores:
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
Tiendo a usar esta función:
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)]
}
Como en:
recoder(x = dat$product, from = 1:12, to = c(rep("Product1", 3), rep("Product2", 3), rep("Product3", 3), rep("Product4", 3)))
Uno podría usar una lista como una matriz asociativa para definir la brand -> product code
mapeo de brand -> product code
, es decir:
brands <- list(Tylenol=1:3, Advil=4:6, Bayer=7:9, Generic=10:12)
Una vez que tenga esto, puede invertirlo para crear un product code -> brand
lista de product code -> brand
(podría necesitar mucha memoria), o simplemente usar una función de búsqueda:
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)
}
Estoy seguro de que hay mejores formas de escribir esta función (¡el bucle for
me está molestando!), Pero al menos está vectorizado, por lo que solo se requiere un solo paso a través de la lista.
Usarlo sería algo como:
> 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
Las soluciones de recode
y levels<-
son muy agradables, pero también son mucho más lentas que esta (y una vez que hayas encontrado la find.key
es más fácil para los humanos que recode
y estar a la par con los 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
(No puedo lograr que la versión del switch
realice una evaluación comparativa correctamente, pero parece ser más rápida que todas las anteriores, aunque es aún peor para los humanos que la solución de recode
).