This lab focuses on creating functions: the why, the how, and some best practices. Much of this comes from the R for Data Science 2nd edition online text https://r4ds.hadley.nz/functions.html, conversations with CS faculty, and my experience (i.e. learn from my mistakes).

Directions (Please read before starting)

  1. Please work together with your assigned partner. Make sure you both fully understand each concept before you move on.
  2. Please record your answers and any related code for all embedded lab questions. I encourage you to try out the embedded examples, but you shouldn’t turn them in.
  3. Please ask for help, clarification, or even just a check-in if anything seems unclear.

\(~\)

Preamble

Packages and Datasets

library(dplyr)
library(ggplot2)

\(~\)

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. Don’t over-generalize

How to create functions:

  • It is generally easiest to start with a working example and then generalize (e.g. the magic numbers from Homework 1)
  • A function consists of a name, the arguments, and the body of the function.
    • Naming conventions:
      • Names should be consistent using either camelCase or snake_case
      • Similar functions should have similar prefixes to make Rs auto completion easier for you
      • DO NOT Overwrite existing functions: e.g. don’t name a new function “sum”
    • Arguments:
      • The inputs to the function
      • you can include default values with the normal way we assign values (e.g. var1=1)
    • Body:
      • What the function does
    • General Format:
      • functionName=function(arguments){Body return(output)}
  • In R we tend to have 3 types of functions:
    • Vector functions:
      • Input: Vector(s)
      • Output: Vector(s)
    • Data Frame Functions:
      • Input: Data Frame
      • Output: Data Frame
    • Plot Functions:
      • Input: Dataframe
      • Output: plot
  • It is good practice to test your code
    • In this class: informal tests will normally be sufficient
    • In general: test suites, including edge cases, are very helpful
  • Comments are important
    • I disagree with the text, explicitly commenting what a line should do can make bug finding easier
    • I like to include a comment with the expected inputs and outputs

\(~\)

Example 1: Taken from Homework 1

This was the original code given where I asked you to remove the “magic numbers”

x <- c(1,2,3, "four", 5, "six", 7, 6, 2)


### START ###

x <- as.numeric(x)

# Get rid of NA values
x <- x[c(1,2,3,5,7,8,9)]

# Create a vector to add to x
y <- rep(1, length = 7)

# Create new data.frame with x, y, and x+y
df <- data.frame(x = x, y = y, z = x + y)

## Only keep values where z > 5 and x <= 7 and grab column "z"
z_new <- df[c(4,5,6), 3]


### END ###

z_new
## [1] 6 8 7

Most of you successfully turned this into code that looked like the following:

x <- c(1,2,3, "four", 5, "six", 7, 6, 2)


### START ###

x <- as.numeric(x)

# Get rid of NA values
x <- na.omit(x)

# Create a vector to add to x
y <- rep(1, length = length(x))

# Create new data.frame with x, y, and x+y
df <- data.frame(x = x, y = y, z = x + y)

## Only keep values where z > 5 and x <= 7 and grab column "z"
z_new <- subset(df, df$z>5 & df$x<=7)$z


### END ###

z_new
## [1] 6 8 7

Now let’s work on turning this into a function.

  1. What does the function do?
  • Given an input vector
  • Subsets it to numbers
  • Creates a new vector of the same length with values of “1”
  • Creates a dataframe consisting of these two vectors and a new vector that is the sum of them
  • Keeps the values of the new vector where the values are greater than 5 and the input is <=7
  • Prints the vector
x <- c(1,2,3, "four", 5, "six", 7, 6, 2)
exampleFunction1=function(x){
  x <- as.numeric(x)
  x <- na.omit(x)# Get rid of NA values
  y <- rep(1, length = length(x))# Create a vector to add to x
  df <- data.frame(x = x, y = y, z = x + y)# Create new data.frame with x, y, and x+y
  z_new <- subset(df, df$z>5 & df$x<=7)$z## Only keep values where z > 5 and x <= 7 and grab column "z"
  z_new ## Print column "z"
}

exampleFunction1(x)
## [1] 6 8 7
  1. Can we make this function more efficient?
  • Are there any pipes we can use to make the code more legible?
  • Do we really need the dataframe?
  • Does creating and saving y do anything for us?
x <- c(1,2,3, "four", 5, "six", 7, 6, 2)
exampleFunction2=function(x){
  as.numeric(x)%>% 
  na.omit()%>% # Get rid of NA values
  {.+1}%>% # Add 1
  "["(.>5)%>% # Filter to greater than 5
  "["(.<=8) #Filter to less than or equal to 8 (so x is <=7 since z=x+1)
}

exampleFunction2(x)
## [1] 6 8 7
  1. Good Practices:
  • It is generally better to return a vector rather than print it so you can use it later.
  • We should test vectors for expected outputs
  • Include a comment with the expected inputs and outputs
x <- c(1,2,3, "four", 5, "six", 7, 6, 2)
exampleFunction3=function(x){
  # exampleFunction:
    # Expected input: Vector
    # Expected output: Vector consisting of the input vector + 1 subject to constraints
      # input vector <= 7
      # output vector >5
  outputVector<-as.numeric(x)%>% 
    na.omit()%>% # Get rid of NA values
    {.+1}%>% # Add 1
    "["(.>5)%>% # Filter to greater than 5
    "["(.<=8) #Filter to less than or equal to 8 (so x is <=7 since z=x+1)
  return(outputVector) #return the output
}

output=exampleFunction3(x)
print(output)
## [1] 6 8 7
x1 <- c(1,2,3, "four", 5, "six", 7, 6, 2) #base vector. Expected output: 6 8 7
x2 <- c(3, 7, "four", 2) #HW 1 vector. Expected output: 8
x3 <- c("a","b","c") #No Numbers. Expected output: None
x4 <- c(1,2,3,8,9,10) #All integers, none that should be printed (both above and below). Expected output: None
x5 <- c(4,5,6,7) #All integers, all should be printed. Expected output: 6 7 8
x6 <- c(1,2,3,4,5,6,7,8,9,10) #All integers, some should be printed. Expected output: 6 7 8
x7 <- c(5.1,4.1,3.9,7.1,6.8,55/10,75/10) #Non integer numbers. Expected output: 6.1 5.1 7.8 6.5
testVectors<-list(x1,x2,x3,x4,x5,x6,x7)

for (testVect in testVectors){ #we will work with Loops more on Thursday
  print(exampleFunction3(testVect))
}
## [1] 6 8 7
## [1] 8
## numeric(0)
## numeric(0)
## [1] 6 7 8
## [1] 6 7 8
## [1] 6.1 5.1 7.8 6.5

\(~\)

Lab

At this point you should begin working with your partner. This lab will continue building on the fundamental aspects of R introduced previously. The lab’s examples will continue using the “Happy Planet” data, so please make sure you include code to load it.

Part 1: Vector Functions

Basic Vector Functions

We will start with vector functions: Functions that take a vector or set of vectors and return a vector.

Question #1 Write a function that takes a vector, and then by element: squares the element and then adds 5. An example input and expected output are below:

## [1] 1.0 2.0 3.0 4.0 5.5
## [1]  6.00  9.00 14.00 21.00 35.25

Mutate Functions

Mutate functions are functions that work well inside mutate and filter because they return an output of the same length as the input. we’ve used z scores in a number of recent labs (e.g. Lab 7). Since this is something we have used multiple times, and will probably use again, it makes sense to create a function to do this.

zScore <- function(x) {
  (x - mean(x, na.rm = TRUE)) / sd(x, na.rm = TRUE)
}

Summarize Functions

Summarize functions are functions that return a single value for use in summarize(). For example, we could create a squared error function:

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

Question #2 Taken from the reading. Write bothNA(), a summary function that takes two vectors of the same length and returns the number of positions that have an NA in both vectors.

## [1] "Vector1: " "1"         "NA"        "3"         "NA"        "5"        
## [7] "NA"        "7"         "6"
##  [1] "Vector2: " "1"         "2"         "3"         "NA"        "5"        
##  [7] "NA"        "7"         "6"         "2"         "3"         "7"        
## [13] "NA"        "2"
## [1] "Expected Output: " "4"                 "6"

Part 2: Dataframe Functions

Data frame functions work like dplyr verbs: they take a data frame as the first argument, some extra arguments that say what to do with it, and return a data frame or a vector.

Sometimes we need to use embracing. wrapping a variable in braces {} tells dplyr to use the value stored inside the argument not the argument as the variable name.

As an example, we seem to be doing a lot of dataframe manipulations that involve dropping na values and then combining 2 columns.

dropAndAdd<-function(df,col1,col2){
  df=df%>%
    filter(!is.na({{col1}}))%>%
    filter(!is.na({{col2}}))%>%
    mutate(sumCol1Col2={{col1}}+{{col2}})
  return(df)
}

# Testing using airquality base R dataset

airquality2=dropAndAdd(airquality,Ozone,Solar.R)

airquality3<-airquality%>%
  filter(!is.na(Ozone))%>%
  filter(!is.na(Solar.R))%>%
  mutate(sumCol1Col2=Ozone+Solar.R)

sum((airquality3==airquality2)==FALSE,na.rm=TRUE)
## [1] 0

Question #3 Using user defined functions Write a function that does the following

  • Given: airquality
    • id column: Day
    • variable columns: Ozone, Solar.R
  • Output: df with 2 columns
    • Day
    • zscore
  • Body:
    • remove rows with na values in the dataset
    • add the variable columns together
    • calculate the zscore for the new column
    • returns a df consisting of only the two columns above

Hint: you should be able to do this with 2 pipes (3 total lines)

Question #4 Write a function that when given a dataset, an id column, and 2 variable columns does the following:

  • Removes rows with na values in the dataset
  • adds the two variable columns together
  • calculates the z score of the newly created column
  • Returns a dataframe with only the id column and the 2 variable columns

Note: Do this without calling any user-built functions. Figuring out the proper way to feed dataframe functions within a new dataframe function is outside the purview of this lab.

\(~\)

Part 3: Plot Functions

Plot functions take a dataframe and return a plot. For example, we’ve been making a significant number of histograms where the color and fill are based on the same variable, the colors are the same (so the borders match the fill), a minimal theme, and stacked bars.

basicHistogramPlot<-function(df,xCol,fillName,colorsToUse,alpha=0.5,bins=10){ #note that alpha and bins have defaults
  outPlot=ggplot(data=df,aes(x={{xCol}},fill={{fillName}},color={{fillName}}))+
    geom_histogram(position = "stack", alpha = alpha, bins=bins) +
    scale_fill_manual(values = colorsToUse)+
    scale_color_manual(values = colorsToUse)+
    theme_minimal()
  return(outPlot)
}
diamonds_data=diamonds
diamonds2 = diamonds_data%>%
  filter(cut=='Fair')%>%
  filter(clarity %in% c('VVS2','VVS1','IF'))%>%
  filter(color %in% c('D','H','I','J'))

basicHistogramPlot(diamonds2,price,clarity,c("red","blue","green"),bins=2) #note that I overwrote the default bin, but not the default alpha

Question #5 Modify the above function to include a factor variable and title variables. Demonstrate that it works by recreating the goalplot from https://rebelskyw.cs.grinnell.edu/wp-content/uploads/2024/09/MatchingPlots.html excluding the axis break changes.