`poziomy <--` (Co to za czary?

114

W odpowiedzi na kolejne pytanie @Marek zamieścił następujące rozwiązanie: 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)
  )

Który produkuje jako wynik:

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

To tylko wydruk wektora, więc aby go zapisać, możesz zrobić jeszcze bardziej zagmatwane:

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

Oczywiście jest to wywołanie funkcji poziomów, ale nie mam pojęcia, co się tutaj dzieje. Jaki jest termin określający ten rodzaj czarów i jak mogę zwiększyć swoje magiczne zdolności w tej dziedzinie?

Ari B. Friedman
źródło
1
Jest też names<-i [<-.
huon
1
Zastanawiałem się też nad tym przy innym pytaniu, ale nie zapytałem: czy jest jakiś powód dla structure(...)konstrukcji zamiast po prostu data.frame(product = c(11L, 11L, ..., 8L))? (Jeśli dzieje się tam jakaś magia, też chciałbym nią władać!)
huon
2
Jest to wywołanie "levels<-"funkcji function (x, value) .Primitive("levels<-"):, coś w rodzaju podobnego X %in% Yskrótu "%in%"(X, Y).
BenBarnes
2
@dbaupp Bardzo przydatne w przypadku powtarzalnych przykładów: stackoverflow.com/questions/5963269/ ...
Ari B. Friedman
8
Nie mam pojęcia, dlaczego ktoś głosował za zamknięciem tego jako nie konstruktywnego? Q ma bardzo jasną odpowiedź: jakie jest znaczenie składni użytej w przykładzie i jak to działa w R?
Gavin Simpson

Odpowiedzi:

104

Odpowiedzi tutaj są dobre, ale brakuje w nich ważnego punktu. Spróbuję to opisać.

R jest językiem funkcjonalnym i nie lubi mutować swoich obiektów. Ale pozwala na instrukcje przypisania, używając funkcji zastępczych:

levels(x) <- y

jest równa

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

Rzecz w tym, że przepisywanie odbywa się przez <-; nie jest to zrobione przez levels<-. levels<-jest zwykłą funkcją pobierającą dane wejściowe i wyświetlającą dane wyjściowe; niczego nie mutuje.

Jedną z konsekwencji tego jest to, że zgodnie z powyższą zasadą <-musi być rekurencyjny:

levels(factor(x)) <- y

jest

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

jest

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

To całkiem piękne, że ta czysto funkcjonalna transformacja (aż do samego końca, gdzie następuje zadanie) jest równoważna temu, czym byłoby zadanie w języku imperatywnym. Jeśli dobrze pamiętam, ten konstrukt w językach funkcjonalnych nazywa się soczewką.

Ale potem, kiedy już zdefiniujesz funkcje zastępcze, takie jak levels<-, otrzymujesz kolejną, nieoczekiwaną gratkę: nie tylko masz możliwość wykonywania przypisań, masz przydatną funkcję, która przyjmuje czynnik i wydaje inny czynnik o różnych poziomach. Naprawdę nie ma w tym nic „przydziału”!

Tak więc kod, który opisujesz, po prostu wykorzystuje tę inną interpretację levels<-. Przyznaję, że nazwa levels<-jest trochę zagmatwana, ponieważ sugeruje zadanie, ale nie o to chodzi. Kod po prostu tworzy rodzaj potoku:

  • Zacząć od dat$product

  • Zamień go na współczynnik

  • Zmień poziomy

  • Przechowuj to w res

Osobiście uważam, że ta linia kodu jest piękna;)

piekarnik
źródło
33

Żadnej magii, tak właśnie definiuje się (pod) funkcje przypisania. levels<-jest trochę inna, ponieważ (pod) przypisywanie atrybutów czynnika jest prymitywne, a nie samych elementów. Istnieje wiele przykładów tego typu funkcji:

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

Inne operatory binarne również można nazwać w ten sposób:

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

Teraz, gdy już to wiesz, coś takiego powinno naprawdę zaskoczyć:

Data <- data.frame(x=1:10, y=10:1)
names(Data)[1] <- "HI"              # How does that work?!? Magic! ;-)
Joshua Ulrich
źródło
1
Czy możesz wyjaśnić trochę więcej, kiedy warto wywoływać funkcje w ten sposób, a nie w zwykły sposób? Pracuję nad przykładem @ Marka w powiązanym pytaniu, ale pomogłoby mi mieć bardziej wyraźne wyjaśnienie.
Drew Steen
4
@DrewSteen: ze względu na przejrzystość / czytelność kodu powiedziałbym, że nigdy nie ma to sensu, ponieważ `levels<-`(foo,bar)jest takie samo jak levels(foo) <- bar. Na przykładzie @ Marek: `levels<-`(as.factor(foo),bar)to to samo, co foo <- as.factor(foo); levels(foo) <- bar.
Joshua Ulrich
Niezła lista. Nie sądzisz, że levels<-jest to tylko skrót attr<-(x, "levels") <- value, a przynajmniej prawdopodobnie tak było, dopóki nie został przekształcony w prymityw i przekazany kodowi C.
IRTFM
30

Powodem tej „magii” jest to, że formularz „przypisania” musi mieć rzeczywistą zmienną, nad którą można pracować. I factor(dat$product)nie był do niczego przypisany.

# 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
źródło
+1 Myślę, że czystsze byłoby najpierw przekonwertowanie na czynnik, a następnie zastąpienie poziomów za pomocą a within()i transform()wywołanie, gdy w ten sposób zmodyfikowany obiekt zostanie zwrócony i przypisany.
Gavin Simpson
4
@GavinSimpson - Zgadzam się, tylko wyjaśniam magię, nie bronię jej ;-)
Tommy
16

W przypadku kodu użytkownika zastanawiam się, dlaczego takie manipulacje językowe są tak używane? Pytasz, co to za magia, a inni wskazywali, że wywołujesz funkcję zastępczą, która ma nazwęlevels<- . Dla większości ludzi jest to magia i tak naprawdę jest to zamierzone zastosowanie levels(foo) <- bar.

Pokazany przypadek użycia jest inny, ponieważ product nie istnieje w środowisku globalnym, więc zawsze istnieje tylko w lokalnym środowisku wywołania, levels<-więc zmiana, którą chcesz wprowadzić, nie zachowuje się - nie było ponownego przypisania dat.

W takich okolicznościach within() jest to idealna funkcja do użycia. Naturalnie chciałbyś pisać

levels(product) <- bar

w R, ale oczywiście productnie istnieje jako obiekt.within()omija ten problem, ponieważ konfiguruje środowisko, na którym chcesz uruchomić kod R, i ocenia Twoje wyrażenie w tym środowisku. Przypisanie obiektu zwracanego z wywołania do w within()ten sposób powiedzie się w odpowiednio zmodyfikowanej ramce danych.

Oto przykład (nie musisz tworzyć nowego datX- po prostu to robię, aby kroki pośrednie pozostały na końcu)

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

Co daje:

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

Z trudem widzę, jak konstrukcje takie jak ta, którą pokazujesz, są przydatne w większości przypadków - jeśli chcesz zmienić dane, zmienić dane, nie tworzyć kolejnej kopii i zmieniać to (co jest wszystkim, co levels<-robi połączenie ).

Gavin Simpson
źródło