`levels<-`是什么魔法?

120

在回答另一个问题时,@Marek发布了以下解决方案:https://dev59.com/Ymkv5IYBdhLWcg3wqiqS#10432263

该链接指向一个stackoverflow网站上的答案。
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)
  )

显然,这是对levels函数的某种调用,但我不知道在这里做了什么。这种魔法的术语是什么,如何增强在该领域的神奇能力呢?


1
另外,我在另一个问题上也想知道,为什么要使用structure(...)这种结构,而不是直接使用data.frame(product = c(11L, 11L, ..., 8L))?(如果有什么神奇的地方,请告诉我,我也想掌握它!) - huon
2
这是对"levels<-"函数的调用:function (x, value) .Primitive("levels<-"),有点像X %in% Y"%in%"(X, Y)的缩写。 - BenBarnes
1
@dbaupp 我刚刚使用了 dput 命令来输出我通过对实际数据进行子集操作所创建的对象,而 dput 默认返回 structure 调用。 - Ari B. Friedman
2
@dbaupp非常适用于可重复的示例:https://dev59.com/eG025IYBdhLWcg3whGSx - Ari B. Friedman
8
我不知道为什么有人投票将这个问题标记为“不构造性的”?这个问题有一个非常清晰的答案:示例中使用的语法的含义是什么,以及在R中它是如何工作的? - Gavin Simpson
显示剩余4条评论
4个回答

111
这里的答案都很好,但是它们缺少了一个重要的观点。让我试着描述一下。
R是一种函数式语言,不喜欢改变其对象。但是它允许使用替换函数进行赋值语句:
levels(x) <- y

等同于

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

诀窍是,这个重写是由<-完成的;它不是由levels<-完成的。 levels<-只是一个普通的函数,它接受一个输入并产生一个输出;它不会改变任何内容。

其中的一个结果是,根据上述规则,<-必须是递归的:

levels(x)[1] <- "a"


levels(x) <- `[<-`(levels(x), 1, "a")


x <- `levels<-`(x, `[<-`(levels(x), 1, "a"))

这个功能性转换有点美妙,一直到最后的赋值操作之前,它与命令式编程语言中的赋值是等价的。在函数式语言中,这种构造被称为lens。在某些编程语言中,使用镜头可能会有些笨拙,但在R中,它们非常有效。
然后,一旦你定义了类似levels<-的替代函数,你会得到另一个意想不到的收益:你不仅能够进行赋值,还有一个方便的函数,它接受一个因子,并输出具有不同水平的另一个因子。实际上,这与"赋值"毫无关系!
因此,你所描述的代码只是利用了levels<-的另一种解释方式。我承认,levels<-的命名可能有点令人困惑,因为它暗示了一种赋值操作,但实际上并非如此。这段代码只是设置了一种管道的形式。
  • dat$product 開始

  • 將其轉換為因子

  • 修改層級

  • 將其儲存在 res

就我個人而言,我認為這行程式碼非常美麗 ;)


为了完整起见,这也在R语言定义中(有点正式地)描述:https://cran.r-project.org/doc/manuals/R-lang.html#Subset-assignment - krlmlr

34

没有什么神奇的,这就是(子)赋值函数的定义。 levels<- 有些不同,因为它是一种原始的方法来(sub)分配因子的属性,而不是元素本身。 有很多这种类型函数的例子:

`<-`              # 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的例子进行工作,但更明确的解释会有所帮助。 - Drew Steen
4
基于代码清晰和易读性的原因,我会说这永远都没有意义,因为\levels<-`(foo,bar)levels(foo) <- bar相同。以@ Marek的示例为例:`levels<-`(as.factor(foo),bar)foo <- as.factor(foo); levels(foo) <- bar`相同。 - Joshua Ulrich
不错的列表。你觉得levels<-只是attr<-(x, "levels") <- value的简写,或者至少在它被转换为原始代码并移交给C代码之前是这样的吗? - IRTFM

31
那个“神奇”的原因是,“赋值”表单必须有一个真实的变量来操作。而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 我认为更干净的做法是先转换为因子,然后通过 within()transform() 调用替换级别,从而修改对象并返回分配。 - Gavin Simpson
4
@GavinSimpson - 我同意,我只是解释这个神奇的东西,我不会为它辩护 ;-) - Tommy

17

对于用户代码,我想知道为什么要使用这样的语言操作?你问这是什么魔法,其他人指出你正在调用具有名称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<- 调用的主要功能就是创建另一个副本并进行更改)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接