Skip to content
Switch branches/tags
Go to file
Cannot retrieve contributors at this time
title: 'Many similar models - Part 1: How to make a function for model fitting'
author: Ariel Muldoon
date: '2019-06-24'
slug: function-for-model-fitting
- r
- statistics
- analysis
- teaching
- functions
draft: FALSE
description: "In this post I discuss how to construct the formula that can be passed to model fitting functions like lm(). I then demonstrate how to use this within a user-created function in order to streamline the process of fitting many similar models."
```{r setup, echo = FALSE, message = FALSE, purl = FALSE}
knitr::opts_chunk$set(comment = "#")
filename = 'render_toc.R')
I worked with several students over the last few months who were fitting many linear models, all with the same basic structure but different response variables. They were struggling to find an efficient way to do this in R while still taking the time to check model assumptions.
A first step when working towards a more automated process for fitting many models is to learn how to build model formulas with `paste()` and `as.formula()`. Once we learn how to build model formulas we can create functions to streamline the model fitting process.
I will be making residuals plots with **ggplot2** today so will load it here.
```{r packages, warning = FALSE, message = FALSE}
library(ggplot2) # v.3.2.0
## Table of Contents
```{r toc, echo = FALSE, purl = FALSE}
# Building a formula with paste() and as.formula()
Model formula of the form `y ~ x` can be built based on variable names passed as *character strings*. A character string means the variable name will have quotes around it.
The function `paste()` is great for the job of concatenating strings together. (Also see `glue::glue()`.)
Here is an example, using `mpg` as the response variable and `am` as the explanatory variable. These need to be separated by a tilde, `~`. In this example I paste the character string `"mpg"` to the character string `"~ am"`.
The output of this is the formula as a string. You can see it is in quotes in the output.
```{r paste1}
paste("mpg", "~ am")
We turn this into a formula ready to be passed to a model function using `as.formula()`.
```{r formula1}
as.formula( paste("mpg", "~ am") )
# Using a constructed formula in lm()
Once we've built the formula we can put it in as the first argument of a model fitting function like `lm()` in order to fit the model. I'll be using the `mtcars` dataset throughout the model fitting examples.
Since `am` is a 0/1 variable, this particular analysis is a two-sample t-test with `mpg` as the response variable.
```{r lm1}
lm( as.formula( paste("mpg", "~ am") ), data = mtcars)
# Making a function for model fitting
This `paste()` / `as.formula()` combination is essential for making user-defined model fitting functions.
For example, say I wanted to do the same t-test with `am` for many response variables. I could create a function that takes the response variable as an argument and build the model formula within the function with `paste()` and `as.formula()`.
The response variable name is passed to the `response` argument as a character string.
```{r fun1}
lm_fun = function(response) {
lm( as.formula( paste(response, "~ am") ), data = mtcars)
Here are two examples of this function in action, using `mpg` and then `wt` as the response variables.
```{r usefun1}
lm_fun(response = "mpg")
lm_fun(response = "wt")
# Using bare names instead of strings (i.e., non-standard evaluation)
As you can see, this approach to building formula relies on character strings. This is going to be great once we start looping through variable names, but if making a function for interactive use it can be nice for the user to pass bare column names.
We can use some `deparse()`/`substitute()` magic in the function for this. Those two functions will turn bare names into strings within the function rather than having the user pass strings directly.
```{r fun2}
lm_fun2 = function(response) {
resp = deparse( substitute( response) )
lm( as.formula( paste(resp, "~ am") ), data = mtcars)
Here's an example of this function in action. Note the use of the bare column name for the response variable.
```{r usefun2}
lm_fun2(response = mpg)
You can see that one thing that happens when using `as.formula()` like this is that the formula in the model output shows the formula-building code instead of the actual variables used in the model.
lm(formula = as.formula(paste(response, "~ am")), data = mtcars)
While this often won't matter in practice, there are ways to force the model to show the variables used in the model fitting. See [this blog post]( for some discussion as well as code for how to do this.
# Building a formula with varying explanatory variables
The formula building approach can also be used for fitting models where the explanatory variables vary. The explanatory variables should have plus signs between them on the right-hand side of the formula. To get plus signs between a bunch of variables we can use the `collapse` argument in `paste()`.
Here's an example with a vector of two explanatory variables. Passing a vector of variable names to `paste()` and using `collapse = "+"` puts plus signs between the variables.
```{r expl}
expl = c("am", "disp")
paste(expl, collapse = "+")
This additional pasting step can then be included within `as.formula()`.
```{r explform}
as.formula( paste("mpg ~", paste(expl, collapse = "+") ) )
Let's go through an example of using this in a function that can fit a model with different explanatory variables.
In this function I demonstrate building the formula as a separate step and then passing it to `lm()`. This can be easier to read compared to building the formula within `lm()` as a single step like I did earlier.
```{r explfun}
lm_fun_expl = function(expl) {
form = as.formula( paste("mpg ~ ", paste(expl, collapse = "+") ) )
lm(form, data = mtcars)
To use the function we pass a vector of variable names as strings to the `expl` argument.
```{r useexplfun}
lm_fun_expl(expl = c("am", "disp") )
# The dots for passing many variables to a function
Using dots (...) instead of named arguments can allow the user to list the explanatory variables separately instead of in a vector.
I'll demonstrate a function using dots to indicate some undefined number of additional arguments for putting as many explanatory variables as desired into the model. I wrap the dots in `c()` within the function in order to collapse variables together with `+`.
Note that if you want non-standard evaluation you could wrap the `...` in `rlang::exprs()` within the function and then pass bare variable names.
```{r explfun2}
lm_fun_expl2 = function(...) {
form = as.formula( paste("mpg ~ ", paste( c(...), collapse = "+") ) )
lm(form, data = mtcars)
Now variables are passed individually as strings separated by commas instead of as a vector.
```{r useexplfun2}
lm_fun_expl2("am", "disp")
# Example function that returns residuals plots and model output
One of the reasons to make a function is to increase efficiency when fitting many models. For example, it might be useful to make a function that returns residual plots and any desired statistical results simultaneously.
Here's an example of such a function, using some of the tools covered above. The function takes the response variable as a bare name, fits a model with `am` hard-coded as the explanatory variable and the `mtcars` dataset, and then makes two residual plots.
The function outputs a list that contains the two residuals plots as well as the overall $F$ tests from the model.
```{r fullfun}
lm_modfit = function(response) {
resp = deparse( substitute( response) )
mod = lm( as.formula( paste(resp, "~ am") ), data = mtcars)
resvfit = qplot(x = mod$fit, y = mod$res) + theme_bw()
resdist = qplot(x = "Residual", mod$res, geom = "boxplot") + theme_bw()
list(resvfit, resdist, anova(mod) )
mpgfit = lm_modfit(mpg)
Individual parts of the output list can be extracted as needed. To check model assumptions prior to looking at any results we'd pull out the two plots, which are the first two elements of the output list.
```{r fullfunout1}
If we deem the model fit acceptable we can extract the overall $F$ tests from the third element of the output.
```{r fullfunout2}
# Next step: looping
This post focused on using `paste()` and `as.formula()` for building model formula and then making user-defined functions for interactive use. When working with many models we'd likely want to automate the process more by using some sort of looping. I wrote a follow-up post on looping through variables and fitting models with the `map` family of functions from package **purrr**, which you can see [here](
# Just the code, please
```{r getlabels, echo = FALSE, purl = FALSE}
labs = knitr::all_labels()
labs = labs[!labs %in% c("setup", "toc", "getlabels", "allcode", "makescript")]
```{r makescript, include = FALSE, purl = FALSE}
options(knitr.duplicate.label = "allow") # Needed to purl like this
input = knitr::current_input() # filename of input document
output = here::here("static", "script", paste(tools::file_path_sans_ext(input), "R", sep = ".") )
knitr::purl(input, output, documentation = 0, quiet = TRUE)
Here's the code without all the discussion. Copy and paste the code below or you can download an R script of uncommented code [from here](/script/2019-06-24-how-to-make-a-function-for-model-fitting.R).
```{r allcode, ref.label = labs, eval = FALSE, purl = FALSE}