Importance of R Functions: A Guide for Data Analysts

What a function is, how to write one, and when it pays off, with step-by-step examples for summary statistics and grouped summaries.
R programming
functions
dplyr
beginners
Author

Gutama Girja Urago

Published

May 15, 2025

Modified

November 6, 2026

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.

NoteWhat you will learn
  • 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:

mean(c(2, 4, 9))
[1] 5

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 called x and y”.
  • Everything between { and } is the body: the code that runs each time you use the function.
  • return(result) sends the value of result back 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:

calc_stats <- function(data, column) {
  mean_val <- mean(data[[column]], na.rm = TRUE)
  sd_val <- sd(data[[column]], na.rm = TRUE)
  list(mean = mean_val, sd = sd_val)
}

calc_stats(mtcars, "mpg")
$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 as mtcars$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:

columns <- c("mpg", "hp", "wt")
results <- lapply(columns, function(col) calc_stats(mtcars, col))
do.call(rbind, results)
  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:

library(dplyr)

set.seed(123)
scores <- data.frame(
  group = rep(c("A", "B"), each = 5),
  value = rnorm(10)
)

scores
   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:

analyze_data <- function(data, group_col, num_col) {
  data |>
    group_by(.data[[group_col]]) |>
    summarize(
      mean_value = mean(.data[[num_col]], na.rm = TRUE),
      sd_value = sd(.data[[num_col]], na.rm = TRUE)
    )
}

analyze_data(scores, "group", "value")
# 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, so data |> group_by(...) means “take data, 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, and summarize() computes one row of results per group.
  • .data[[group_col]] is how you tell dplyr “use the column whose name is stored in group_col”. Inside dplyr you normally write bare column names like group, but here the name arrives as text, so .data[[ ]] does the lookup. It is the dplyr version of data[[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”):

analyze_data2 <- function(data, group_col, num_col) {
  data |>
    group_by({{ group_col }}) |>
    summarize(
      mean_value = mean({{ num_col }}, na.rm = TRUE),
      sd_value = sd({{ num_col }}, na.rm = TRUE)
    )
}

analyze_data2(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

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 mean or c, or a data frame data, hides R’s own version and leads to confusing errors. That is why the example dataset above is called scores.
  • 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.

Back to top