`livelli <-` (Che stregoneria è questa?


114

In una risposta a un'altra domanda, @Marek ha pubblicato la seguente soluzione: 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)
  )

Che produce come output:

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

Questa è solo la stampa di un vettore, quindi per memorizzarla puoi fare ancora più confusione:

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

Chiaramente questa è una sorta di chiamata alla funzione dei livelli, ma non ho idea di cosa si stia facendo qui. Qual è il termine per questo tipo di stregoneria e come posso aumentare la mia capacità magica in questo dominio?


1
C'è anche names<-e [<-.
huon

1
Inoltre, mi sono chiesto questo sull'altra domanda, ma non ho chiesto: c'è qualche motivo per il structure(...)costrutto invece che solo data.frame(product = c(11L, 11L, ..., 8L))? (Se c'è qualche magia che sta accadendo lì, vorrei
usarla anche io

2
È una chiamata alla "levels<-"funzione function (x, value) .Primitive("levels<-"):, una specie di simile X %in% Yè un'abbreviazione di "%in%"(X, Y).
BenBarnes

2
@dbaupp Molto utile per esempi riproducibili: stackoverflow.com/questions/5963269/…
Ari B. Friedman

8
Non ho idea del motivo per cui qualcuno ha votato per chiudere questo come non costruttivo? La Q ha una risposta molto chiara: qual è il significato della sintassi usata nell'esempio e come funziona in R?
Gavin Simpson

Risposte:


104

Le risposte qui sono buone, ma mancano di un punto importante. Lasciami provare a descriverlo.

R è un linguaggio funzionale e non ama mutarne gli oggetti. Ma consente istruzioni di assegnazione, utilizzando funzioni di sostituzione:

levels(x) <- y

è equivalente a

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

Il trucco è che questa riscrittura viene eseguita da <-; non è fatto da levels<-. levels<-è solo una funzione regolare che accetta un input e fornisce un output; non muta nulla.

Una conseguenza di ciò è che, secondo la regola precedente, <-deve essere ricorsivo:

levels(factor(x)) <- y

è

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

è

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

È piuttosto bello che questa trasformazione puramente funzionale (fino alla fine, dove avviene l'assegnazione) sia equivalente a ciò che sarebbe un compito in un linguaggio imperativo. Se ricordo bene questo costrutto nei linguaggi funzionali è chiamato lente.

Ma poi, una volta definite le funzioni di sostituzione come levels<-, ottieni un altro, inaspettato guadagno: non hai solo la capacità di fare incarichi, hai una comoda funzione che prende in considerazione un fattore e fornisce un altro fattore con livelli diversi. Non c'è davvero nulla di "incarico" a riguardo!

Quindi, il codice che stai descrivendo sta solo facendo uso di quest'altra interpretazione levels<-. Ammetto che il nome levels<-sia un po 'confuso perché suggerisce un incarico, ma non è questo che sta succedendo. Il codice sta semplicemente impostando una sorta di pipeline:

  • Iniziare con dat$product

  • Convertilo in un fattore

  • Cambia i livelli

  • Conservalo in res

Personalmente, penso che quella riga di codice sia bellissima;)


33

Nessuna stregoneria, è così che vengono definite le funzioni di (sotto) assegnazione. levels<-è un po 'diverso perché è una primitiva per (sotto) assegnare gli attributi di un fattore, non gli elementi stessi. Ci sono molti esempi di questo tipo di funzione:

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

Anche altri operatori binari possono essere chiamati in questo modo:

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

Ora che lo sai, qualcosa del genere dovrebbe davvero sbalordirti:

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

1
Puoi spiegare un po 'di più su quando ha senso chiamare le funzioni in questo modo, piuttosto che nel solito modo? Sto lavorando all'esempio di @ Marek nella domanda collegata, ma aiuterebbe avere una spiegazione più esplicita.
Drew Steen

4
@DrewSteen: per motivi di chiarezza / leggibilità del codice, direi che non ha mai senso perché `levels<-`(foo,bar)è uguale a levels(foo) <- bar. Usando l'esempio di @ Marek: `levels<-`(as.factor(foo),bar)è lo stesso di foo <- as.factor(foo); levels(foo) <- bar.
Joshua Ulrich

Bella lista. Non pensi che levels<-sia davvero solo una scorciatoia per attr<-(x, "levels") <- value, o almeno probabilmente lo era fino a quando non è stato trasformato in un primitivo e consegnato al C-code.
IRTFM

30

La ragione di quella "magia" è che il modulo "assegnazione" deve avere una variabile reale su cui lavorare. E factor(dat$product)non è stato assegnato a nulla.

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

+1 Penso che sarebbe più pulito convertire prima in fattore, quindi sostituire i livelli tramite a within()e transform()chiamare dove l'oggetto così modificato viene restituito e assegnato.
Gavin Simpson

4
@GavinSimpson - Sono d'accordo, spiego solo la magia, non la difendo ;-)
Tommy

16

Per il codice utente mi chiedo perché tali manipolazioni del linguaggio vengano utilizzate così? Chiedete che cos'è questa magia e altri hanno sottolineato che state chiamando la funzione di sostituzione che ha il nome levels<-. Per la maggior parte delle persone questo è magico e lo è davvero l'uso previsto levels(foo) <- bar.

Il caso d'uso che mostri è diverso perché productnon esiste nell'ambiente globale, quindi esiste solo nell'ambiente locale della chiamata, levels<-quindi il cambiamento che vuoi apportare non persiste - non c'è stata riassegnazione di dat.

In queste circostanze, within() è la funzione ideale da utilizzare. Naturalmente vorresti scrivere

levels(product) <- bar

in R ma ovviamente productnon esiste come oggetto. within()aggira questo perché imposta l'ambiente in cui desideri eseguire il codice R e valuta la tua espressione all'interno di quell'ambiente. L'assegnazione dell'oggetto restituito dalla chiamata a within()riesce così nel frame di dati correttamente modificato.

Ecco un esempio (non è necessario crearne di nuovi datX- lo faccio solo in modo che i passaggi intermedi rimangano alla fine)

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

Che dà:

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

Faccio fatica a vedere come costrutti come quello che mostri siano utili nella maggior parte dei casi: se vuoi cambiare i dati, cambiare i dati, non creare un'altra copia e cambiarlo (che è tutto ciò che la levels<-chiamata sta facendo dopotutto ).

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.