`niveles <-` (¿Qué hechicería es esta?

114

En respuesta a otra pregunta, @Marek publicó la siguiente solución: https://stackoverflow.com/a/10432263/636656

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

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

Que produce como salida:

 [1] Generic Generic Bayer   Bayer   Advil   Tylenol Generic Advil   Bayer   Generic Advil   Generic Advil   Tylenol
[15] Generic Bayer   Generic Advil   Bayer   Bayer  

Esto es solo la impresión de un vector, por lo que para almacenarlo puede hacer lo que es aún más confuso:

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

Claramente, esta es una especie de llamada a la función de niveles, pero no tengo idea de lo que se está haciendo aquí. ¿Cuál es el término para este tipo de hechicería y cómo puedo aumentar mi habilidad mágica en este dominio?

Ari B. Friedman
fuente
1
También hay names<-y [<-.
huon
1
Además, me pregunté sobre esto en la otra pregunta, pero no pregunté: ¿hay alguna razón para la structure(...)construcción en lugar de solo data.frame(product = c(11L, 11L, ..., 8L))? (¡Si hay algo de magia sucediendo allí, me gustaría
usarlo
2
Es una llamada a la "levels<-"función:, una function (x, value) .Primitive("levels<-")especie de como X %in% Yes una abreviatura de "%in%"(X, Y).
BenBarnes
2
@dbaupp Muy útil para ejemplos reproducibles: stackoverflow.com/questions/5963269/…
Ari B. Friedman
8
No tengo idea de por qué alguien votó para cerrar esto por no ser constructivo. La Q tiene una respuesta muy clara: ¿cuál es el significado de la sintaxis utilizada en el ejemplo y cómo funciona esto en R?
Gavin Simpson

Respuestas:

104

Las respuestas aquí son buenas, pero les falta un punto importante. Déjame intentar describirlo.

R es un lenguaje funcional y no le gusta mutar sus objetos. Pero sí permite declaraciones de asignación, usando funciones de reemplazo:

levels(x) <- y

es equivalente a

x <- `levels<-`(x, y)

El truco es que esta reescritura se realiza mediante <-; no lo hace levels<-. levels<-es solo una función regular que toma una entrada y da una salida; no muta nada.

Una consecuencia de eso es que, según la regla anterior, <-debe ser recursiva:

levels(factor(x)) <- y

es

factor(x) <- `levels<-`(factor(x), y)

es

x <- `factor<-`(x, `levels<-`(factor(x), y))

Es hermoso que esta transformación puramente funcional (hasta el final, donde ocurre la asignación) sea equivalente a lo que sería una asignación en un lenguaje imperativo. Si mal no recuerdo, esta construcción en lenguajes funcionales se llama lente.

Pero luego, una vez que ha definido funciones de reemplazo como levels<-, obtiene otra ganancia inesperada: no solo tiene la capacidad de hacer asignaciones, tiene una función útil que toma un factor y da otro factor con diferentes niveles. ¡Realmente no hay nada de "asignación" al respecto!

Entonces, el código que estás describiendo solo hace uso de esta otra interpretación de levels<-. Admito que el nombre levels<-es un poco confuso porque sugiere una tarea, pero esto no es lo que está sucediendo. El código simplemente está configurando una especie de canalización:

  • Empezar con dat$product

  • Conviértelo en un factor

  • Cambiar los niveles

  • Almacene eso en res

Personalmente, creo que esa línea de código es hermosa;)

Owen
fuente
33

Sin brujería, así es como se definen las funciones de (sub) asignación. levels<-es un poco diferente porque es una primitiva (sub) asignar los atributos de un factor, no los elementos en sí. Hay muchos ejemplos de este tipo de función:

`<-`              # assignment
`[<-`             # sub-assignment
`[<-.data.frame`  # sub-assignment data.frame method
`dimnames<-`      # change dimname attribute
`attributes<-`    # change any attributes

Otros operadores binarios también se pueden llamar así:

`+`(1,2)  # 3
`-`(1,2)  # -1
`*`(1,2)  # 2
`/`(1,2)  # 0.5

Ahora que lo sabes, algo como esto realmente debería volar tu mente:

Data <- data.frame(x=1:10, y=10:1)
names(Data)[1] <- "HI"              # How does that work?!? Magic! ;-)
Joshua Ulrich
fuente
1
¿Puede explicar un poco más sobre cuándo tiene sentido llamar a funciones de esa forma, en lugar de la forma habitual? Estoy trabajando en el ejemplo de @ Marek en la pregunta vinculada, pero ayudaría tener una explicación más explícita.
Drew Steen
4
@DrewSteen: por razones de claridad / legibilidad del código, diría que nunca tiene sentido porque `levels<-`(foo,bar)es lo mismo que levels(foo) <- bar. Usando el ejemplo de @ Marek: `levels<-`(as.factor(foo),bar)es lo mismo que foo <- as.factor(foo); levels(foo) <- bar.
Joshua Ulrich
Buena lista. ¿No crees que en levels<-realidad es solo una abreviatura de attr<-(x, "levels") <- value, o al menos probablemente lo fue hasta que se convirtió en una primitiva y se entregó al código C?
IRTFM
30

La razón de esa "magia" es que el formulario de "asignación" debe tener una variable real sobre la que trabajar. Y el factor(dat$product)no fue asignado a nada.

# This works since its done in several steps
x <- factor(dat$product)
levels(x) <- list(Tylenol=1:3, Advil=4:6, Bayer=7:9, Generic=10:12)
x

# This doesn't work although it's the "same" thing:
levels(factor(dat$product)) <- list(Tylenol=1:3, Advil=4:6, Bayer=7:9, Generic=10:12)
# Error: could not find function "factor<-"

# and this is the magic work-around that does work
`levels<-`(
  factor(dat$product),
  list(Tylenol=1:3, Advil=4:6, Bayer=7:9, Generic=10:12)
  )
Tommy
fuente
+1 Creo que sería más limpio convertir primero a factor, luego reemplazar los niveles a través de una llamada within()y donde transform()el objeto así modificado se devuelve y se asigna.
Gavin Simpson
4
@GavinSimpson - Estoy de acuerdo, solo explico la magia, no la defiendo ;-)
Tommy
16

Para el código de usuario, me pregunto por qué se usan tales manipulaciones del lenguaje. Pregunta qué magia es esta y otros han señalado que está llamando a la función de reemplazo que tiene el nombre levels<-. Para la mayoría de la gente esto es mágico y realmente el uso previsto lo es levels(foo) <- bar.

El caso de uso que muestra es diferente porque productno existe en el entorno global, por lo que solo existe en el entorno local de la llamada, por levels<-lo que el cambio que desea realizar no persiste; no hubo reasignación de dat.

En estas circunstancias, within() es la función ideal a utilizar. Naturalmente desearía escribir

levels(product) <- bar

en R pero, por supuesto product, no existe como objeto. within()evita esto porque configura el entorno en el que desea ejecutar su código R y evalúa su expresión dentro de ese entorno. Asignar el objeto de retorno de la llamada a within()tiene éxito en el marco de datos correctamente modificado.

Aquí hay un ejemplo (no necesita crear uno nuevo datX, solo lo hago para que los pasos intermedios permanezcan al final)

## one or t'other
#dat2 <- transform(dat, product = factor(product))
dat2 <- within(dat, product <- factor(product))

## then
dat3 <- within(dat2, 
               levels(product) <- list(Tylenol=1:3, Advil=4:6, 
                                       Bayer=7:9, Generic=10:12))

Lo que da:

> head(dat3)
  product
1 Generic
2 Generic
3   Bayer
4   Bayer
5   Advil
6 Tylenol
> str(dat3)
'data.frame':   20 obs. of  1 variable:
 $ product: Factor w/ 4 levels "Tylenol","Advil",..: 4 4 3 3 2 1 4 2 3 4 ...

Me cuesta ver cómo las construcciones como la que muestra son útiles en la mayoría de los casos: si desea cambiar los datos, cambie los datos, no cree otra copia y cambie eso (que es todo lo que hace la levels<-llamada después de todo ).

Gavin Simpson
fuente