Preamble

So far, we have primarily used functions that are already built into R or provided by packages. In this lab, we will learn how to create functions of our own by combining operations and functions that we already know. Writing our own functions allows us to turn code that accomplishes a particular task into a reusable tool that can be applied to different scenarios.

Much of this comes from the R for Data Science online texts online text https://r4ds.had.co.nz/iteration.html, https://r4ds.hadley.nz/iteration.html, conversations with CS faculty (Prof. Will Rebelsky), and experience (i.e. learn from my mistakes).

\(~\)

Goals

  • Functions
    • What are functions
    • Why/when we should write new functions
    • How we can write new functions
    • Best practices
    • Importing User-Defined Functions

\(~\)

Review: Useful built in functions:

%in%: is useful for determining wehter values of x are also in y. For example c(2,3,4) %in% c(2,5,6) would return TRUE FALSE FALSE. Sometimes it is useful to use the initial vectors and only return those values:

x=c(2,3,4)
y=c(2,5,6)
x%in%y #booleans
## [1]  TRUE FALSE FALSE
x[x%in%y] #boolean indexing
## [1] 2

which(boolean): returns the indices where the boolean (T/F) is true

which(x%in%y)
## [1] 1

Functions

Why create and use functions:

  • Readability: a well named function tells the reader what is going on. It also makes reading long sections of code easier if much of it is condensed to a single line/name.
  • Ease of updates and minimization of mistakes: you only need to change one place not many if your use changes
  • Ease of use: it is much easier to reuse a function from project to project.

When to use functions:

  • If you find yourself copy-pasting the same code more than twice
  • When you believe you will be reusing the code in the future

When not to use functions:

  • If this is a one time use, or is being created for a specific scenario we are unlikely to revisit. Don’t over-generalize

How to create functions:

When creating a function, it is generally easiest to start with code that works for a specific example and then generalize it. Before writing the function, we should think about two things: what information does the function need as input, and what do we want it to return as an output?

A function consists of a few basic pieces:

  • Name: The name we give the function so that we can use it later.
  • Arguments: The inputs supplied to the function.
  • Body: The code that the function runs using those inputs.
  • Output: The result that we want the function to return.

The general format we will use is:

function_name = function(arguments){
  # Body of the function
  return(output)
}

The arguments act as placeholders for the values that will be supplied when we use the function. The body contains the operations we want to perform, and return() specifies the output that we want to get back from the function.

Example: Writing a function

Suppose that we frequently work with temperatures recorded in Fahrenheit, but want to report the average temperature in Celsius. Before worrying about creating a function, let’s first write code that accomplishes this for one particular set of temperatures.

temperatures = c(45, 63, 71, 82, 95)

temperatures_c = (temperatures - 32) * 5/9
mean(temperatures_c)
## [1] 21.77778

This code works, but it is specific to the vector temperatures. If we wanted to perform the same calculation on several different sets of temperatures, we would have to repeatedly change temperatures to the new vector and rerun the code.

Instead, we can generalize this code into a function. To do this, we first need to identify our input and output.

Our input is the vector of Fahrenheit temperatures. This is the information that will change each time we use the function, so it should become an argument. Our output is the average temperature in Celsius.

We can now place our working code inside a function:

mean_celsius = function(temperatures){
  temperatures_c = (temperatures - 32) * 5/9
  mean_temperature = mean(temperatures_c)
  return(mean_temperature)
}

The object temperatures inside the function is now an argument rather than one specific vector. When we call the function, R substitutes the value we provide for that argument and runs the code in the body.

old_temp_values = c(45, 63, 71, 82)
mean_celsius(old_temp_values)
## [1] 18.47222

We can now reuse the same function with completely different inputs without rewriting the calculation, either defined as new vectors or just written using c() notation:

mean_celsius(c(90, 95, 100)) #input a new vector argument
## [1] 35
mean_celsius(32) #also works on a numeric input (length-1 vector)
## [1] 0

This is the basic process we will generally use when creating functions:

  1. Write code that works for a specific example.
  2. Decide which values need to change when the code is reused. These become the arguments.
  3. Decide what result the function should produce. This becomes the output.
  4. Generalize the working code by placing it inside function().
  5. Test the function on different inputs to make sure it works

Good Practices

Once we have a working function, there are a few practices that make them easier to understand, reuse, and debug.

Use informative names. Function and object names should describe what they represent or do. Function names should also follow a consistent convention, such as function_name, FunctionName, functionName or function.name. Be careful not to overwrite existing R functions; for example, do not create a new function named mean or sum.

Example: This code is not very informative as to what the function is trying to accomplish when compared to how we originally wrote it.

f = function(x){
  y = (x - 32) * 5/9 
  z = mean(y)
  return(z) }

Avoid unnecessary output. Intermediate objects can make complicated code easier to understand, but we generally do not need to print every intermediate result. The function should return the result that we actually want to use. Here is a bad example that gives us too much output that might reflect what you would do when test-writing a function.

mean_celsius_bad = function(temperatures){
  print(temperatures)
  temperatures_c = (temperatures - 32) * 5/9
  print(temperatures_c)
  mean_temperature = mean(temperatures_c)
  return(mean_temperature)
}
mean_celsius_bad(c(40, 53, 65, 87))
## [1] 40 53 65 87
## [1]  4.444444 11.666667 18.333333 30.555556
## [1] 16.25

Use comments when they make the code easier to understand. In particular, it can be useful to document what inputs a function expects, what it returns, and what less-obvious lines of code are intended to accomplish.

Test your function. A function that works on the example used to create it may not necessarily work on other reasonable inputs. In this class, informal testing will normally be sufficient. Try the function on several different inputs and check whether the results are what you expect.

For example, we might test mean_celsius() on several different vectors:

mean_celsius(c(32, 32, 32))       # Expected: 0
## [1] 0
mean_celsius(c(212))              # Expected: 100
## [1] 100
mean_celsius(c(32, 50, 68))       # Expected: 10
## [1] 10

In larger programming projects, functions are often checked using formal test suites that include many different inputs and potential edge cases. We will generally not need that level of testing in this course.

Lab Start

Part 1: Functions

Now that we have seen how a function is constructed, we will practice writing functions of our own. Most functions that we write will combine operations and built-in functions that we already know. To start, think about what the inputs and outputs should be.

Vector Functions

We will use the term vector function for a function that accepts a vector (or set of vectors) as an input. Depending on what the function is intended to accomplish, its output might be another vector or a single value.

Two useful types of vector functions are mutate functions and summarize functions:

  • Mutate functions take a vector and return a vector of the same length. Each input value has a corresponding output. (This naming convention matches mutate in dplyr.)
  • Summarize functions take a vector and reduce the information to a single summary value. (This naming convention matches summarize in dplyr.)

For now, these names are useful for thinking about what kind of object a function takes as its input and what kind of object it returns.

Basic Vector Functions

We will start with a simple vector function to practice the syntax.

Question #1:

  • A: Write a function named square_plus_five() that takes a numeric vector as its argument. For each element, square the value and then add 5. Your function should return the resulting vector.

An example input and expected output are shown below:

## [1] 1.0 2.0 3.0 4.0 5.5
## [1]  6.00  9.00 14.00 21.00 35.25
  • B: Is square_plus_five() a mutate function or a summarize function? How can you tell from its output?

Mutate Functions

Suppose we have the following vector of exam scores:

exam_scores = c(72, 81, 65, 90, 84, 77)

We can calculate the z-score of every observation using functions that we have already seen:

(exam_scores - mean(exam_scores)) / sd(exam_scores)
## [1] -0.6939506  0.3188422 -1.4816784  1.3316350  0.6564398 -0.1312880

Notice that this code contains several operations, including calls to the functions mean() and sd(). Functions that we write ourselves will often make use of other functions inside their body.

We expect to calculate z-scores several times throughout this course. Rather than rewriting this calculation each time, it makes sense to turn it into a function.

Question #2:

  • A: Convert the code above into a function named zScore(). Your function should accept a numeric vector as its argument and return the z-score corresponding to each observation.

  • B: Test your function using exam_scores. Then test it on a second numeric vector of your choice.

Summarize Functions

Sometimes we do not want a value corresponding to every observation. Instead, we want to use the observations in a vector to calculate a single value.

For example, consider the following function:

squaredError = function(x) {
  return(sum((x - mean(x))^2, na.rm = TRUE))
}

This function combines several familiar operations. When functions are used inside other functions, it can be helpful to work from the inside out to understand what the code is doing.

In squaredError():

  1. mean(x) calculates the mean of the vector.
  2. x - mean(x) calculates each observation’s deviation from the mean.
  3. (x - mean(x))^2 squares each of those deviations.
  4. sum() adds the squared deviations together.

Although several values are calculated along the way, squaredError() ultimately returns one value. We would therefore describe this as a summarize function.

Functions can also contain logical operations, indexing, and calls to several other functions. For example, recall some of the tools reviewed at the beginning of this lab:

is.na(x)
which(is.na(x))
x %in% y
x[x %in% y]

These operations can be combined inside a function just like the arithmetic operations used in our previous examples.

Question #3:

  • A: Write a function named bothNA() that takes two vectors of the same length and returns the positions that contain an NA in both vectors.

You may find some of is.na(), which(), length(), and logical indexing useful. Remember that the output of one function can be used as the input to another function.

  • B: Test your function using the following vectors:
x = as.numeric(c(1, "a", 3, "four", 5, "six", 7, 6))
x2 = as.numeric(c(3, 7, "four", 2, 5, "six", 1, 2))

bothNA(x, x2)
  • C: Write a function named count_bothNA() that counts the number of positions that contain NA in both variables using the bothNA() function from Part B. Verify it works on vectors x and x2.

More Function Practice

We have now seen several examples of something called nesting function calls, where the output of one function is used directly as the input to another.

For example, recall that table() returns the frequency of each unique value in a vector:

animals = c("cat", "dog", "cat", "bird", "dog", "cat")
table(animals)
## animals
## bird  cat  dog 
##    1    3    2

Question #4

  • A: Create a function named top_cat() that takes a categorical vector as its argument and returns the frequency of the most common category. You can accomplish this using the table() and max() functions.

  • B: Test your function using animals. The correct output should be 3.

  • C: Load the colleges data set below and use top_cat() to find the number of times the most common Region shows up. (Answer should be 400)

colleges = read.csv("https://remiller1450.github.io/data/Colleges2019.csv")

Day 2

Before moving on to a few additional topics, we will start with some brief practice writing functions.

Function Review

Recall the general format we have been using:

function_name = function(arguments){
  # Body
  return(output)
}

Question #5A: Write a function named add_ten() that takes a numeric vector x and adds 10 to every value. Test your function on the vector below.

review_x = c(4, 8, 15, 16, 23, 42)

Question #5B: Write a function named range_size() that takes a numeric vector x and returns the difference between its maximum and minimum values. Test your function using review_x.


Functions with Multiple Arguments

So far, many of the functions we have written have taken a single vector as their input. However, functions can accept as many arguments as necessary to perform their task.

For example, suppose we want to count how many values in a vector are greater than some cutoff. For a particular example, we could write:

x = c(4, 8, 15, 16, 23, 42)
cutoff = 15

sum(x > cutoff)
## [1] 3

If we want to turn this into a reusable function, both x and cutoff need to be arguments because we may want to change either one:

count_above = function(x, cutoff){
  return(sum(x > cutoff))
}

We can now use different combinations of inputs:

count_above(x = review_x, cutoff = 15)
## [1] 3
count_above(x = review_x, cutoff = 20)
## [1] 2
count_above(x = c(1, 5, 10, 20), cutoff = 7)
## [1] 2

The arguments serve somewhat different purposes. x provides the data that the function operates on, while cutoff controls how the function operates on those data.

Just like the built-in functions we have used, arguments can be supplied by name or by their position:

count_above(x = review_x, cutoff = 15)
## [1] 3
count_above(review_x, 15)
## [1] 3
count_above(15, review_x) # this treats 15 as the vector and review_x as our cutoff - BAD!
## [1] 2

Question #6: Write a function named between() that takes three arguments: a numeric vector x, a lower value low, and an upper value high. The function should return the values of x that are greater than low and less than high.

Test your function using review_x, with low = 10 and high = 30.


Default Argument Values

Sometimes an argument has a value that we expect to use most of the time. Rather than requiring the user to provide that value every time the function is called, we can give the argument a default value.

Consider the count_above() function:

count_above = function(x, cutoff){
  return(sum(x > cutoff))
}

Suppose that we usually want to count values greater than zero. We can specify 0 as the default value of cutoff when we define the function:

count_above = function(x, cutoff = 0){
  return(sum(x > cutoff))
}

Now consider:

example_values = c(-5, -2, 3, 8, 12)

count_above(example_values)
## [1] 3
count_above(example_values, cutoff = 5)
## [1] 2

If we do not provide cutoff, R uses the default value of 0. If we provide our own value, the value we supply replaces the default.

We have already used many built-in functions with default arguments. For example:

mean(example_values)
## [1] 3.2

works without specifying every argument of mean(). We only need to specify an optional argument when we want behavior that differs from its default.

Question #7: The function below calculates the mean of a vector:

my_mean = function(x){
  return(mean(x))
}

Modify my_mean() so that it has a second argument named remove_na. Give remove_na a default value of TRUE, and pass this argument to the na.rm argument of mean().

Test your function on the following vector both with and without using the default value.

missing_values = c(3, 7, NA, 10, 12)

Question #8: Modify the zScore() function you created in Question #2 so that it includes arguments for the mean and standard deviation used in the z-score calculation. By default, the function should use the sample mean and sample standard deviation of x, but the user should be able to specify different values for either argument. Test your function both using the defaults and using a specified mean and standard deviation.


Local and Global Objects

So far, we have mostly focused on the inputs and outputs of functions. However, functions can also create objects internally while performing their calculations.

Consider the following function:

square_difference = function(x, y){
  difference = x - y
  squared = difference^2
  
  return(squared)
}

When we use the function:

square_difference(10, 4)
## [1] 36

R temporarily creates the objects x, y, difference, and squared while the function is running. These objects exist in the function’s local environment.

Try running:

difference
squared

Assuming we have not previously created objects with these names, R will tell us that they cannot be found. Objects created inside a function do not automatically become objects in our normal, or global, environment.

This is useful because we can create intermediate objects that make our functions easier to write and understand without filling our global environment with objects that we only needed temporarily.

Local Objects and Existing Objects

Objects inside and outside a function can even have the same name without necessarily referring to the same object.

For example:

number = 10

add_five = function(x){
  number = x + 5
  return(number)
}

add_five(20)
## [1] 25
number
## [1] 10

The function returns 25, but the original value of number is still 10. The assignment:

number = x + 5

creates a local object named number while the function is running. It does not overwrite the number that already exists in our global environment.

Returning and Saving Objects

If we want to keep the result of a function, we can return it and then assign that output to an object:

my_result = square_difference(10, 4)

my_result
## [1] 36

Here, difference and squared were created locally inside the function, but the value returned by the function was assigned to my_result in the global environment.

Question #9:

  • A: Before running the following code, predict the values that will be printed for a, b, and result. Then run the code and check your predictions.
a = 5
b = 10

math_function = function(a){
  b = a^2
  result = b + 3
  return(result)
}

result = math_function(4)

a
b
result
  • B: Briefly explain why the values of a and b in the global environment were not changed by the objects created inside math_function().

Importing User-Defined Functions

One of the advantages of user-defined functions is that we can reuse them in future projects. However, copying and pasting our function definitions into every new R Markdown file defeats some of the purpose of creating reusable functions.

A useful approach is to save functions that we expect to reuse in an R script and then import them using source().

The functions we will use for this example are available here. Download this file and save it in the same location as your R Markdown file. (If you haven’t already done so, now is a good time to make a STA230 folder on your computer)

If the file is in your current working directory, we can run all of the R code contained in the file using:

source("Week2functions.R")

If the file is stored somewhere else, you will need to provide the appropriate file path.

Question #10: Import Week2functions.R using source(). Demonstrate that you were successful by running the following:

exampleFunction1(Week2functionsTest)
exampleFunction2(Week2functionsTest)

Question #11: Investigate the Week2Functions file. In your own words, describe what exampleFunction1() and exampleFunction2() do.

\(~\)

Functions are particularly useful when we want to perform the same operation repeatedly on different inputs. In the next lab, we will introduce iteration (also called loops), which provides ways to systematically repeat operations without manually running the same code over and over.