Skip to Content
Course content

7: Writing Functions in R

Click on the "Edit" button in the top corner of the screen to edit your slide content.

When I first started writing R, I had a nagging feeling that functions were just "shortcuts" for lines of code I was too lazy to type twice. This led me to a mistake that almost every R learner makes: I thought that if I changed a variable inside a function, it would automatically update that variable in my main workspace. I treated functions like macros rather than isolated environments.

"Why didn't my variable change?"

Look at this snippet. Imagine you're trying to "clean" a number by rounding it using a function:


my_number <- 10.567
round_it <- function(x) {
  x <- round(x, 2)
}

round_it(my_number)
print(my_number) 
# You might expect 10.57, but it's still 10.567!

It feels broken, right? You passed the variable in, the function did the work, but the original my_number didn't budge. This happens because R uses lexical scoping. When you pass my_number into the function, R creates a local copy of that value for the variable x. The function plays around with x in its own little bubble, and then, the moment the function finishes, that bubble pops and everything inside it vanishes.

Thinking in Returns and Scopes

To actually get something out of a function, you have to explicitly tell R to "hand it back" to you using the return() statement, or by making the last line of the function the value you want. I generally prefer being explicit with return() when I'm mentoring juniors—it makes the intent crystal clear.

Let's build something more useful than a rounding tool. Say you're analyzing sales data and you frequently need to calculate the percentage growth between two periods, but your data is messy and full of NA values. Instead of writing the same ifelse logic ten times, we wrap it in a function:


calculate_growth <- function(current, previous) {
  # Guard clause: handle cases where previous is 0 to avoid Inf
  if (previous == 0) {
    return(NA)
  }
  
  growth <- (current - previous) / previous
  return(growth * 100)
}

# Now we assign the result back to a variable in the global environment
jan_sales <- 100
feb_sales <- 120

feb_growth <- calculate_growth(feb_sales, jan_sales)
print(feb_growth) # 20

Notice what happened here. I didn't try to change jan_sales; I captured the output of the function into a new variable, feb_growth. That is the fundamental mental shift you need: functions are like vending machines. You put inputs in, and it spits a result out. It doesn't change the money in your pocket; it just gives you a soda.

Adding Flexibility with Default Arguments

One of the most powerful parts of writing your own functions is the ability to set defaults. I use this constantly for settings that "usually" stay the same but occasionally need to be tweaked. If you don't want to force the user to provide every single argument every time, you can assign a value in the function definition.

Let's expand our growth function to allow for a "rounding" preference:


calculate_growth_fancy <- function(current, previous, digits = 2) {
  if (previous == 0) return(NA)
  
  growth <- ((current - previous) / previous) * 100
  return(round(growth, digits))
}

# Use the default (2 digits)
calculate_growth_fancy(120, 100) # 20

# Override the default for a more precise report
calculate_growth_fancy(120.456, 100.123, digits = 4) # 20.3134

By setting digits = 2, the function becomes easier to use for 90% of your tasks, but remains powerful enough for the 10% where you need precision. It's a small touch, but it's what separates "scripts that work" from "professional software."




📋 Practical Task

Build a Custom Weighted-Average Price Calculator

In financial analysis, you often can't just take a simple average of prices because you bought different amounts of an asset at different prices. You need a Weighted Average Price.

Your Task: Write a function called calc_weighted_avg that takes two vectors: prices and volumes.

  • The function should multiply each price by its corresponding volume, sum those products, and then divide by the total volume.
  • Constraint: Add a check to ensure that the prices and volumes vectors are the same length. If they aren't, the function should return a helpful error message using stop("Your error message here").
  • Constraint: Ensure the final result is rounded to 2 decimal places.

Test your function with this data:


buy_prices <- c(150.20, 155.10, 148.00)
buy_volumes <- c(10, 20, 15)
# Expected Result: 151.83
Rating
0 0

There are no comments for now.

to be the first to leave a comment.