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

114

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

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  

Ce n'est que l'impression d'un vecteur, donc pour le stocker, vous pouvez faire ce qui est encore plus déroutant:

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 augmenter ma capacité magique dans ce domaine?

Ari B. Friedman
la source
1
Il y a aussi names<-et [<-.
huon
1
Aussi, je me suis posé des questions à ce sujet sur l'autre question, mais je n'ai pas demandé: y a-t-il une raison pour la structure(...)construction au lieu de simplement data.frame(product = c(11L, 11L, ..., 8L))? (S'il y a de la magie là-bas, j'aimerais aussi la manier!)
huon
2
C'est un appel à la "levels<-"fonction function (x, value) .Primitive("levels<-"):, un peu comme X %in% Yest une abréviation pour "%in%"(X, Y).
BenBarnes
2
@dbaupp Très pratique pour des exemples reproductibles: stackoverflow.com/questions/5963269/…
Ari B. Friedman
8
Je ne sais pas pourquoi quelqu'un a voté pour fermer cela comme non constructif? Le Q a une réponse très claire: quelle est la signification de la syntaxe utilisée dans l'exemple et comment cela fonctionne-t-il dans R?
Gavin Simpson

Réponses:

104

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

R est un langage fonctionnel et n'aime pas muter ses objets. Mais il autorise les 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 se fait 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écursive:

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 toute fin, là où se déroule la mission) équivaut à ce que serait une mission 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 aubaine inattendue: vous n'avez pas seulement la possibilité de faire des affectations, vous avez une fonction pratique qui prend en compte un facteur et donne un autre facteur avec différents niveaux. Il n'y a vraiment rien de "cession" à ce sujet!

Donc, 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 mission, 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

  • Conservez ça dans res

Personnellement, je trouve que cette ligne de code est magnifique;)

Owen
la source
33

Pas de sorcellerie, c'est juste comment les fonctions de (sous) affectation sont définies. levels<-est un peu différent car il s'agit d'une primitive pour (sous) attribuer 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 également ê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 épater:

Data <- data.frame(x=1:10, y=10:1)
names(Data)[1] <- "HI"              # How does that work?!? Magic! ;-)
Joshua Ulrich
la source
1
Pouvez-vous expliquer un peu plus quand il est logique d'appeler des fonctions de cette façon, plutôt que de la manière habituelle? Je travaille sur l'exemple de @ Marek dans la question liée, mais cela aiderait à avoir une explication plus explicite.
Drew Steen
4
@DrewSteen: pour des raisons de clarté / lisibilité du code, je dirais que cela n'a jamais de sens car `levels<-`(foo,bar)c'est la même chose que levels(foo) <- bar. En utilisant l'exemple de @ Marek: `levels<-`(as.factor(foo),bar)est le même que foo <- as.factor(foo); levels(foo) <- bar.
Joshua Ulrich
Belle liste. Ne pensez-vous pas que levels<-c'est vraiment juste un raccourci pour attr<-(x, "levels") <- value, ou du moins c'était probablement jusqu'à ce qu'il soit transformé en primitif et remis au code C.
IRTFM
30

La raison de cette "magie" est que le formulaire "affectation" doit avoir une vraie variable sur laquelle travailler. Et le factor(dat$product)n'était assigné à 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)
  )
Tommy
la source
+1 Je pense qu'il serait plus propre de convertir d'abord en facteur, puis de remplacer les niveaux via a within()et d' transform()appeler où l'objet ainsi modifié est renvoyé et attribué.
Gavin Simpson
4
@GavinSimpson - Je suis d'accord, je n'explique que la magie, je ne la défends pas ;-)
Tommy
16

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 c'est vraiment l'usage prévu levels(foo) <- bar.

Le cas d’utilisation que vous montrez est différent car il productn’existe pas dans l’environnement global, il n’existe donc que dans l’environnement local de l’appel. levels<-Ainsi, le changement que vous souhaitez apporter ne persiste pas - il n’y a pas eu de réaffectation de dat.

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

levels(product) <- bar

en R mais productn'existe bien sûr 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()aboutit ainsi à la trame de données correctement modifiée.

Voici un exemple (vous n'avez pas besoin de créer de nouveau 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 à quel point des 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 créez pas une autre copie et changez cela (ce que l' levels<-appel fait après tout ).

Gavin Simpson
la source