20 Evaluation

Published

August 15, 2025

Modified

August 20, 2025

Introduction

“引用”有两个反面——“解引用”和“评估”。通常,“解引用”面向使用者,赋予了使用者选择性评估“表达式”的能力;而“评估”面向开发者,赋予了开发者在自定义环境中评估“表达式”的能力。

本章从最纯粹的评估模式开始讨论,介绍eval()如何在环境中评估一个表达式,及如何使用它实现许多重要的base R 函数。然后延申评估,介绍两种重要思想:

  • Quoseure: 一种用于捕获表达式及其关联环境的数据结构。

  • 数据掩码:使在“数据框环境”中评估表达式更加容易。

总之,准引用、quosure、数据掩码共同构成了我们所说的整洁评估(tidy-eval)。整洁评估为非标准评估提供了一种原则性的方法,使得可以交互使用这些函数并将其与其他函数进行嵌套。整洁评估是所有这些理论中最重要的实际含义,因此将花一些时间来探讨它们的含义。本章最后讨论了base R的最相关方法,以及如何围绕它们的缺点进行编程。

Outline

  • 20.2节:介绍base::eval()函数,及如何使用它实现local()source()

  • 20.3节:介绍quosure数据结构,及如何生成与评估它。

  • 20.4节:介绍数据掩码和避免歧义的声明。

  • 20.5节:介绍使用整洁评估的实例。

  • 20.6节:介绍base R中的非标准性评估及其缺陷。

Prerequisites

要求熟悉前两章内容和第7章有关环境的内容。

library(rlang)
library(purrr)
#> 
#> Attaching package: 'purrr'
#> The following objects are masked from 'package:rlang':
#> 
#>     %@%, flatten, flatten_chr, flatten_dbl, flatten_int,
#>     flatten_lgl, flatten_raw, invoke, splice

Evaluation basics

eval()函数有两个参数:epxrenv

expr参数是待评估的“表达式”或符号。由于eval()函数不会对输入引用,所以需要与expr()一同使用:

x <- 10
eval(expr(x))
#> [1] 10

y <- 2
eval(expr(x + y))
#> [1] 12

env参数用来指定评估“表达式”的“环境”,如果没有指定,则默认为当前环境。

eval(expr(x + y), env(x = 1000))
#> [1] 1002

当指定了环境,却没有引用输入时,会导致错误的结果:

eval(print(x + 1), env(x = 1000))
#> [1] 11
#> [1] 11

eval(expr(print(x + 1)), env(x = 1000))
#> [1] 1001

在了解了基础知识后,让我们探索一些应用,根据底层原理重新实现base R中的某些函数。

Application:local()

有时我们会创建一些临时变量来执行一系列计算,这些临时变量不会长期使用,可能也会相当占用内存,需要在使用结束后删除。一种方法是使用rm()清楚临时变量;另一种是将计算过程打包为函数,仅调用一次。更优雅的方式是使用local()函数,它可以创建一个临时环境,并执行其中的代码。

# Clean up variables created earlier
rm(x, y)

foo <- local({
  x <- 10
  y <- 200
  x + y
})

foo
#> [1] 210
x
#> Error: object 'x' not found
y
#> Error: object 'y' not found

local()函数的本质很简单,我们可以采用下面的策略实现它。首先捕获输入的“表达式”,然后使用local()函数的执行环境作为eval()的调用环境参与评估。

local2 <- function(expr) {
  env <- env(caller_env())
  eval(enexpr(expr), env)
}

foo <- local2({
  x <- 10
  y <- 200
  x + y
})

foo
#> [1] 210
x
#> Error: object 'x' not found
y
#> Error: object 'y' not found

base::local()的底层实现很复杂,它使用了eval()substitute()

Application:source()

我们可以通过组合eval()parse_expr()来实现source()的功能。首先从磁盘中读取文件,然后使用parse_expr()将字符串转换成“表达式”列表,最后使用eval()评估“表达式”。实现如下:

source2 <- function(path, env = caller_env()) {
  file <- paste(readLines(path, warn = FALSE), collapse = "\n")
  exprs <- parse_exprs(file)

  res <- NULL
  for (i in seq_along(exprs)) {
    res <- eval(exprs[[i]], env)
  }

  invisible(res)
}

真实的base::source()函数更加复杂,会打印输入输出信息,同时有许多额外参数控制行为。

Expression vectors

上一章讲到,base::parse()函数解析字符串时,如果捕获到多个“表达式”,会返回一个包含多个表达式的向量。base::eval()函数可以直接评估这个向量,而不用上面的for循环。

source3 <- function(file, env = parent.frame()) {
  lines <- parse(file)
  res <- eval(lines, envir = env)
  invisible(res)
}

Gotcha:function()

如果你使用eval()expr()来生成函数,有一个小小的漏洞需要注意:

x <- 10
y <- 20
f <- eval(expr(function(x, y) !!x + !!y))
f
#> function (x, y) 
#> 10 + 20

这个函数看起来不像能正常运行,其实可以:

f()
#> [1] 30

这是因为,如果函数有“srcref”属性,就会打印它,但“srcref”是一个base R的特性,它无法识别准引用。

要解决这个问题,可以使用new_function()或删除“srcref”属性:

attr(f, "srcref") <- NULL
f
#> function (x, y) 
#> 10 + 20

Quosures

几乎eval()的所有使用都包括“表达式”和“环境”两个参数,但是base R中没有能同时提供这两个参数的数据结构,“rlang”包创建了这种数据结构——“quosures”。quosures是“quoting”和“closure”的复合体,意味着它同时包含了“表达式”和环境。

在本节中,你将学习如何创建和操作quosure, 以及一些关于如何实现它。

Creating

有三中方式创建quosure:

  • 使用enquo()enquos(),它们会同时捕获表达式和环境。许多quosure都是由此创建的。
foo <- function(x) enquo(x)
foo(a + b)
#> <quosure>
#> expr: ^a + b
#> env:  global
  • 使用quo()quos(),与enquo()enquos()的关系可以参考expr()``enexpr()。使用的场景很少。
quo(x + y + z)
#> <quosure>
#> expr: ^x + y + z
#> env:  global
  • 使用new_quosure(),输入“表达式”和环境来创建quosure。使用场景也极少。
new_quosure(expr(x + y), env(x = 1, y = 10))
#> <quosure>
#> expr: ^x + y
#> env:  0x000001c76004dd30

Evaluting

只能使用eval_tidy()来评估quosure。

q1 <- new_quosure(expr(x + y), env(x = 1, y = 10))
eval_tidy(q1)
#> [1] 11

Dots

enquos()可以正确识别...中传入的参数及其绑定的环境。例如,下面的qs对象,正确评估了globalf的环境。

f <- function(...) {
  x <- 1
  g(..., f = x)
}
g <- function(...) {
  enquos(...)
}

x <- 0
qs <- f(global = x)
qs
#> <list_of<quosure>>
#> 
#> $global
#> <quosure>
#> expr: ^x
#> env:  global
#> 
#> $f
#> <quosure>
#> expr: ^x
#> env:  0x000001c75ec5b668
map_dbl(qs, eval_tidy)
#> global      f 
#>      0      1

Under the hood

Quosures 数据结构受R中的“formulas”启发,因为“formula”同样也是同时捕获“表达式”与“环境”。早期也确实使用“formula”来进行评估,但因为无法简单的将~变为准引用函数,所以放弃使用“formula”。

f <- ~ runif(3)
str(f)
#> Class 'formula'  language ~runif(3)
#>   ..- attr(*, ".Environment")=<environment: R_GlobalEnv>

Quosures 同样也是“formula”的子类:

q4 <- new_quosure(expr(x + y + z))
class(q4)
#> [1] "quosure" "formula"

这意味着一些函数可以直接作用于Quosures:

is_call(q4)
#> [1] TRUE

q4[[1]]
#> Warning: Subsetting quosures with `[[` is deprecated as of rlang 0.4.0
#> Please use `quo_get_expr()` instead.
#> This warning is displayed once every 8 hours.
#> `~`
q4[[2]]
#> x + y + z

有一个用于存放环境的属性:

attr(q4, ".Environment")
#> <environment: R_GlobalEnv>

但是不建议使用上面的函数,而是使用get_expr()get_env()来获取表达式和环境:

get_expr(q4)
#> x + y + z
get_env(q4)
#> <environment: R_GlobalEnv>

Nested quosures

准引用支持在“表达式”中引入quosures,这是一种高级技术,使得创建嵌套quosures变得可能。例如下面的“表达式”中嵌套了两个短语。

q2 <- new_quosure(expr(x), env(x = 1))
q3 <- new_quosure(expr(x), env(x = 10))

x <- expr(!!q2 + !!q3)

它可以被正确地评估,但是如果打印它,你会发现它的“formula”形式:

eval_tidy(x)
#> [1] 11
x
#> (~x) + ~x

可以使用rlang::expr_print()来更好的展示,在终端中根据不同环境源显示不同颜色:

expr_print(x)
#> (^x) + (^x)

Data masks

本节介绍数据掩码(data mask)相关内容,这是一种同时在“环境”与“数据框构成的环境”中评估“表达式”的技术。它的核心思想与base R中的with(),subset()transform()类似,被广泛应用在“tidyverse”系列包中。

Note

注意:enquo()保证了能在不同环境中正确评估“表达式”中的变量,expr()不能,这是一个重要的区别。但是本节所有示例中的enquo()expr()都可以替换,不影响结果。

Basics

数据掩码允许你混合环境来源和数据框来源的变量。你可以将数据框当作环境变量传递给eval_tidy()的第二个参数。

q1 <- new_quosure(expr(x * y), env(x = 100))
df <- data.frame(y = 1:10)

eval_tidy(q1, df)
#>  [1]  100  200  300  400  500  600  700  800  900 1000

上面的代码可能有些难以理解,我们可以作一些拆分:

x <- 100
df <- data.frame(y = 1:10)
eval_tidy(expr(x * y), df)
#>  [1]  100  200  300  400  500  600  700  800  900 1000

稍加修改,改写为类似base::with()的函数:

with2 <- function(data, expr) {
  expr <- enquo(expr)
  eval_tidy(expr, data)
}

with2(df, x * y)
#>  [1]  100  200  300  400  500  600  700  800  900 1000

base::eval()可以实现类似的效果,传递数据框到第二个参数,环境到第三个参数。

with3 <- function(data, expr) {
  expr <- substitute(expr)
  eval(expr, data, caller_env())
}

Pronouns

数据掩码会引起歧义。例如,在以下代码中,除非你知道df中包含哪些变量,否则你无法知道x是来自数据掩码还是环境。

with2(df, x)
#> [1] 100

为了解决歧义问题,数据掩码提供了两个声明:.data.env

  • .data$x 表示数据掩码中的变量x
  • .env$x 表示环境中的变量x
x <- 1
df <- data.frame(x = 2)

with2(df, .data$x)
#> [1] 2
with2(df, .env$x)
#> [1] 1

对于两个声明,你可以使用[[,但是要注意它们是特殊的对象,和真实的数据框、环境不同。例如,如果找不到变量,它会抛出错误:

df$y
#> NULL
with2(df, .data$y)
#> Error in `.data$y`:
#> ! Column `y` not found in `.data`.

Application: subset()

下面是subset()的使用场景之一:直接通过某个“表达式”进行过滤数据框的行。

sample_df <- data.frame(a = 1:5, b = 5:1, c = c(5, 3, 1, 4, 1))

# Shorthand for sample_df[sample_df$a >= 4, ]
subset(sample_df, a >= 4)
#>   a b c
#> 4 4 2 4
#> 5 5 1 1

# Shorthand for sample_df[sample_df$b == sample_df$c, ]
subset(sample_df, b == c)
#>   a b c
#> 1 1 5 5
#> 5 5 1 1

subset()的核心逻辑是:

  • 两个参数:数据框data和“表达式”rows
  • 在数据框data中,评估rows,并返回结果逻辑向量。
  • 根据逻辑向量,返回数据框的行。
subset2 <- function(data, rows) {
  rows <- enquo(rows)
  rows_val <- eval_tidy(rows, data)
  stopifnot(is.logical(rows_val))

  data[rows_val, , drop = FALSE]
}

subset2(sample_df, a >= 4)
#>   a b c
#> 4 4 2 4
#> 5 5 1 1

Application: transform()

transform()函数类似dplyr::mutate(),可以在数据框中添加新的一列。

df <- data.frame(x = c(2, 3, 1), y = runif(3))
transform(df, x = -x, y2 = 2 * y)
#>    x         y        y2
#> 1 -2 0.9811686 1.9623372
#> 2 -3 0.4927797 0.9855595
#> 3 -1 0.2881747 0.5763493

下面是transform()的简单等价实现:

transform2 <- function(.data, ...) {
  dots <- enquos(...)

  for (i in seq_along(dots)) {
    name <- names(dots)[[i]]
    dot <- dots[[i]]

    .data[[name]] <- eval_tidy(dot, .data)
  }

  .data
}

transform2(df, x2 = x * 2, y = -y)
#>   x          y x2
#> 1 2 -0.9811686  4
#> 2 3 -0.4927797  6
#> 3 1 -0.2881747  2

Application: select()

数据掩码不总是作用于数据框,也可以是list。这是subset()的另一个使用场景——根据“表达式”选择某些列——的底层逻辑。

df <- data.frame(a = 1, b = 2, c = 3, d = 4, e = 5)
subset(df, select = b:d)
#>   b c d
#> 1 2 3 4

它的关键思想是创建一个有name属性的list,list的每个元素是对应列的位置索引。

vars <- as.list(set_names(seq_along(df), names(df)))
str(vars)
#> List of 5
#>  $ a: int 1
#>  $ b: int 2
#>  $ c: int 3
#>  $ d: int 4
#>  $ e: int 5

然后在list中进行评估,返回位置索引:

select2 <- function(.data, ...) {
  dots <- enquos(...)

  vars <- as.list(set_names(seq_along(.data), names(.data)))
  cols <- unlist(map(dots, eval_tidy, vars))

  .data[, cols, drop = FALSE]
}
select2(df, b:d)
#>   b c d
#> 1 2 3 4

Using tidy evaluation

本节将会给出一些使用tidy evaluation的函数例子。

Quoting and unquoting

嵌套函数传递“表达式”时,函数内部一定要先使用enquo()引用“表达式”,然后再使用!!解引用。

假设有这样一个随机排序数据框的函数:

resample <- function(df, n) {
  idx <- sample(nrow(df), n, replace = TRUE)
  df[idx, , drop = FALSE]
}

现在要结合上面的subset2()函数,同时实现筛选和随机排序:

subsample <- function(df, cond, n = nrow(df)) {
  df <- subset2(df, cond)
  resample(df, n)
}

df <- data.frame(x = c(1, 1, 1, 2, 2), y = 1:5)
rm(x)
subsample(df, x == 1)
#> Error: object 'x' not found

由于subsample()函数没有对cond进行引用,所以导致subset2()无法正确评估x。这种嵌套传递“表达式”需要“引用-解引用”式的中间步骤。

subsample <- function(df, cond, n = nrow(df)) {
  cond <- enquo(cond)
  df <- subset2(df, !!cond)
  resample(df, n)
}

subsample(df, x == 1)
#>     x y
#> 1   1 1
#> 1.1 1 1
#> 2   1 2

Handling ambiguity

当既有指向数据框的参数也有指向环境的参数时,会导致引用歧义,产生不符合预期的结果。

假设现在有一个根据提供的参数值来过滤数据框的函数。

threshold_x <- function(df, val) {
  subset2(df, x >= val)
}
  • x在环境中存在,但不在数据框中时:
x <- 10
no_x <- data.frame(y = 1:3)
threshold_x(no_x, 2)
#>   y
#> 1 1
#> 2 2
#> 3 3
  • 当数据框中有val列时:
has_val <- data.frame(x = 1:3, val = 9:11)
threshold_x(has_val, 2)
#> [1] x   val
#> <0 rows> (or 0-length row.names)

特殊情况会产生不符合预期的结果,所以我们需要声明参数来源。

threshold_x <- function(df, val) {
  subset2(df, .data$x >= .env$val)
}

x <- 10
threshold_x(no_x, 2)
#> Error in `.data$x`:
#> ! Column `x` not found in `.data`.
threshold_x(has_val, 2)
#>   x val
#> 2 2  10
#> 3 3  11

通常使用.env声明的参数也可以使用!!来替代:

threshold_x <- function(df, val) {
  subset2(df, .data$x >= !!val)
}

二者的区别在于何时评估参数val:如果使用!!val会被enquo()评估,如果使用.envval会被eval_tidy()评估。这种差别的影响微乎其微。

Quoting and ambiguity

Warning

本小节的内容,没有搞懂作者的意图。

将上面的threshold_x()函数的筛选列由固定的x改为参数var提供,可以使用.data[[var]]来访问列:

threshold_var <- function(df, var, val) {
  var <- as_string(ensym(var))
  subset2(df, .data[[var]] >= !!val)
}

df <- data.frame(x = 1:10)
threshold_var(df, x, 8)
#>     x
#> 8   8
#> 9   9
#> 10 10

也可以使用enquo()!!来处理列:

threshold_expr <- function(df, expr, val) {
  expr <- enquo(expr)
  subset2(df, !!expr >= !!val)
}

threshold_expr(df, x, 8)
#>     x
#> 8   8
#> 9   9
#> 10 10

Base evaluation

本节介绍base R中替代tidy evaluation的两种常用函数:

  • substitute()和在调用环境中评估(base::subset()使用的函数)。

  • match.call()控制调用和在调用环境中评估(stats::lm()使用的函数)。

substitute()

base R中最常见的非标准性评估(NSE)模式是substitute() + eval()。下面是使用这种模式编写的subset()。二者的主要区别是评估的环境不同,前者在调用环境中评估,后者在“表达式”定义时的环境中评估。

subset_base <- function(data, rows) {
  rows <- substitute(rows)
  rows_val <- eval(rows, data, caller_env())
  stopifnot(is.logical(rows_val))

  data[rows_val, , drop = FALSE]
}

subset_tidy <- function(data, rows) {
  rows <- enquo(rows)
  rows_val <- eval_tidy(rows, data, env = caller_env())
  stopifnot(is.logical(rows_val))

  data[rows_val, , drop = FALSE]
}

Programming with subset()

subset()的文档中由这样的警告:

Warning

This is a convenience function intended for use interactively. For programming it is better to use the standard subsetting functions like [, and in particular the non-standard evaluation of argument subset can have unanticipated consequences.

它存在三个主要问题:

  • base::subset()总是在调用环境中评估rows,这可能会导致评估错误。

    f1 <- function(df, ...) {
      xval <- 3
      subset_base(df, ...)
      # subset_tidy(df, ...)
    }
    
    my_df <- data.frame(x = 1:3, y = 3:1)
    xval <- 1
    f1(my_df, x == xval)
    #>   x y
    #> 3 3 1

    这也意味着subset_base()类型的函数不能与map()lapply()一起使用。

    local({
      zzz <- 2
      dfs <- list(data.frame(x = 1:3), data.frame(x = 4:6))
      lapply(dfs, subset_base, x == zzz)
    })
    #> Error in eval(rows, data, caller_env()): object 'zzz' not found
  • 从另一个函数调用subset()需要注意:你必须使用substitute()来捕获对substitute()完整表达式的调用,然后进行求值。这段代码很难理解,因为substitute()没有使用语法标记来取消引用。

    f2 <- function(df1, expr) {
      call <- substitute(subset_base(df1, expr))
      expr_print(call)
      eval(call, caller_env())
    }
    
    my_df <- data.frame(x = 1:3, y = 3:1)
    f2(my_df, x == 1)
    #> subset_base(my_df, x == 1)
    #>   x y
    #> 1 1 3
  • eval()不提供任何“声明”,所以无法准确区分“表达式”来源。据我所知,除非手动检查df中是否存在z变量,否则无法确保以下函数的安全。

    f3 <- function(df) {
      call <- substitute(subset_base(df, z > 0))
      expr_print(call)
      eval(call, caller_env())
    }
    
    my_df <- data.frame(x = 1:3, y = 3:1)
    z <- -1
    f3(my_df)
    #> subset_base(my_df, z > 0)
    #> [1] x y
    #> <0 rows> (or 0-length row.names)

What about [?

既然tidy-eval很复杂,为什么不直接使用[?首先,[只能交互使用,不能应用在函数中,会不具有通用性。其次,相较于[subset()有两个有点:

  • 它默认设置了drop = FALSE,保证返回的始终是数据框。

  • 它丢掉了条件是NA的行。

这意味着,subset(df, x == y) 不等于df[x == y, ],而是等价于df[x == y & !is.na(x == y), , drop = FALSE]。而且,类似subset()的函数,如dplyr::filter()甚至可以将R语言的过滤规则转化为SQL语言,使得它在编程方面应用广泛。

match.call()

base R中另外一种NSE模式是使用match.call()。与substitute()类似,都会捕获”表达式“,但match.call()会捕获完整的调用”表达式“,并修改然后评估它。”rlang“中没有与其等价的函数。

g <- function(x, y, z) {
  match.call()
}
g(1, 2, z = 3)
#> g(x = 1, y = 2, z = 3)

match.call()的一个重要有应用是write.csv()write.csv()捕获调用write.table()的表达式,然后修改参数,最后评估:

write.csv <- function(...) {
  call <- match.call(write.table, expand.dots = TRUE)

  call[[1]] <- quote(write.table)
  call$sep <- ","
  call$dec <- "."

  eval(call, parent.frame())
}

但是也可以不使用NSE直接实现这种功能:

write.csv <- function(...) {
  write.table(..., sep = ",", dec = ".")
}

Wrapping modelling functions

match.call()的另一个重要应用是lm()。但这一技术同时也导致打印捕获的”formula“不完整。让我们思考下面这一简单地对lm()的包装:

lm2 <- function(formula, data) {
  lm(formula, data)
}

这个包装函数可以成功运行,但无法准确捕获到”formula“:

lm2(mpg ~ disp, mtcars)
#> 
#> Call:
#> lm(formula = formula, data = data)
#> 
#> Coefficients:
#> (Intercept)         disp  
#>    29.59985     -0.04122

为了修复这一错误,我们需要使用准引用技术。

lm3 <- function(formula, data, env = caller_env()) {
  formula <- enexpr(formula)
  data <- enexpr(data)

  lm_call <- expr(lm(!!formula, data = !!data))
  expr_print(lm_call)
  eval(lm_call, env)
}
lm3(mpg ~ disp, mtcars)
#> lm(mpg ~ disp, data = mtcars)
#> 
#> Call:
#> lm(formula = mpg ~ disp, data = mtcars)
#> 
#> Coefficients:
#> (Intercept)         disp  
#>    29.59985     -0.04122

当你想要封装一个base R中的NSE函数时,你需要注意下面三点:

  • 使用enexpr()捕获未评估的参数,使用caller_env()获取调用函数的env。

  • 使用expr()!!组合新的调用“表达式”。

  • 要在caller_env()中评估新的“表达式”。

Evaluation environment

如果在lm3()中,对data进行了某种处理,如resample(),那么会导致data的环境由外部的调用环境变为内部的运行环境,最终导致报错。

resample_lm0 <- function(formula, data, env = caller_env()) {
  formula <- enexpr(formula)
  resample_data <- resample(data, n = nrow(data))

  lm_call <- expr(lm(!!formula, data = resample_data))
  expr_print(lm_call)
  eval(lm_call, env)
}

df <- data.frame(x = 1:10, y = 5 + 3 * (1:10) + round(rnorm(10), 2))
resample_lm0(y ~ x, data = df)
#> lm(y ~ x, data = resample_data)
#> Error in eval(mf, parent.frame()): object 'resample_data' not found

有两种方法可以避免报错:

  • 直接将data解引用进行传递,但这会导致打印出的data很奇怪:

    resample_lm1 <- function(formula, data, env = caller_env()) {
      formula <- enexpr(formula)
      resample_data <- resample(data, n = nrow(data))
    
      lm_call <- expr(lm(!!formula, data = !!resample_data))
      expr_print(lm_call)
      eval(lm_call, env)
    }
    resample_lm1(y ~ x, data = df)$call
    #> lm(y ~ x, data = <df[,2]>)
    #> lm(formula = y ~ x, data = list(x = c(10L, 7L, 6L, 8L, 7L, 6L, 
    #> 9L, 5L, 7L, 6L), y = c(34.45, 24.36, 22.83, 28.5, 24.36, 22.83, 
    #> 32.83, 19.01, 24.36, 22.83)))
  • 将修改后的data重新添加到caller_env()中:

    resample_lm2 <- function(formula, data, env = caller_env()) {
      formula <- enexpr(formula)
      resample_data <- resample(data, n = nrow(data))
    
      lm_env <- env(env, resample_data = resample_data)
      lm_call <- expr(lm(!!formula, data = resample_data))
      expr_print(lm_call)
      eval(lm_call, lm_env)
    }
    resample_lm2(y ~ x, data = df)
    #> lm(y ~ x, data = resample_data)
    #> 
    #> Call:
    #> lm(formula = y ~ x, data = resample_data)
    #> 
    #> Coefficients:
    #> (Intercept)            x  
    #>       4.961        2.915
Back to top