I'm trying to write some functions to ease refitting multiple models, but find it painful, as R is unable to locate proper data, when it plunges deeper into evaluation tree.
Despite an effort was made to store the formula environment inside the model, I guess there's really no way to unambiguously point to the raw data object.
This becomes even harder for fitting survival curves using survfit, where no terms object is being stored inside.
Do I really need to retype the data/formula as a parameter each time?
Example:
# model-fitting wrapper function
fn <- function(fn_formula, fn_data) {
lm(formula = fn_formula, data = fn_data)
}
# specify exemplary data and formula
data <- data.frame(
y = rnorm(100),
x1 = rnorm(100),
x2 = rnorm(100))
formula <- y ~ x1
# try to create and update the fit with different parameters
fn_fit <- fn(formula, data)
update(fn_fit, ~ x2)
# Error in is.data.frame(data) : object 'fn_data' not found
terms(fn_fit) %>% attr('.Environment')
# <environment: R_GlobalEnv>
terms(fn_fit$model) %>% attr('.Environment')
# <environment: R_GlobalEnv>
getCall(fn_fit)
# lm(formula = fn_formula, data = fn_data)
The variable that stores the data should be in the same scope for both the lm() and update() with the same name. Not sure what you are really trying to accomplish, bit if you want a function that creates a signature you can use in the global environment, you can do something like this would work
fn <- function(fn_formula, fn_data) {
do.call("lm", list(fn_formula, data=substitute(fn_data)))
}
fn_fit <- fn(formula, data)
update(fn_fit, ~ x2)
Otherwise if you really wanted to capture that variable in the local function scope, you can create a helper to fun update in the correct environment.
fn <- function(fn_formula, fn_data) {
environment(fn_formula) <- environment()
lm(formula = fn_formula, data = fn_data)
}
fn_update <- function(object, ...) {
mc<-match.call(definition = update)
mc[[1]] <- quote(update)
eval(mc, envir=environment(terms(object)))
}
fn_fit <- fn(formula, data)
fn_update(fn_fit, ~x2)
When you passed formula, the only items stored in the ['model'] sublist were those that were needed.
> names(fn_fit$model)
[1] "y" "x1"
But there's nothing named either 'data' or 'fn_data' in that object. MrFlick second suggestion is more resilient to modifications in the calling tree of frames:
> fn <- function(fn_formula, fn_data) {
+ do.call("lm", list(fn_formula, data=substitute(fn_data)))
+ }
> fn_fit <- fn(formula, data); rm(data) # mess with the calling environment
> update(fn_fit, ~ x2)
Error in terms.formula(formula, data = data) :
'data' argument is of the wrong type
That error occurred because the R interpreter only found the function named data; if instead you deploy the second option you get:
> data <- data.frame(
+ y = rnorm(100),
+ x1 = rnorm(100),
+ x2 = rnorm(100))
> fn <- function(fn_formula, fn_data) {
+ environment(fn_formula) <- environment()
+ lm(formula = fn_formula, data = fn_data)
+ }
>
> fn_update <- function(object, ...) {
+ mc<-match.call(definition = update)
+ mc[[1]] <- quote(update)
+ eval(mc, envir=environment(terms(object)))
+ }
>
> fn_fit <- fn(formula, data) ; rm(data)
> fn_update(fn_fit, ~x2)
Call:
lm(formula = y ~ x2, data = fn_data)
Coefficients:
(Intercept) x2
0.01117 -0.13004
Related
I have been stymied by an error that traces back to predict.lme, running inside a function, failing to interpret a formula based on a variable that has been passed from outside the function. I know the issue has to do with variable scope and different environments, but I've been unable to fully understand it or find a workaround. Your help would be much appreciated.
Here's a reproducible example:
# This will be the nested function.
train_test_perf <- function(train_data, test_data, model, termLabels) {
fixForm <- reformulate(termlabels=termLabels, response="Y")
fit <- nlme::lme(fixForm, data=train_data, random=~ 1|ID)
train_pred <- predict(fit, newdata=train_data, level=0, na.action=na.exclude)
rtrain <- cor.test(train_data$Y, train_pred)
test_pred <- predict(fit, newdata=test_data, level=0, na.action=na.exclude)
rtest <- cor.test(test_data$Y, test_pred)
tmp <- data.frame(Model=model,
R_train=rtrain$estimate,
R_test=rtest$estimate)
return(tmp)
}
# And here is the function that calls it.
myfunc <- function(df, newdf, varList) {
for (v in varList) {
perf <- train_test_perf(train_data=df, test_data=newdf, model=v, termLabels=v)
print(perf)
}
}
# The outer function call.
myfunc(df=dat, newdf=newdat, varList=list("W", "X"))
Running this gives the following error and traceback:
Error in eval(mCall$fixed) : object 'fixForm' not found
7.
eval(mCall$fixed)
6.
eval(mCall$fixed)
5.
eval(eval(mCall$fixed)[-2])
4.
predict.lme(fit, newdata = train_data, level = 0, na.action = na.exclude)
3.
predict(fit, newdata = train_data, level = 0, na.action = na.exclude)
2.
train_test_perf(train_data = df, test_data = newdf, model = v,
termLabels = v)
1.
myfunc(df = dat, newdf = newdat, varList = list("W", "X"))
It seems clear that predict.lme does not have access to the fixForm variable, but I haven't been able to work out a way to both define a formula based on a variable and have the value accessible to predict.lme. I'm not sure whether the nested function structure is part of the problem here--if it is, I would prefer to find a workaround that would maintain this structure, as my real-life code includes some other things inside myfunc that occur before and after the call to train_test_perf.
Thanks,
Jeff Phillips
Using a variable as formula doesn't stores the variable not the formula which might be the issue. We can use a do.call.
train_test_perf <- function(train_data, test_data, model, termLabels) {
fixForm <- reformulate(termlabels=termLabels, response="Y")
fit <- do.call(nlme::lme, list(fixForm, data=quote(train_data), random=~ 1|ID))
train_pred <- predict(fit, newdata=train_data, level=0, na.action=na.exclude)
rtrain <- cor.test(train_data$Y, train_pred)
test_pred <- predict(fit, newdata=test_data, level=0, na.action=na.exclude)
rtest <- cor.test(test_data$Y, test_pred)
tmp <- data.frame(Model=model, R_train=rtrain$estimate,
R_test=rtest$estimate)
return(tmp)
}
Finally put it in an sapply to avoid tedious for loops.
t(sapply(c("W", "X"), \(x) train_test_perf(train_data=dat, test_data=newdat, model=x, termLabels=x)))
# Model R_train R_test
# [1,] "W" 0.1686495 -0.001738604
# [2,] "X" 0.4138526 0.2992374
I use speedglm to fit a GLM to data. When I call the function directly, the code works as expected, but when I create a function to fit the model, I get an error that an argument is not found.
The variable (w in the example below) clearly exists in the scope of the function but it seems that the variable is evaluated only later within the speedglm function where w is no longer available or so I think. This is where I start questioning my current understanding of R.
Did I make an error while creating the function, does speedglm use some weird trick to scope the variable (source code here) that breaks the normal (?) logic or do I have a wrong understanding of how R functions work?
I am trying to understand this behavior and also fix my train_glm function to make it work with speedglm and weights.
MWE
library(speedglm)
# works as expected
m1 <- speedglm(wt ~ cyl, data = mtcars, weights = mtcars$wt)
# define a small helper function that just forwards its arguments
train_glm <- function(f, d, w) {
speedglm(formula = f, data = d, weights = w)
}
# does not work
m <- train_glm(wt ~ cyl, d = mtcars, w = mtcars$wt)
#> Error in eval(extras, data, env) : object 'w' not found
Even weirder, if I change the code I found the following
# removing the weights as a base case -> WORKS
train_glm3 <- function(f, d) {
speedglm(formula = f, data = d)
}
m3 <- train_glm3(wt ~ cyl, d = mtcars)
# works
# hardcoding the weights inside the function -> BREAKS
train_glm4 <- function(f, d) {
speedglm(formula = f, data = d, weights = d$wt)
}
m4 <- train_glm4(wt ~ cyl, d = mtcars)
# Error in eval(extras, data, env) : object 'd' not found
# creating a new dataset and hardcoding the weights inside the function
# but using the name of the dataset at the highest environment -> WORKS
train_glm5 <- function(f, d) {
speedglm(formula = f, data = d, weights = mtcars2$wt)
}
mtcars2 <- mtcars
m5 <- train_glm5(wt ~ cyl, d = mtcars2)
# works
The solution (thanks to #Mike for the hint) is to evaluate the code either by using the solution given by this answer or by using do.call like so:
library(speedglm)
train_glm_docall <- function(f, d, w) {
do.call(
speedglm,
list(
formula = f,
data = d,
weights = w
)
)
}
m2 <- train_glm_docall(f = wt ~ cyl, d = mtcars, w = mtcars$wt)
class(m2)
#> [1] "speedglm" "speedlm"
I am attempting to build a general framework for quickly evaluating a variety of models. I am trying to use a factory pattern to generate "model trainer" functions that take a data frame and return a trained model. However, I am running into unexpected behavior of R's built-in lm function within this framework.
gen_lm_model_trainer <- function(formula, weights_col = NULL) {
function(train_data) {
trained_lm <- lm(formula = formula,
data = train_data,
weights = train_data[[weights_col]])
pred_func <- function(test_data) {
prediction <- predict(trained_lm, newdata = test_data)
return(prediction)
}
return(list(predict = pred_func, info = trained_lm))
}
}
mtcars$random_weights <- rbeta(nrow(mtcars), shape1 = 5, shape2 = 2)
trainer <- gen_lm_model_trainer(formula = mpg ~ ., weights_col = 'random_weights')
trained_model <- trainer(mtcars)
The response to this code is the following:
Error in eval(extras, data, env) : object 'train_data' not found
This is similar another SO question, Object not found error when passing model formula to another function, but this problem is not solved by assigning the formula's environment to the generated function's environment, i.e.
gen_lm_model_trainer <- function(formula, weights_col = NULL) {
function(train_data) {
scoped_formula <- as.formula(formula, env = environment())
trained_lm <- lm(formula = scoped_formula,
data = train_data,
weights = train_data[[weights_col]])
pred_func <- function(test_data) {
prediction <- predict(trained_lm, newdata = test_data)
return(prediction)
}
return(list(predict = pred_func, info = trained_lm))
}
}
A solution that works consistently for both problems would be most appreciated.
I have found a partial answer to the question -- partial in that it solves only this case and not the linked SO question. The problem seems to be that lm's arguments are being evaluated in the environment that corresponds to calling with(train_data, lm(...)). It should therefore be safe to use parent.frame() to traverse to the environment of the calling function (the "model trainer"). This happens to correspond to a depth of n = 1 -- in this case, I think that n = 1 is the data frame's environment, n = 2 is eval's environment, and n = 3 is the environment from which lm is being called.
gen_lm_model_trainer <- function(formula, weights_col = NULL) {
function(train_data) {
trained_lm <- lm(formula = formula,
data = train_data,
weights = get('train_data', parent.frame(3))[[get('weights_col', parent.frame(3))]])
pred_func <- function(test_data) {
prediction <- predict(trained_lm, newdata = test_data)
return(prediction)
}
return(list(predict = pred_func, info = trained_lm))
}
}
mtcars$random_weights <- rbeta(nrow(mtcars), shape1 = 5, shape2 = 2)
trainer <- gen_lm_model_trainer(formula = mpg ~ ., weights_col = 'random_weights')
trained_model <- trainer(mtcars)
Why lm changes scope so unusually is unclear to me and seems like a bug.
The following worked for me:
gen_lm_model_trainer <- function(formula, weights_col = NULL) {
function(train_data, .fml = formula, .wts = weights_col) {
w <- train_data[[.wts]]
environment(.fml) <- environment()
trained_lm <- lm(formula = .fml,
data = train_data,
weights = w)
pred_func <- function(test_data) {
predict(trained_lm, newdata = test_data)
}
list(predict = pred_func, info = trained_lm)
}
}
mtcars$random_weights <- rbeta(nrow(mtcars), shape1 = 5, shape2 = 2)
trainer <- gen_lm_model_trainer(formula = mpg ~ ., weights_col = 'random_weights')
trained_model <- trainer(mtcars)
I may have made some cosmetic changes but there are just two real changes:
1)
environment(.fml) <- environment()
# to make sure that the object within the function's scope are accessible
# otherwise it won't find the weights thing but curiously, it can find the data
2) Passing the formula and weights column names as arguments.
I can't quite explain why this combination works ... it's an interesting case. I've generated lm models with a different approach and there's always trouble with it.
For interesting reasons adding
random_weights <- train_data[[weights_col]]
or, more generically
assign(weights_col, train_data[[weights_col]])
to the beginning of your function(train_data) { and passing the random_weights as weights to lm will fix this, resulting function looks like this:
gen_lm_model_trainer <- function(formula, weights_col = NULL) {
function(train_data) {
assign(weights_col, train_data[[weights_col]])
trained_lm <- lm(formula = formula, data = train_data, weights = random_weights)
pred_func <- function(test_data) {
prediction <- predict(trained_lm, newdata = test_data)
return(prediction)
}
return(list(predict = pred_func, info = trained_lm))
}
}
mtcars$random_weights <- rbeta(nrow(mtcars), shape1 = 5, shape2 = 2)
trainer <- local(gen_lm_model_trainer(formula = mpg ~ ., weights_col = 'random_weights'))
trained_model <- trainer(mtcars)
Reasoning:
The underlying reason is that the weights are passed to stats::model.frame.default as part of ... and that gets evaluated separately:
env <- environment(formula)
# ...
# more code
# ...
extras <- substitute(list(...))
extranames <- names(extras[-1L])
extras <- eval(extras, data, env)
I am trying to write my own modeling function in R, one which takes a formula, some data, and maybe some extra context, like weights; after calling model.frame to extract the necessary numeric data, it will perform a fit. My first pass looked like:
my_modfunc <- function(formula,data,weights=NULL) {
mf <- model.frame(formula,data=data,weights=weights)
wt <- model.weights(mf)
# do some fitting here...
}
# make fake data to test it
set.seed(1234)
data <- data.frame(x1=rnorm(50),x2=rnorm(50),y=rnorm(50),w=runif(50))
# call it:
my_modfunc(y ~ x1 + x2,data=data,weights=w)
This fails, I get the error:
Error in model.frame.default(formula, data = data, weights = weights) :
invalid type (closure) for variable '(weights)'
Similarly, if I call
my_modfunc(y ~ x1 + x2,data=data,weights='w')
I get the same error. I suspect there is some problem with environment, quoting, and so on.
Cutting and pasting the source for lm, I could rewrite my function as
# based on lm
weird_modfunc <- function(formula,data,weights=NULL ) {
cl <- match.call() # what?
mf <- match.call(expand.dots = FALSE) # what??
m <- match(c("formula", "data", "weights"), names(mf), 0L)
mf <- mf[c(1L, m)] # ??
mf$drop.unused.levels <- TRUE # ??
mf[[1L]] <- quote(stats::model.frame) ## ???
mf <- eval(mf, parent.frame())
wt <- as.vector(model.weights(mf))
# do some fitting here...
}
# this runs without error:
weird_modfunc(y ~ x1 + x2,data=data,weights=w)
# this fails with the same error as above about variable lengths.
weird_modfunc(y ~ x1 + x2,data=data,weights='w')
The problem is that this contains multiple somewhat mystical incantations that I do not know how to interpret, modify or maintain.
What is the right way to call model.frame? Bonus points for making my function accept both weights=w and weights='w'
Welcome to the joys of non-standard evaluation. I suggest you base your function on the lm approach. It constructs a call to model.frame and evaluates it. That's necessary, because model.frame does non-standard evaluation, i.e., it accepts/expects a symbol for the weights parameter. Furthermore, it also ensures correct scoping regarding the formula's environment.
weird_modfunc <- function(formula,data,weights=NULL ) {
#cl not needed, lm only adds this call to the return object
mf <- match.call(expand.dots = FALSE)
message("Call with ellipses not expanded: ")
#note that there are no ellipses in the function arguments for now,
#but you might want to change that later
print(mf)
#turn weights into symbol if character is passed
if (is.character(mf$weights)) mf$weights <- as.symbol(mf$weights)
m <- match(c("formula", "data", "weights"), names(mf), 0L)
message("Position of formula, data and weights in the call:")
print(m)
mf <- mf[c(1L, m)]
message("New call that only contains what is needed:")
print(mf)
mf$drop.unused.levels <- TRUE
message("Call with argument added:")
print(mf)
mf[[1L]] <- quote(stats::model.frame)
message("Change call to a call to model.frame:")
print(mf)
mf <- eval(mf, parent.frame()) #evaluate call
wt <- as.vector(model.weights(mf))
# do some fitting here...
message("Return value:")
wt
}
# this runs without error:
weird_modfunc(y ~ x1 + x2,data=data,weights=w)
#Call with ellipses not expanded:
#weird_modfunc(formula = y ~ x1 + x2, data = data, weights = w)
#Position of formula, data and weights in the call
#[1] 2 3 4
#New call that only contains what is needed:
#weird_modfunc(formula = y ~ x1 + x2, data = data, weights = w)
#Call with argument added:
#weird_modfunc(formula = y ~ x1 + x2, data = data, weights = w,
# drop.unused.levels = TRUE)
#Change call to a call to model.frame:
#stats::model.frame(formula = y ~ x1 + x2, data = data, weights = w,
# drop.unused.levels = TRUE)
#Return value:
# [1] 0.35299850 0.98095832 0.53888276 0.44403386 0.94936678 0.45248337 0.19062580 0.99160915 0.54845545 0.76881577 0.91342167 0.68211200 0.40725142
#[14] 0.40759230 0.14608279 0.19666771 0.19220934 0.40841440 0.34822131 0.83454285 0.19840001 0.86180531 0.39718531 0.15325377 0.33928338 0.36718044
#[27] 0.42737908 0.18633690 0.65801660 0.92041138 0.73389406 0.88231927 0.95334653 0.19490154 0.47261674 0.38605066 0.37416586 0.02785566 0.92935521
#[40] 0.41052928 0.95584022 0.27215284 0.51724649 0.97830984 0.36969649 0.31043044 0.03420963 0.66756585 0.92091638 0.04498960
#this runs without error too:
weird_modfunc(y ~ x1 + x2,data=data,weights='w')
Here is a simpler version but there might be problems (well, more than usual with non-standard evaluation):
my_modfunc <- function(formula,data,weights=NULL) {
weights <- substitute(weights)
if (!is.symbol(weights)) weights <- as.symbol(weights)
#substitute the symbol into the call:
mf <- eval(substitute(model.frame(formula,data=data,weights=weights)))
wt <- model.weights(mf)
# do some fitting here...
wt
}
my_modfunc(y ~ x1 + x2,data=data,weights=w)
#works
my_modfunc(y ~ x1 + x2,data=data,weights="w")
#works
I have a multiply imputed dataset of class mids. I use the with() function to estimate the m different datasets with the coxph() function.
However, I'm having trouble using the with() function within my own function.
The code below is a simplified example that reproduces the error: Error in Surv(enter,exit,event) object 'enter' not found
list<-"X1+X2"
var.used<-formula(paste("Surv(enter,exit,event)~",list,sep=""))
with.coxph<-function(form,dataset){
with(dataset,coxph(form))
}
with.coxph(var.used,data)
When I simply run the function on its own:
with(dataset, coxph(Surv(enter,exit,event)~X1+X2))
It works fine.
I think the problem is related to the environment where with() is called. I found different posts in here, but I can't seem to make it work. I tried assigning the dataset and the formula to the global environment:
with.coxph2<-function(form,dataset){
assign(".dataset",dataset,envir=.GlobalEnv)
assign(".form",dataset,envir=.GlobalEnv)
with(dataset,coxph(form))
remove(".dataset",dataset,envir=.GlobalEnv)
remove(".form",dataset,envir=.GlobalEnv)
}
with.coxph2(var.used,data)
but this produced the same error.
EDIT
I have attempted to fix the problem as described below. When i simply run the function with out the with() statement it works perfectly.
makeModel<-function(resp, explan, mData) {
mF <- formula(paste(resp, paste(explan, collapse = "+"), sep = "~"))
mod <- coxph(mF, data = mData)
mod$call$formula <- mF
mod$call$data <- as.symbol(deparse(substitute(mData)))
mod
}
cp <- makeModel("Surv(start, stop, event)", "X1", complete(data))
# This works fine
However, I still get the same error when I include the with() statement in the equation:
with.coxph<-function(resp, explan, mData) {
mF <- formula(paste(resp, paste(explan, collapse = "+"), sep = "~"))
mod <- with(mData,coxph(mF))
mod$call$formula <- mF
mod$call$data <- as.symbol(deparse(substitute(mData)))
mod
}
cp <- with.coxph("Surv(start, stop, event)", "X1", data)
# Error in Surv(enter,exit,event): object 'enter' not found
I had similiar issues when using the lm function and I wanted to pass the formula and/or data argument to it. Tha's what I am doing now to circumvent that:
makeModel <- function(resp, explan, mData) {
mF <- formula(paste(resp, paste(explan, collapse = "+"), sep = "~"))
mod <- coxph(mF, data = mData)
mod$call$formula <- mF
mod$call$data <- as.symbol(deparse(substitute(mData)))
mod
}
makeModelBad <- function(resp, explan, mData) {
mF <- formula(paste(resp, paste(explan, collapse = "+"), sep = "~"))
coxph(mF, data = mData)
}
library(survival)
example(coxph) # to load the data
(cp <- makeModel("Surv(start, stop, event)", "x", test2))
# Call:
# coxph(formula = Surv(start, stop, event) ~ x, data = test2)
#
#
# coef exp(coef) se(coef) z p
# x -0.0211 0.979 0.795 -0.0265 0.98
#
# Likelihood ratio test=0 on 1 df, p=0.979 n= 10, number of events= 7
cp.bad <- makeModelBad("Surv(start, stop, event)", "x", test2)
Explanation
In order to use the models created inside a function, I had to explicitely change the respective slots, because mData is not known outside the function and a call to update for example will fail:
update(cp, . ~ 1) # works
update(cp.bad, . ~ 1) # does not work
# Error in terms.formula(formula, special, data = data) :
# object 'mData' not found
The change to the formula slot, is more eye candy to show the formula in the print of the object.