我无法理解公式如何在R中起作用,公式是什么。 那么,如果我有一个包含月度时间序列的向量,我怎么能创建一个盒子图,其中数据按季节逻辑划分?我想要12箱,每月一箱。
答案 0 :(得分:6)
在存储级别,公式为parse tree。解析树使用一个或两个参数对`~`()
函数的调用进行编码。对于单侧公式,它需要一个表示RHS的参数,而对于双侧公式,它需要两个参数来表示公式的LHS和RHS。
应该提到的是,嵌入在公式的解析树存储表示中的对`~`()
的调用并不真正意味着什么。一般来说,`~`()
函数实际上并没有做任何事情,除了允许创建公式对象,显式(例如`~`(a+b,c/d)
)或使用R语言提供的语法糖特性(例如a+b~c/d
)。在公式的解析树存储表示的顶级编码中使用`~`()
函数是相当随意且无关紧要的实现细节。我稍后会对此进行扩展。
R语言可以将解析树分解为递归列表结构,这可以帮助我们检查和理解这些解析树的结构。
我已经编写了一个可以执行此操作的简短递归函数:
ptunwrap <- function(x) if (typeof(x)=='language') lapply(as.list(x),ptunwrap) else x;
让我们看一个例子:
f1 <- a+b~c/d;
f1;
## a + b ~ c/d
ptunwrap(f1);
## [[1]]
## `~`
##
## [[2]]
## [[2]][[1]]
## `+`
##
## [[2]][[2]]
## a
##
## [[2]][[3]]
## b
##
##
## [[3]]
## [[3]][[1]]
## `/`
##
## [[3]][[2]]
## c
##
## [[3]][[3]]
## d
##
当解析树包含函数调用时,函数调用在递归列表结构中表示为单个列表节点。该函数的符号作为该列表的第一个列表组件嵌入,其参数作为列表的后续列表组件嵌入。
上面的解析树中有三个函数调用。顶级列表表示对`~`()
函数的调用,如前所述。第二个顶级列表组件进一步分支到另一个列表,该列表包含对`+`()
函数的调用,该函数本身带有两个参数,符号a
和b
。第三个组件类似,表示对`/`()
函数的调用,并再次使用两个参数,符号c
和d
。
重要的是要理解尽管解析树总是表示语法上有效的R代码,并且能力被评估以产生单个结果值,但它不是必要的来评估一个解析树。完全可以创建一个解析树,而不会对它进行评估。
那么,创建一个永远不会评估的解析树的目的是什么?在R中,这通常是为了便于使用方便的语法将某些信息传递给函数。
作为一个随机示例,data.table包允许以下语法糖使用:=
运算符向现有data.table添加列:
library(data.table);
dt <- data.table(a=1:3);
dt[,b:=a*2L];
dt;
## a b
## 1: 1 2
## 2: 2 4
## 3: 3 6
在内部,data.table使用非标准参数求值来检索第二个参数的解析树(技术上在函数定义中为第三个;在句法 - 糖形式中为第二个)到`[.data.table`()
函数,通常被称为作为&#34; j
参数&#34;由于参数名称为j
。如果你愿意,你可以直接在R中检查源本身。这里是最相关的代码段的片段:
data.table:::`[.data.table`;
## function (x, i, j, by, keyby, with = TRUE, nomatch = getOption("datatable.nomatch"),
## mult = "all", roll = FALSE, rollends = if (roll == "nearest") c(TRUE,
## TRUE) else if (roll >= 0) c(FALSE, TRUE) else c(TRUE,
## FALSE), which = FALSE, .SDcols, verbose = getOption("datatable.verbose"),
## allow.cartesian = getOption("datatable.allow.cartesian"),
## drop = NULL, on = NULL)
## {
##
## ... snip ...
##
## if (!missing(j)) {
## jsub = substitute(j)
## if (is.call(jsub))
## jsub = construct(deconstruct_and_eval(replace_dot(jsub),
## parent.frame(), parent.frame()))
## }
##
## ... snip ...
##
我们可以看到他们正在使用substitute(j)
来检索j
参数的解析树。对于上面的演示,这是他们将得到的:
ptunwrap(substitute(b:=a*2L));
## [[1]]
## `:=`
##
## [[2]]
## b
##
## [[3]]
## [[3]][[1]]
## `*`
##
## [[3]][[2]]
## a
##
## [[3]][[3]]
## [1] 2
##
稍后在代码中,他们测试顶级函数符号是否为:=
,这是支持向data.table添加(或修改或删除RHS为NULL)列的运算符。 。如果是这样,他们会测试LHS是否包含单个裸字,该裸字被视为要添加(或修改或删除)的列的名称。事实上,在这种情况下,它们实际上是不可能来实际评估解析树的LHS,因为它由data.table中尚不存在的符号组成。但是,RHS最终会被评估以生成要添加到新名称下的data.table的列向量。
因此,应该清楚公式可以在R中的各种上下文中使用,并且它们并不总是被评估。有时,只需检查解析树以检索从调用者传递给被调用者的信息。即使在 评估的上下文中,有时也只会单独评估LHS或RHS(或两者),忽略在创建时解析树中嵌入的顶级函数符号
转到boxplot()
函数,让我们看一下formula
参数的文档:
一个公式,例如y~grp,其中y是根据分组变量grp(通常是因子)分成组的数据值的数值向量。
在这种情况下,公式的两边最终都要独立评估,LHS提供数据向量,RHS提供分组定义。
证明这一点的好方法如下:
boxplot(1:9~1:9%%3L);
注意公式的两边是如何由文字表达式组成的:
1:9;
## [1] 1 2 3 4 5 6 7 8 9
1:9%%3L;
## [1] 1 2 0 1 2 0 1 2 0
在内部,boxplot()
必须独立评估公式的每一侧以生成数据和分组向量,就像将两个表达式作为单独的参数传递一样。
所以,让我们创建一个月度时间序列框图的简单演示:
N <- 36L; df <- data.frame(date=seq(as.Date('2016-01-01'),by='month',len=N),y=rnorm(N));
df;
## date y
## 1 2016-01-01 -1.56004488
## 2 2016-02-01 0.65699747
## 3 2016-03-01 0.05729631
## 4 2016-04-01 -0.02092276
## 5 2016-05-01 0.46673530
## 6 2016-06-01 -0.18652580
## 7 2016-07-01 0.06228650
## 8 2016-08-01 1.54452267
## 9 2016-09-01 1.06643594
## 10 2016-10-01 -1.51178160
## 11 2016-11-01 0.82904673
## 12 2016-12-01 0.37667201
## 13 2017-01-01 -0.10135801
## 14 2017-02-01 0.94692462
## 15 2017-03-01 -1.60781946
## 16 2017-04-01 0.47189753
## 17 2017-05-01 -1.32869317
## 18 2017-06-01 -0.49821455
## 19 2017-07-01 0.54474606
## 20 2017-08-01 0.47565264
## 21 2017-09-01 -0.97494730
## 22 2017-10-01 -1.22781588
## 23 2017-11-01 -0.34919086
## 24 2017-12-01 -0.78153843
## 25 2018-01-01 -0.59355220
## 26 2018-02-01 -2.58287605
## 27 2018-03-01 1.42148186
## 28 2018-04-01 -1.01278176
## 29 2018-05-01 -0.80961662
## 30 2018-06-01 0.19793126
## 31 2018-07-01 -1.03072915
## 32 2018-08-01 -0.87896416
## 33 2018-09-01 -2.36216655
## 34 2018-10-01 1.82708221
## 35 2018-11-01 0.05579195
## 36 2018-12-01 1.28612246
boxplot(y~months(date),df);
如果需要,可以学习源代码,这需要跟踪S3查找过程:
boxplot;
## function (x, ...)
## UseMethod("boxplot")
## <bytecode: 0x600b50760>
## <environment: namespace:graphics>
graphics:::boxplot.formula;
## function (formula, data = NULL, ..., subset, na.action = NULL)
## {
## if (missing(formula) || (length(formula) != 3L))
## stop("'formula' missing or incorrect")
## m <- match.call(expand.dots = FALSE)
## if (is.matrix(eval(m$data, parent.frame())))
## m$data <- as.data.frame(data)
## m$... <- NULL
## m$na.action <- na.action
## m[[1L]] <- quote(stats::model.frame)
## mf <- eval(m, parent.frame())
## response <- attr(attr(mf, "terms"), "response")
## boxplot(split(mf[[response]], mf[-response]), ...)
## }
## <bytecode: 0x6035c67f8>
## <environment: namespace:graphics>
这几乎是令人恼火的迂回和复杂,但graphics:::boxplot.formula()
有效地检索(通过match.call()
)导致其自己调用的解析树,按摩它一点点,最明显的是替换它自己的函数符号{{ 1}}使用boxplot.formula
,然后计算新的解析树,从而调用stats::model.frame()
。该功能本身非常复杂,涉及进一步的S3查找,但这是最相关的代码:
stats::model.frame
因此,最终,它从公式对象中检索单个表达式,并使用eval()
使用给定的data.frame和公式的闭包环境作为上下文来计算它们,这给出了结果向量:
model.frame;
## function (formula, ...)
## UseMethod("model.frame")
## <bytecode: 0x601464b18>
## <environment: namespace:stats>
model.frame.default;
## function (formula, data = NULL, subset = NULL, na.action = na.fail,
## drop.unused.levels = FALSE, xlev = NULL, ...)
## {
##
## ... snip ...
##
## if (!inherits(formula, "terms"))
## formula <- terms(formula, data = data)
## env <- environment(formula)
## rownames <- .row_names_info(data, 0L)
## vars <- attr(formula, "variables")
## predvars <- attr(formula, "predvars")
## if (is.null(predvars))
## predvars <- vars
## varnames <- sapply(vars, function(x) paste(deparse(x, width.cutoff = 500),
## collapse = " "))[-1L]
## variables <- eval(predvars, data, env)
##
## ... snip ...
##
要重申前面提到的观点,请注意在评估过程中无法找到attr(terms(y~months(date),data=df),'variables');
## list(y, months(date))
eval(attr(terms(y~months(date),data=df),'variables'),df);
## [[1]]
## [1] -1.56004488 0.65699747 0.05729631 -0.02092276 0.46673530 -0.18652580 0.06228650 1.54452267 1.06643594 -1.51178160 0.82904673 0.37667201 -0.10135801 0.94692462 -1.60781946 0.47189753 -1.32869317 -0.49821455 0.54474606
## [20] 0.47565264 -0.97494730 -1.22781588 -0.34919086 -0.78153843 -0.59355220 -2.58287605 1.42148186 -1.01278176 -0.80961662 0.19793126 -1.03072915 -0.87896416 -2.36216655 1.82708221 0.05579195 1.28612246
##
## [[2]]
## [1] "January" "February" "March" "April" "May" "June" "July" "August" "September" "October" "November" "December" "January" "February" "March" "April" "May" "June" "July"
## [20] "August" "September" "October" "November" "December" "January" "February" "March" "April" "May" "June" "July" "August" "September" "October" "November" "December"
##
函数。它是R公式的任意实现细节,它们将`~`()
函数编码为公式对象的分析树存储表示中的顶级函数符号。如果出现公式的一方(s)的实际评估,则不涉及对该函数的评估。
最后,让我们考虑如果您实际评估包含公式的存储表示的整个解析树会发生什么。回想一下,`~`()
函数只是从其参数创建一个公式。因此,评估一个公式有一个有趣的效果,即吐出刚刚评估的完全相同的公式:
`~`()
我已经在你可能感兴趣的解析树和公式上写了几个其他答案: