[1] 5
If you have ever copied a block of R code, pasted it further down your script and changed one variable name, you have already met the problem that functions solve. Copy-paste works for a while. Then you fix a mistake in one copy and forget the other two, and the results from different parts of your script no longer agree.
A function lets you write that block once, give it a name, and run it on any input you like. This post walks through how functions work in R, from a two-line example to a grouped summary with dplyr. You only need to know how to run code in RStudio and what a data frame is.
- what a function is and the parts every R function has
- how arguments, default values and return values work
- why code inside a function does not change your workspace
- when it is worth turning code into a function
- two practical examples: summary statistics for any column, and grouped summaries with dplyr
What is a function?
You use functions all the time already. mean(), sum(), read.csv() and library() are all functions. Each one takes some input, does a job and gives you something back:
Here the input is the vector c(2, 4, 9), the job is “add the numbers and divide by how many there are”, and the output is 5. The inputs a function accepts are called arguments, and the thing it hands back is its return value.
Writing your own function means doing the same thing for a job that R does not have a ready-made function for.
Your first function
Here is a function that adds two numbers:
add_numbers <- function(x, y) {
result <- x + y
return(result)
}Running this chunk prints nothing. That is expected: you have defined the function, the same way a <- 5 defines a variable without printing it. Line by line:
-
add_numbers <-stores the function under a name, just like any other object in R. -
function(x, y)says “this is a function, and it takes two arguments calledxandy”. - Everything between
{and}is the body: the code that runs each time you use the function. -
return(result)sends the value ofresultback to whoever called the function.
Now you can call it:
add_numbers(5, 2)[1] 7
add_numbers(x = 10, y = 3)[1] 13
The second call names the arguments. Both styles work. Naming them makes longer calls easier to read and lets you give them in any order.
You do not always need return()
An R function automatically returns the value of the last line it runs, so this shorter version does exactly the same thing:
add_numbers <- function(x, y) {
x + y
}
add_numbers(5, 2)[1] 7
I still use return() in longer functions, because it makes it obvious where the result comes from. In short functions like this one, most R users leave it out.
Default values
You can give an argument a default, which is used when the caller does not supply that argument:
add_numbers <- function(x, y = 10) {
x + y
}
add_numbers(5) # y is not given, so the default 10 is used[1] 15
add_numbers(5, 1) # y is given, so the default is ignored[1] 6
You have used defaults without noticing. mean() has an argument na.rm that defaults to FALSE, which is why mean(c(1, NA)) returns NA until you write na.rm = TRUE.
What happens inside a function stays inside
Variables created inside a function exist only while the function runs. They do not appear in your workspace afterwards:
add_numbers <- function(x, y) {
result <- x + y
result
}
add_numbers(5, 2)[1] 7
exists("result")[1] FALSE
exists("result") is FALSE because result lived inside the function and disappeared when the function finished. This is a good thing. A function cannot accidentally overwrite an object called result that you created somewhere else in your script.
When should you write a function?
A common rule of thumb, from R for Data Science, is to write a function once you have copied and pasted the same block of code more than twice. Look at this:
mean(mtcars$mpg, na.rm = TRUE)[1] 20.09062
sd(mtcars$mpg, na.rm = TRUE)[1] 6.026948
mean(mtcars$hp, na.rm = TRUE)[1] 146.6875
sd(mtcars$hp, na.rm = TRUE)[1] 68.56287
mean(mtcars$wt, na.rm = TRUE)[1] 3.21725
sd(mtcars$mpg, na.rm = TRUE) # oops: copied from above and not changed to wt[1] 6.026948
The last line is a real copy-paste bug. It runs without any error and gives you the standard deviation of mpg while you think you are looking at wt. Bugs like this are hard to spot because nothing looks broken.
Turning repeated code into a function helps in three practical ways:
- One place to fix things. If the calculation needs to change, you change it once.
- Fewer copy-paste slips. You only type the column name, so there is less to forget.
-
Clearer scripts. A good function name, such as
calc_stats(), tells the reader what the code does without making them read every line.
Example 1: mean and standard deviation of any column
Here is the repeated code above, written once as a function:
$mean
[1] 20.09062
$sd
[1] 6.026948
Two details are worth slowing down for:
-
data[[column]]pulls out one column of a data frame when the column name is stored as text.mtcars[["mpg"]]is the same asmtcars$mpg, but the[[ ]]form works when the name is inside a variable, which is exactly the situation inside a function. -
list(mean = mean_val, sd = sd_val)returns two values at once by packing them into a named list. You can pull one out with$:
mpg_stats <- calc_stats(mtcars, "mpg")
mpg_stats$mean[1] 20.09062
A small improvement: return a data frame
A list is fine for one column, but when you summarize several columns you usually want a table. If the function returns a one-row data frame, the results stack neatly:
calc_stats <- function(data, column) {
values <- data[[column]]
data.frame(
variable = column,
mean = mean(values, na.rm = TRUE),
sd = sd(values, na.rm = TRUE)
)
}
calc_stats(mtcars, "mpg") variable mean sd
1 mpg 20.09062 6.026948
Now run it on several columns at once:
variable mean sd
1 mpg 20.09062 6.0269481
2 hp 146.68750 68.5628685
3 wt 3.21725 0.9784574
lapply() calls calc_stats() once for each column name and collects the three small data frames in a list. do.call(rbind, results) then stacks them into one table. The copy-paste bug from earlier cannot happen here, because each column name is typed exactly once.
Example 2: grouped summaries with dplyr
A very common task in survey work is “give me the mean and standard deviation of this variable for each group”, for example yield by region or income by household type. The dplyr package is built for this.
To have something to work with, let’s create a small made-up dataset. rnorm() draws random numbers from a normal distribution, and set.seed() makes sure you get the same “random” numbers every time you run the code, so your output matches mine:
group value
1 A -0.56047565
2 A -0.23017749
3 A 1.55870831
4 A 0.07050839
5 A 0.12928774
6 B 1.71506499
7 B 0.46091621
8 B -1.26506123
9 B -0.68685285
10 B -0.44566197
Now the function:
# A tibble: 2 × 3
group mean_value sd_value
<chr> <dbl> <dbl>
1 A 0.194 0.811
2 B -0.0443 1.16
What is going on:
-
|>is the pipe. It passes the result of one step into the next, sodata |> group_by(...)means “takedata, then group it”. You may also see%>%in older code; for this purpose they do the same thing. -
group_by()splits the data into groups, andsummarize()computes one row of results per group. -
.data[[group_col]]is how you tell dplyr “use the column whose name is stored ingroup_col”. Inside dplyr you normally write bare column names likegroup, but here the name arrives as text, so.data[[ ]]does the lookup. It is the dplyr version ofdata[[column]]from Example 1.
Because the column names are arguments, the same function works on any dataset. Fuel efficiency by number of cylinders in mtcars:
analyze_data(mtcars, "cyl", "mpg")# A tibble: 3 × 3
cyl mean_value sd_value
<dbl> <dbl> <dbl>
1 4 26.7 4.51
2 6 19.7 1.45
3 8 15.1 2.56
Passing bare column names with { }
If you would rather call the function with bare names, like you do with dplyr itself, wrap the arguments in double curly braces (dplyr calls this “embracing”):
# A tibble: 3 × 3
cyl mean_value sd_value
<dbl> <dbl> <dbl>
1 4 26.7 4.51
2 6 19.7 1.45
3 8 15.1 2.56
Use .data[[ ]] when the column names come as text, for example from a vector of names you loop over. Use { } when you want the function to feel like a normal dplyr verb.
Common mistakes
- “could not find function”. You called the function before running the chunk that defines it. Run the definition first; in a new R session you have to run it again.
- Using objects from outside the function. If the body refers to a data frame that is not one of the arguments, the function only works in the script where that object happens to exist. Pass everything the function needs as an argument.
-
Reusing a common name. Calling your own function
meanorc, or a data framedata, hides R’s own version and leads to confusing errors. That is why the example dataset above is calledscores. -
Expecting more than the last value back. Only the last value (or what you pass to
return()) comes out. If you need several results, return a list or a data frame, as in Example 1.
Recap
A function is a named piece of code with inputs (arguments) and an output (the return value). You create one with function(), give arguments defaults with =, and get results back from the last line or from return(). Objects created inside a function stay inside it. Once you notice yourself pasting the same block a third time, that block is a good candidate for a function, and the examples above show how far one short function can take you: from one column to many, and from one dataset to any dataset.
The functions in this post trust their inputs. Give calc_stats() a column that does not exist and it fails with a confusing message. The next post, Error Handling in R, shows how to add checks so your functions fail early with a message that says what went wrong.