`уровни <-` (Что это за колдовство?

114

В ответ на другой вопрос @Marek опубликовал следующее решение: 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)
  )

Что дает на выходе:

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

Это просто распечатка вектора, поэтому для ее сохранения можно сделать еще более запутанную:

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

Ясно, что это какой-то вызов функции уровней, но я понятия не имею, что здесь делается. Как называется этот вид колдовства и как мне увеличить свои магические способности в этой области?

Ари Б. Фридман
источник
1
Также есть names<-и [<-.
huon
1
Кроме того, я задумался об этом по другому вопросу, но не спросил: есть ли причина для structure(...)конструкции вместо просто data.frame(product = c(11L, 11L, ..., 8L))? (Если там творится какая-то магия, я бы тоже ею стал владеть!)
хуон
2
Это вызов "levels<-"функции:, function (x, value) .Primitive("levels<-")вроде как X %in% Yэто сокращение для "%in%"(X, Y).
BenBarnes
2
@dbaupp Очень удобно для воспроизводимых примеров: stackoverflow.com/questions/5963269/…
Ари Б. Фридман
8
Понятия не имею, почему кто-то проголосовал за то, чтобы закрыть это как неконструктивное? Q дает очень четкий ответ: в чем смысл синтаксиса, использованного в примере, и как это работает в R?
Гэвин Симпсон

Ответы:

104

Ответы здесь хорошие, но в них упускается важный момент. Позвольте мне попытаться описать это.

R - это функциональный язык, и он не любит видоизменять свои объекты. Но он позволяет операторы присваивания с использованием функций замены:

levels(x) <- y

эквивалентно

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

Хитрость в том, что это переписывание выполняется с помощью <-; это не делается levels<-. levels<-это просто обычная функция, которая принимает вход и выдает результат; он ничего не мутирует.

Одним из следствий этого является то, что, согласно приведенному выше правилу, <-должно быть рекурсивным:

levels(factor(x)) <- y

является

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

является

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

Прекрасно, что это чисто функциональное преобразование (до самого конца, где происходит присваивание) эквивалентно тому, что присваивание было бы в императивном языке. Если я правильно помню, эта конструкция в функциональных языках называется линзой.

Но затем, как только вы определили функции замены, например levels<-, вы получите еще одну неожиданную удачу: у вас не просто есть возможность выполнять назначения, у вас есть удобная функция, которая принимает фактор и выдает другой фактор с разными уровнями. В этом нет ничего "поручения"!

Итак, код, который вы описываете, просто использует эту другую интерпретацию levels<-. Я признаю, что это название levels<-немного сбивает с толку, потому что оно предполагает задание, но это не то, что происходит. Код просто настраивает своего рода конвейер:

  • Начать с dat$product

  • Преобразуйте это в коэффициент

  • Измените уровни

  • Храните это в res

Лично я считаю эту строчку кода красивой;)

Оуэн
источник
33

Никакого колдовства, просто так определяются функции (под) присваивания. levels<-немного отличается, потому что это примитив для (под) назначения атрибутов фактора, а не самих элементов. Есть множество примеров такого типа функций:

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

Так же могут вызываться и другие бинарные операторы:

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

Теперь, когда вы это знаете, что-то вроде этого должно действительно поразить вас:

Data <- data.frame(x=1:10, y=10:1)
names(Data)[1] <- "HI"              # How does that work?!? Magic! ;-)
Джошуа Ульрих
источник
1
Не могли бы вы объяснить немного больше о том, когда имеет смысл вызывать функции таким способом, а не обычным способом? Я работаю над примером @Marek в связанном вопросе, но было бы полезно получить более подробное объяснение.
Дрю Стин
4
@DrewSteen: из соображений ясности / читаемости кода я бы сказал, что это никогда не имеет смысла, потому что `levels<-`(foo,bar)это то же самое, что и levels(foo) <- bar. Используя пример @Marek: `levels<-`(as.factor(foo),bar)то же самое, что и foo <- as.factor(foo); levels(foo) <- bar.
Джошуа Ульрих
Хороший список. Вам не кажется, что levels<-это действительно просто сокращение attr<-(x, "levels") <- value, или, по крайней мере, вероятно, так было до тех пор, пока его не превратили в примитив и не передали в C-код.
IRTFM
30

Причина этой «магии» в том, что форма «присваивания» должна иметь реальную переменную для работы. И factor(dat$product)ни к чему не приписывался.

# 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 Думаю, было бы чище сначала преобразовать в множитель, а затем заменить уровни через a within()и transform()call, если таким образом измененный объект будет возвращен и назначен.
Гэвин Симпсон
4
@GavinSimpson - Я согласен, я только объясняю магию, я ее не защищаю ;-)
Томми
16

Что касается пользовательского кода, мне интересно, почему такие языковые манипуляции так используются? Вы спрашиваете, что это за магия, а другие указывали, что вы вызываете функцию замены, у которой есть имя levels<-. Для большинства людей это магия и действительно так и по назначению levels(foo) <- bar.

Показанный вами вариант использования отличается, поскольку productон не существует в глобальной среде, поэтому он всегда существует только в локальной среде вызова, levels<-поэтому изменение, которое вы хотите сделать, не сохраняется - не было переназначения dat.

В этих обстоятельствах within() это идеальная функция для использования. Вы, естественно, захотите написать

levels(product) <- bar

в R, но, конечно product, не существует как объект. within()обходит это, потому что он устанавливает среду, в которой вы хотите запустить свой код R, и оценивает ваше выражение в этой среде. within()Таким образом, присвоение возвращаемого объекта из вызова успешно измененному кадру данных.

Вот пример (вам не нужно создавать новое datX- я просто делаю это, чтобы промежуточные шаги остались в конце)

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

Который дает:

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

Я изо всех сил пытаюсь понять, насколько полезны конструкции, подобные той, которую вы показываете, в большинстве случаев - если вы хотите изменить данные, изменить данные, не создавайте еще одну копию и не меняйте ее (в levels<-конце концов, это все, что делает вызов ).

Гэвин Симпсон
источник