web-dev-qa-db-fra.com

`niveaux <-` (De quelle sorcellerie s'agit-il?

En réponse à une autre question, @Marek a publié la solution suivante: 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)
  )

Ce qui produit en sortie:

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

Ceci est juste l'impression d'un vecteur, donc pour le stocker, vous pouvez faire encore plus de confusion:

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

Il s'agit clairement d'une sorte d'appel à la fonction des niveaux, mais je n'ai aucune idée de ce qui se fait ici. Quel est le terme pour ce genre de sorcellerie, et comment puis-je augmenter ma capacité magique dans ce domaine?

112
Ari B. Friedman

Les réponses ici sont bonnes, mais il leur manque un point important. Permettez-moi d'essayer de le décrire.

R est un langage fonctionnel et n'aime pas muter ses objets. Mais cela permet des instructions d'affectation, en utilisant des fonctions de remplacement:

levels(x) <- y

est équivalent à

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

L'astuce est que cette réécriture est effectuée par <-; ce n'est pas fait par levels<-. levels<- est juste une fonction régulière qui prend une entrée et donne une sortie; il ne mute rien.

Une conséquence de cela est que, selon la règle ci-dessus, <- doit être récursif:

levels(factor(x)) <- y

est

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

est

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

C'est assez beau que cette transformation purement fonctionnelle (jusqu'à la fin, où l'affectation se produit) soit équivalente à ce qu'une affectation serait dans un langage impératif. Si je me souviens bien, cette construction dans les langages fonctionnels s'appelle une lentille.

Mais ensuite, une fois que vous avez défini des fonctions de remplacement comme levels<-, vous obtenez une autre manne inattendue: vous n'avez pas seulement la possibilité de faire des affectations, vous avez une fonction pratique qui prend un facteur et donne un autre facteur avec différents niveaux. Il n'y a vraiment rien "d'affectation" à ce sujet!

Ainsi, le code que vous décrivez utilise simplement cette autre interprétation de levels<-. J'avoue que le nom levels<- est un peu déroutant car il suggère une affectation, mais ce n'est pas ce qui se passe. Le code met simplement en place une sorte de pipeline:

  • Commencer avec dat$product

  • Convertissez-le en facteur

  • Changer les niveaux

  • Stockez cela dans res

Personnellement, je pense que cette ligne de code est belle;)

101
Owen

Pas de sorcellerie, c'est ainsi que les fonctions de (sous-) affectation sont définies. levels<- est un peu différent car c'est une primitive pour (sub) assigner les attributs d'un facteur, pas les éléments eux-mêmes. Il existe de nombreux exemples de ce type de fonction:

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

D'autres opérateurs binaires peuvent aussi être appelés comme ça:

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

Maintenant que vous le savez, quelque chose comme ça devrait vraiment vous couper le souffle:

Data <- data.frame(x=1:10, y=10:1)
names(Data)[1] <- "HI"              # How does that work?!? Magic! ;-)
32
Joshua Ulrich

La raison de cette "magie" est que le formulaire "d'affectation" doit avoir une vraie variable sur laquelle travailler. Et la factor(dat$product) n'était assignée à rien.

# 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)
  )
30
Tommy

Pour le code utilisateur, je me demande pourquoi de telles manipulations de langage sont utilisées ainsi? Vous demandez ce qu'est la magie et d'autres ont souligné que vous appelez la fonction de remplacement qui porte le nom levels<-. Pour la plupart des gens, c'est magique et vraiment l'utilisation prévue est levels(foo) <- bar.

Le cas d'utilisation que vous montrez est différent parce que product n'existe pas dans l'environnement global, il n'existe donc que dans l'environnement local de l'appel à levels<-, Donc la modification que vous souhaitez effectuer existe persiste pas - il n'y a pas eu de réaffectation de dat.

Dans ces circonstances, within() est la fonction idéale à utiliser. Vous souhaitez naturellement écrire

levels(product) <- bar

dans R mais bien sûr product n'existe pas en tant qu'objet. within() contourne ce problème car il configure l'environnement dans lequel vous souhaitez exécuter votre code R et évalue votre expression dans cet environnement. L'affectation de l'objet de retour de l'appel à within() réussit donc dans la trame de données correctement modifiée.

Voici un exemple (vous n'avez pas besoin de créer de nouveaux datX - je le fais juste pour que les étapes intermédiaires restent à la fin)

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

Qui donne:

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

J'ai du mal à voir comment les constructions comme celle que vous montrez sont utiles dans la majorité des cas - si vous voulez changer les données, changer les données, ne pas créer une autre copie et changer cela (ce qui est tout le levels<- appel fait après tout).