How to Create a Function in R: Custom Median & ggplot2 Examples

Learn to write your own R functions with real examples — a from-scratch median() and a reusable ggplot2 function for group comparisons, using tidyvers

If you've written the same block of R code three times this week — a group summary, a plot, a cleanup step — you don't need another package. You need a function. A custom function is the solution. For example, two functions you'll actually reuse: a custom median() built from scratch, and a reusable ggplot2 function that plots group means for any variable in any data frame — the kind of thing that saves real time across dissertation chapters.

Streamlining R Code with Functions Median Function Example Demonstrates function creation Group Comparison Chart Function Provides a ready-to-use visualization tool Reusability Clarity Allows code to be used across projects Makes code easier to understand Code Efficiency Reduces repetitive coding tasks

Who this is for: You already run R functions like mean(), lm(), or filter(). You now want to write your own — either to stop repeating code across analysis scripts, or because a reviewer/supervisor is asking for reproducible, parameterized code.
Table of Contents

The Syntax: Function, Body, Return

Every R function has the same three parts: a name, a set of arguments, and a body.

my_function <- function(arg1, arg2) {
  result <- arg1 + arg2
  return(result)
}

my_function(3, 5)
#> [1] 8

You assign the function to a name with <-, list the inputs inside function(), and write the logic inside { }. Once this runs, my_function exists in your environment exactly like mean() or sd(), until you restart the session or remove it.

You don't actually need return() — R automatically returns the value of the last line evaluated in the body, with or without it:

add_two <- function(x) x + 2   # no return() needed
add_two(10)
#> [1] 12

return() is still worth keeping when you want to exit a function early, or when it makes the exit point explicit for anyone reading your code later — including you in six months. And since return() only sends back one object, when you need more than one result, package them into a list(): list(mean = mean(x), sd = sd(x)) returns both in a single call.

How to Create a Function in R: Custom Median & ggplot2 Examples

Build Your Own median() From Scratch

Base R's median() is a black box until you write the logic yourself once. Here's a function that reproduces it, using the mtcars dataset:

get_median <- function(x) {
  x <- sort(x)
  n <- length(x)

  if (n %% 2 == 0) {
    (x[n / 2] + x[n / 2 + 1]) / 2   # even count: average the two middle values
  } else {
    x[(n + 1) / 2]                  # odd count: take the middle value
  }
}

get_median(mtcars$mpg)
#> [1] 19.2

median(mtcars$mpg)
#> [1] 19.2

mtcars$mpg has 32 observations — an even count — so the function sorts all 32 values and averages the two middle ones (positions 16 and 17, both 19.2 in this dataset). That matches base R's median() exactly, confirming the logic is correct rather than coincidentally right for one input.

Why bother writing this if median() already exists? Not to replace it — to understand the odd/even branching logic once, so you're not guessing when a reviewer asks you to justify a "median split" or a robust-statistics choice in your methodology section. It's the same reason we walk through the math of ANOVA before running aov().

A Reusable ggplot2 Function for Group Comparisons

This is the function worth keeping in your own personal R toolkit. It takes any data frame, any grouping column, and any numeric column, and returns a bar chart of group means with standard-error bars — the exact plot most dissertation results chapters need repeatedly, for different variables, without rewriting the ggplot2 code each time.

library(dplyr)
library(ggplot2)

plot_group_means <- function(data, group_var, value_var, plot_title = NULL) {
  summary_df <- data %>%
    group_by({{ group_var }}) %>%
    summarise(
      mean_val = mean({{ value_var }}, na.rm = TRUE),
      se_val   = sd({{ value_var }}, na.rm = TRUE) / sqrt(n()),
      .groups  = "drop"
    )

  ggplot(summary_df, aes(x = {{ group_var }}, y = mean_val)) +
    geom_col(fill = "#4C72B0", width = 0.6) +
    geom_errorbar(
      aes(ymin = mean_val - se_val, ymax = mean_val + se_val),
      width = 0.15
    ) +
    labs(title = plot_title, y = "Mean", x = NULL) +
    theme_minimal(base_size = 13)
}

plot_group_means(mtcars, cyl, mpg, "Mean MPG by Cylinder Count")

Mean MPG by Cylinder Count by using the custom function from dplyr and ggplot2


The {{ }} (curly-curly) syntax is what makes this function reusable rather than one-off. It lets you pass a bare column name — cyl, not "cyl" — into dplyr verbs inside your own function, the same way you'd type it directly in a script. Without it, this function would only work if you hard-coded the column names, which defeats the point of writing it in the first place.

Here's what the function actually calculates before it plots anything, so the logic isn't hidden:

CylindersnMean MPGStandard Error
41126.71.36
6719.70.55
81415.10.68

Swap in your own data frame and column names — plot_group_means(my_data, treatment_group, outcome_score) — and the function does the grouping, summarizing, and plotting in one call. If you haven't installed ggplot2 yet, the setup steps are here: how to install ggplot2 in R. For styling the output further — centering titles, removing legends, adjusting themes — see this guide to customizing ggplot2, and the full ggplot2 cheat sheet for extending this into other chart types.

Making Functions Flexible

Four things separate a function that works once from one you'll actually reuse.

Default arguments

Set a default with = to make an argument optional:

greet <- function(name = "Guest") paste("Hello,", name)
greet()
#> [1] "Hello, Guest"

The ... argument

Add ... when you want your function to forward extra, unnamed arguments straight through to another function inside it — this is how you wrap a base R function without hard-coding every one of its arguments:

my_plot <- function(x, y, ...) plot(x, y, ...)
my_plot(mtcars$wt, mtcars$mpg, col = "steelblue", pch = 16)

Scope, and why to avoid <<-

A variable created inside a function only exists inside that function — this is local scope, and it's a feature: your functions can't silently overwrite variables in your main script by accident. <<- (superassignment) lets a function reach outside itself and modify a global variable. It works, but it makes a function's behavior depend on state outside its own arguments — exactly the kind of bug that's hard to trace in a 300-line analysis script. Default to passing values in as arguments and getting results back with return(); reserve <<- for genuinely rare cases like updating a counter across repeated calls.

A variable created inside a function only exists inside that function

Anonymous functions

For a short, one-time operation — especially inside sapply(), lapply(), or purrr::map() — you often don't need to name the function at all:

sapply(1:5, \(i) i * 2)   # R 4.1+ shorthand
#> [1]  2  4  6  8 10

Use a named function when the logic will be reused or is more than one or two lines; use an anonymous one when it's genuinely disposable.

Common Mistakes When Writing R Functions

  1. Forgetting {{ }} inside dplyr verbs. If your custom function uses group_by() or summarise() and you pass a column name as an argument, you need {{ }} around it — plain argument names will throw an "object not found" error.
  2. Writing scalar logic and expecting it to vectorize. An if statement checks one condition, not one per row. For row-by-row logic on a vector, use ifelse() or dplyr::case_when() instead of if/else inside a function meant to run on a whole column.
  3. Reaching for <<- to "fix" a scope error. If a function can't see a variable, the fix is almost always to pass it in as an argument — not to force the function to reach outside itself.
  4. No input validation. A function that assumes its input is always numeric, always complete, or always the right length will fail silently or produce a wrong number instead of an error. A single stopifnot() line at the top catches this early.
Common Mistakes When Writing R Functions

Frequently Asked Questions

Do I need to use return() in every R function?

No. R returns the value of the last line evaluated in the function body automatically. Use return() when you want to exit the function early, or when it makes the exit point clearer for someone reading the code later.

Can an R function return more than one value?

A function can only pass back one object through return(), but that object can be a list() holding as many named results as you need — a mean, a standard deviation, and a sample size in one call, for example.

What's the difference between a function and a script in R?

A script runs top to bottom once. A function is reusable code you define once and call repeatedly, with different inputs each time, without retyping the logic. If you find yourself copy-pasting the same 5 lines with one number changed, that's a function, not a script.

Why does my custom function fail inside mutate() or summarise() but work outside it?

This almost always means the function is receiving a column name as a plain argument instead of using tidy evaluation. Wrap the argument in {{ }} wherever it's used inside a dplyr verb, as shown in the plot_group_means() example above.

How do I let my function accept a column name without quotes?

Use {{ }} (curly-curly) from the tidyverse's tidy evaluation system. It lets you write my_function(data, cyl) instead of my_function(data, "cyl"), matching how you'd type the column name directly in a script.

Next Step

Writing a general-purpose function is straightforward once you've done it a few times. Writing one that survives contact with a real dissertation dataset — missing values, unbalanced groups, non-standard factor levels — is where most self-taught R code breaks. If you're building a function or a full analysis pipeline for a thesis chapter and want a second pair of eyes on it, message us directly on WhatsApp.

Related reading: descriptive statistics in R · how to use dplyr in R · dplyr cheat sheet

📊

Need this analysis done for your thesis or dissertation? I'll handle it in R, SPSS, or Minitab — with APA-formatted results delivered fast.

Post a Comment