【问题标题】:Recommended way of creating reusable objects within an R function在 R 函数中创建可重用对象的推荐方法
【发布时间】:2020-12-01 18:16:27
【问题描述】:

假设我们有以下数据:

# simulate data to fit
set.seed(21)
y = rnorm(100)
x = .5*y + rnorm(100, 0, sqrt(.75))

我们还假设用户已经拟合了一个模型:

# user fits a lm
mod = lm(y~x)

现在假设我有一个 R 包,旨在对对象 mod 执行多项操作。为简单起见,假设我们有两个函数,一个绘制数据,一个计算系数。但是,作为中介,假设我们要对数据执行一些操作(在本例中,加十)。

例子:

# function that adds ten to all scores
add_ten = function(model) {
  data = model$model
  data = data + 10
  return(data)
}

# functions I defined that do something to the "add_ten" dataset
plot_ten = function(model) {
  new_data = data.frame(add_ten(model))
  x = all.vars(formula(model))[2]
  y = all.vars(formula(model))[1]
  ggplot2::ggplot(new_data, aes_string(x=x, y=y)) + geom_point() + geom_smooth()
}

coefs_ten = function(model) {
  new_data = data.frame(add_ten(model))
  coef(lm(formula(model), new_data))
}

(显然,这样做很愚蠢。实际上,我要执行的操作是多重插补,计算量很大)。

请注意,在上面的示例中,我必须调用 add_ten 函数两次,一次用于 plot_ten,一次用于 coefs_ten。这是低效的。

那么,现在我的问题是,在函数中创建可重用对象的最佳方法是什么?

我当然可以,create an object to be placed in the user's global environment:

add_ten = function(model) {
  # check for add_ten_data in the global environment
  if (exists("add_ten_data", where = .GlobalEnv)) return(get("add_ten_data", envir = .GlobalEnv))
  data = model$model
  data = data + 10
  # assign add_ten_data to the global environment
  assign('add_ten_data', data, envir = .GlobalEnv)
  return(data)
}

我很乐意这样做,但担心将某些东西放在用户环境中的“网络礼节”。如果用户在他们的环境中碰巧有一个名为“add_ten_data”的对象,也会有一个潜在的问题。

那么,最好的方法是什么?

提前致谢!

【问题讨论】:

  • 调用add_ten() 函数一次,然后将结果传递给plot_tencoefs_ten 函数。函数不应创建全局变量,并且函数假定某些全局变量存在并不是一个好主意。
  • 同意。通常认为函数是自包含的最佳实践。传入输入,并返回结果。这可以防止奇怪的行为,并使以后更容易更新代码。所以将new_data = data.frame(add_ten(model)) 移到你的函数之外。然后通过传入new_data 而不是传入model 来运行这些函数。如果你愿意,你可以在完成后删除 new_data。
  • 感谢 cmets。我希望避免在用户端采取额外的步骤,但如果不违反最佳实践,这似乎是不可避免的。

标签: r environment-variables


【解决方案1】:

您当然应该避免将对象写入全局环境。如果您发现必须在多个不同函数的顶部重复相同的计算量大的任务,这意味着您执行计算量大的任务为时已晚。

例如,您可以创建一个包含必要组件的 S3 类,以生成“廉价”图和“廉价”系数提取。它甚至具有通用调度的好处:

add_ten <- function(model) model$model + 10

lm_tens <- function(formula, data)
{
  model <- if(missing(data)) lm(formula) else lm(formula, data = data)
  
  structure(list(data = data.frame(add_ten(model)), model = model),
            class = "tens")
}

plot.tens <- function(tens) {
  x = all.vars(formula(tens$data))[2]
  y = all.vars(formula(tens$data))[1]
  ggplot2::ggplot(tens$data, ggplot2::aes(x = x, y = y)) + 
    ggplot2::geom_point() + 
    ggplot2::geom_smooth()
}

coef.tens = function(tens) {
  coef(lm(formula(tens$model), data = tens$data))
}

所以现在我们只需要这样做:

set.seed(21)
y = rnorm(100)
x = .5*y + rnorm(100, 0, sqrt(.75))

mod <- lm_tens(y ~ x)
coef(mod)
#> (Intercept)           x 
#>   4.3269914   0.5775404
plot(mod)
#> `geom_smooth()` using method = 'loess' and formula 'y ~ x'

请注意,这里我们只需要调用一次add_ten

【讨论】:

  • 我见过函数做这样的事情(例如,mice 包和 norm 包)并且总是发现两阶段过程有点令人沮丧。但是,我认为一个很好的替代方案类似于您提出的建议:不要要求 lm_tens 函数,但如果他们调用它就使用它(否则,重复 add_ten)。
  • @dfife 这取决于您对该对象的可能用途。我今天从eulerr 包中遇到了一个示例。 euler 类包含几个不同的轻量级字段,可以方便地将它们捆绑到一个类中,但大部分工作在以后完成; plot.euler 函数很昂贵,因此绘制绘图所需的结构仅在调用 plot 时生成。另一方面,大多数回归函数在一开始就完成了计算量大的部分,您可以传递模型,知道以后对其进行任何工作都会很便宜。
  • 让我提供更多细节(以防我忽略了一些明显的解决方案)。我的包接受从 lavaan 包安装的模型。 (lavaan 不是我的包裹)。我的一些函数需要计算标准误差(通过多重插补估计,这是计算密集型的)。我无法将这些标准错误附加到已经估计的 lavaan 模型中,因此我一直在为每个函数计算它们。但是,对于不同的函数,相同的标准误差计算可能会发生多次。因此,关于将它们置于全球环境中的问题:)
  • @dfife 那么为什么不创建一个类来包装 lavaan 对象并将其作为成员保存,但也保存标准错误呢?假设您的类称为“dfife_class”,并且包含一个成员“模型”,它是一个熔岩对象,以及一个具有计算标准误差的成员“SE”。在每个函数的头部检查它是否被传递了一个 lavaan 对象或一个“dfife_class”对象。如果是 lavaan 对象,计算 SE 并将 lavaan 变成“dfife_class”对象。然后编写你的函数来处理“dfife_class”对象
  • 我明白你在说什么。是的,这是一个好主意,并且会节省一个额外的步骤(假设用户适合 lm_tens 而不是 lm)。谢谢!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-07-15
  • 1970-01-01
  • 2020-11-12
  • 1970-01-01
  • 2019-08-12
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多