Exercises: Writing functions in R
Exercises: Writing functions in R
In this document we will learn how to write functions in R. You can write your own functions in order to make repetitive operations using a single command.
An ICER function
Let’s start by defining a function and the input parameter(s) that the
user will feed to the function. Afterwards you will define the operation
that you desire to program in the body of the function within curly
braces ({}
). Finally, you need to assign the result (or output) of
your function in the return statement.
We are going to define a function that calculates an ICER
calc_ICER <- function(delta_e, delta_c) {
return(delta_c/delta_e)
}
We define calc_ICER
by assigning it to the output of function
. The
list of argument names are contained within parentheses. Next, the body
of the function–the statements that are executed when it runs–is
contained within curly braces ({}
). The statements in the body are
indented by two spaces, which makes the code easier to read but does not
affect how the code operates.
When we call the function, the values we pass to it are assigned to
those variables so that we can use them inside the function. Inside the
function, we use a return
statement to send a result back to whoever
asked for it.
In R, it is not necessary to include the return
statement. R
automatically returns whichever variable is on the last line of the body
of the function. While in the learning phase, we will explicitly define
the return
statement.
- Let’s try running our function. Calling our own function is no different from calling any other function:
calc_ICER(0.9, 100)
Function composition
Now we can see how to create a function that take the individual costs and effectiveness and creates the incremental values
delta_ce <- function(e1, c1, e0, c0) {
delta_e <- e1 - e0
delta_c <- c1 - c0
return(c(delta_e, delta_c))
}
- Test this
delta_ce(0.9, 100, 0.5, 50)
- What about calculating the ICER from the individual cs and es?
We could write a new function or we could compose the two functions we have already created. That is
ce_to_ICER <- function(e1, c1, e0, c0) {
incr_ce <- delta_ce(e1, c1, e0, c0)
icer <- calc_ICER(incr_ce[1], incr_ce[2])
return(icer)
}
ce_to_ICER(0.9, 100, 0.5, 50)
This is our first taste of how larger programs are built: we define basic operations, then combine them in ever-larger chunks to get the effect we want. Real-life functions will usually be larger than the ones shown here–typically half a dozen to a few dozen lines–but they shouldn’t ever be much longer than that, or the next person who reads it won’t be able to understand what’s going on.
Alternatively to how we have performed the calculation above, we could have nested the functions.
- Naively this may look as follows. Try this
calc_ICER(delta_ce(0.9, 100, 0.5, 50))
Can you see what the problem is?
The output for delta_ce()
, which is vector of two numbers, doesn’t
match with the input for calc_ICER()
, which expects the numbers of two
separate arguments and so it throws an error.
One way around this is for delta_ce
to return a list
object instead
like this
delta_ce2 <- function(e1, c1, e0, c0) {
delta_e <- e1 - e0
delta_c <- c1 - c0
return(list(delta_e = delta_e,
delta_c = delta_c))
}
and then we can use the useful do.call()
function which pass each
element of a list into a function as if they we provided as separate
arguments, just as calc_ICER()
wants them.
do.call(calc_ICER, args = delta_ce2(0.9, 100, 0.5, 50))
Another alternative is to make it so that calc_ICER()
takes the vector
as input.
calc_ICER2 <- function(deltas) {
return(deltas[2]/deltas[1])
}
calc_ICER2(delta_ce(0.9, 100, 0.5, 50))
This sort of fiddly complication is a good example of the kind of design decision that you have to make all the time when writing functions.
Generally speaking, you should be careful not to nest too many function calls at once - it can become confusing and difficult to read!
Pipe operators
A way to make nested functions easier to read is to pipe functions
together, where the output of the left hand function is piped into the
next right hand function. There are two pipe operators in R. The native
base R version is newer and looks like this |>
so
delta_ce(0.9, 100, 0.5, 50) |> calc_ICER2()
The older magrittr
package pipe is used throughout the tidyverse
and
especially in the dplyr
package. This looks like this %>%
which
gives
library(dplyr)
delta_ce(0.9, 100, 0.5, 50) %>% calc_ICER2()
Pipes can make code a lot easier to read and they’re great for data analysis. In packages they can be harder to debug because they chain together multiple operations.
Can you write a function to calculate INMB? What design choices will you make? Can you reuse existing code?
Function factories
We can think of the INMB calculation in the same way as ce_to_ICER()
ce_to_INMB <- function(e1, c1, e0, c0) {
delta_ce(e1, c1, e0, c0) |>
calc_INMB2()
}
When we do this can see the similarity between the different calculations. We shall use this example to demonstrate something called a function factory. A function factory is a function that makes functions.
ce_stat <- function(stat) {
stat_fn <-
if (stat == "INMB") {
calc_INMB2
} else {
calc_ICER2}
function(e1, c1, e0, c0) {
delta_ce(e1, c1, e0, c0) |>
stat_fn()
}
}
INMB_stat <- ce_stat("INMB")
We can see that INMB_stat
is itself a function
INMB_stat
## function(e1, c1, e0, c0) {
## delta_ce(e1, c1, e0, c0) |>
## stat_fn()
## }
## <environment: 0x000002123ab40650>
Now we can use this manufactured function to obtain the output value.
INMB_stat(0.9, 100, 0.5, 50)
Repeat for the ICER using the function factory. Can you include a new statistic?