At the end of this section, you should be able to:
Identify the core components of a function definition and explain their role (the function() directive, arguments, argument defaults, function body, return value)
Describe the difference between argument matching by position and by name
Write if-else, if-else if-else statements to conditionally execute code
Write your own function to carry out a repeated task
Replicate your function multiple times using map()
12.2 Functions
12.2.1 Why functions?
Getting really good at writing useful and reusable functions is one of the best ways to increase your expertise in data science. It requires a lot of practice.
If you’ve copied and pasted code 3 or more times, it’s time to write a function. Try to avoid repeating yourself.
Reducing errors: Copy + paste + modify is prone to errors (e.g., forgetting to change a variable name)
Efficiency: If you need to update code, you only need to do it one place. This allows reuse of code within and across projects.
Readability: Encapsulating code within a function with a descriptive name makes code more readable.
df<-tibble( a =rnorm(5), b =rnorm(5), c =rnorm(5), d =rnorm(5),)df|>mutate( a =(a-min(a, na.rm =TRUE))/(max(a, na.rm =TRUE)-min(a, na.rm =TRUE)), b =(b-min(a, na.rm =TRUE))/(max(b, na.rm =TRUE)-min(b, na.rm =TRUE)), c =(c-min(c, na.rm =TRUE))/(max(c, na.rm =TRUE)-min(c, na.rm =TRUE)), d =(d-min(d, na.rm =TRUE))/(max(d, na.rm =TRUE)-min(d, na.rm =TRUE)),)
# A tibble: 5 × 4
a b c d
<dbl> <dbl> <dbl> <dbl>
1 0.707 -0.204 0 0.758
2 0.252 0.796 0.229 0.115
3 0.190 0.235 0.655 0.772
4 1 0.0457 0.157 1
5 0 -0.0266 1 0
You might be able to puzzle out that this rescales each column to have a range from 0 to 1. But did you spot the mistake? (Example from R4DS, and…) When Hadley wrote the code he made an error when copying-and-pasting and forgot to change an a to a b. Preventing exactly this type of mistake is one very good reason to learn how to write functions.
The key to the work above is that we want to repeat a set of code multiple times. The code we want to replicate can be written as:
where █ represents the part of the code that changes each time the function is run.
12.2.3 Parts of a function
To create a function you need three things:
A name. Here we’ll use rescale01() because this function rescales a vector to lie between 0 and 1.
The arguments. The arguments are things that vary across calls and our analysis above tells us that we have just one. We’ll call it x because this is the conventional name for a numeric vector.
The body. The body is the code that’s repeated across all the calls.
Then you create a function by following the template:
df|>mutate( a =rescale01(a), b =rescale01(b), c =rescale01(c), d =rescale01(d),)
# A tibble: 5 × 4
a b c d
<dbl> <dbl> <dbl> <dbl>
1 0.707 0 0 0.758
2 0.252 1 0.229 0.115
3 0.190 0.439 0.655 0.772
4 1 0.250 0.157 1
5 0 0.178 1 0
12.2.4 Ordering and arguments
When calling a function, if you don’t name the arguments, R assumes that you passed them in the order defined inside the function.
my_power<-function(x, y){return(x^y)}my_power(x =2, y =3)
[1] 8
my_power(y =3, x =2)
[1] 8
my_power(2, 3)
[1] 8
my_power(3, 2)
[1] 9
Argument matching
In general, it is safest to match arguments by name and position for your peace of mind. For functions that you are very familiar with (and know the argument order), it’s okay to just use positional matching.
Error in average1(some_data): argument "remove_nas" is missing, with no default
average1(some_data, remove_nas =TRUE)
[1] 13.4
average2(some_data)
Error in average2(some_data): argument "remove_nas" is missing, with no default
average2(some_data, remove_nas =TRUE)
[1] 13.4
average3(some_data)
[1] 13.4
withoutreturn(): the function returns the last value which gets computed and isn’t stored as an object (using <-).
withreturn(): the function will return an object that is explicitly included in the return() call. (Note: if you (accidentally?) have two return() calls, the function will return the object in the first return() call.)
12.3 Control flow
Often inside functions, you will want to execute code conditionally. In a programming language, control structures are parts of the language that allow you to control what code is executed.
12.3.1if and else and ifelse
By far the most common is the if-else if-else structure.
if(logical_condition){# some code}elseif(other_logical_condition){# some other code}else{# yet more code}
Note that inside the curly else brackets, {}, you can have additional lines of code computing objects or conditions, or you can return desired objects.
You can include as many } else if { conditions as your problem calls for.
If the work can be done on one line, often the easiest construction is ifelse().
ifelse()
data.frame(value =c(-2:2))|>mutate(abs_value =ifelse(value>=0, value, -value))# abs val
value abs_value
1 -2 2
2 -1 1
3 0 0
4 1 1
5 2 2
12.3.2 Functions in the tidyverse
Functions that return the same number of rows as the original data frame are good to use inside mutate() and filter(). For example, you might want to capitalize the first word of every string:
Functions that collapse into a single value will work well in the summarize() step of the pipeline. For example, you may want to calculate the coefficient of variation which is the standard deviation divided by the mean.
cv<-function(x, na.rm=FALSE){sd(x, na.rm =na.rm)/mean(x, na.rm =na.rm)}cv(runif(100, min =0, max =50))
Mostly, the tilde will be used for functions we already know but want to modify (if we don’t modify, and it has a simple name, we don’t use the tilde):
library(palmerpenguins)library(broom)penguins_split<-split(penguins, penguins$species)penguins_split|>purrr::map(~lm(body_mass_g~flipper_length_mm, data =.x))|>purrr::map(tidy)|>list_rbind()
Arguments allow us specify the inputs when we call a function
If inputs are not named when calling the function, R uses the ordering from the function definition
All arguments must be specified when calling a function
Default arguments can be specified when the function is defined
The input to a function can be a function!
12.4 Iterating functions
There will be times when you will need to iterate a function multiple times.
12.4.1purrr for functional programming
We will see the R package purrr in greater detail as we go, but for now, let’s get a hint for how it works.
We are going to focus on the map family of functions which will just get us started. Lots of other good purrr functions like pluck() and accumulate() and across() from dplyr.
Much of below is taken from a tutorial by Rebecca Barter.
The map functions are named by the output the produce. For example:
map(.x, .f) is the main mapping function and returns a list
map_df(.x, .f) returns a data frame
map_dbl(.x, .f) returns a numeric (double) vector
map_chr(.x, .f) returns a character vector
map_lgl(.x, .f) returns a logical vector
Note that the first argument is always the data object and the second object is always the function you want to iteratively apply to each element in the input object.
The input to a map function is always either a vector (like a column), a list (which can be non-rectangular), or a dataframe (like a rectangle).
A list is a way to hold things which might be very different in shape:
a_list<-list(a_number =5, a_vector =c("a", "b", "c"), a_dataframe =data.frame(a =1:3, b =c("q", "b", "z"), c =c("bananas", "are", "so very great")))print(a_list)
$a_number
[1] 5
$a_vector
[1] "a" "b" "c"
$a_dataframe
a b c
1 1 q bananas
2 2 b are
3 3 z so very great
What if we want a different type of output? If the function outputs a data frame, we can combine the values in the list using a row-bind, list_rbind().
Error in `list_rbind()`:
! Each element of `x` must be either a data frame or `NULL`.
ℹ Elements 1, 2, and 3 are not.
Darn! We get an error because the output of the add_ten() function is a scalar, not a data frame. In order to use list_rbind() we need to edit the add_ten() function.
c(2, 5, 10)|>purrr::map(add_ten_df)|>list_rbind()# output bound by rows
x
1 12
2 15
3 20
data.frame(a =2, b =5, c =10)|>purrr::map(add_ten_df)|>list_cbind()# output bound by columns
x...10 x...10 x...10
1 12 15 20
c(2, 5, 10)|>purrr::map(add_ten_df)|>list_cbind()# output bound by columns
x...1 x...2 x...3
1 12 15 20
12.5 Simulating models
12.5.1 Goals of Simulating Complicated Models
The goal of simulating a complicated model is not only to create a program which will provide the desired results. We also hope to be able to write code such that:
The problem is broken down into small pieces.
The problem has checks in it to see what works (run the lines inside the functions!).
uses simple code (as often as possible).
12.5.2 Simulate to…
… estimate probabilities (easier than calculus).
… understand complicated models.
Aside: ifelse()
data.frame(value =c(-2:2))|>mutate(abs_value =ifelse(value>=0, value, -value))# abs val
# A tibble: 20 × 5
carat cut color price price_cat
<dbl> <ord> <ord> <int> <chr>
1 1.23 Very Good F 10276 expensive
2 0.35 Premium H 706 inexpensive
3 0.7 Good E 2782 medium
4 0.4 Ideal D 1637 medium
5 0.53 Ideal G 1255 inexpensive
6 2.22 Ideal G 14637 expensive
# ℹ 14 more rows
set.seed(4747)runif(4, 0, 1)# random uniform numbers
[1] 0.195 0.339 0.515 0.452
12.5.3 Probability example 1: hats
10 people are at a party, and all of them are wearing hats. They each place their hat in a pile; when they leave, they choose a hat at random. What is the probability at least one person selected the correct hat?
set.seed(47)num_iter<-10000map(1:20, hat_match_prob, reps =num_iter)|>list_rbind()|>ggplot(aes(x =num_hats, y =match_prob))+geom_line()+labs(y ="probability of at least one match", x ="number of hats")
12.5.4 Probability example 2: Birthday Problem
What is the probability that in a room of 29 people, at least 2 of them have the same birthday?
set.seed(123)num_stud<-40num_class<-1000map(1:num_stud, birth_match_prob, reps =num_class)|>list_rbind()|>ggplot(aes(x =num_stud, y =match_prob))+geom_line()+labs(y ="probability of birthday match", x ="number of students in class")
12.5.5 Good simulation practice
avoid magic numbers
set a seed for reproducibility
use meaningful names
add comments
12.5.6 Simulate to…
… estimate probabilities (easier than calculus).
… understand complicated models.
12.5.7 Model example 1: Bias in a model
Population:
talent ~ Normal (100, 15)
grades ~ Normal (talent, 15)
SAT ~ Normal (talent, 15)
… but they only have access to grades and SAT which are noisy estimates of talent.
Plan for accepting students
Run a regression on a training dataset (talent is known for existing students)
Find a model which predicts talent based on grades and SAT
Choose students for whom predicted talent is above 115
Flaw in the plan …
there are two populations of students, the Reds and Blues.
Reds are the majority population (99%), and Blues are a small minority population (1%)
the Reds and the Blues are no different when it comes to talent: they both have the same talent distribution, as described above.
there is no bias baked into the grading or the exams: both the Reds and the Blues also have exactly the same grade and exam score distributions
What is really different?
But there is one difference: the Blues have more money than the Reds, so they each take the SAT twice, and report only the highest of the two scores to the college. This results in a small but noticeable bump in their average SAT scores, compared to the Reds.
Key aspect:
The value of SAT means something different for the Reds versus the Blues
# A tibble: 2 × 5
color tpr fpr fnr error
<chr> <dbl> <dbl> <dbl> <dbl>
1 blue 0.619 0.0627 0.381 0.112
2 red 0.507 0.0375 0.493 0.109
Two separate models:
# A tibble: 2 × 5
color tpr fpr fnr error
<chr> <dbl> <dbl> <dbl> <dbl>
1 blue 0.503 0.0379 0.497 0.109
2 red 0.509 0.0378 0.491 0.109
What did we learn?
with two populations that have different feature distributions, learning a single classifier (that is prohibited from discriminating based on population) will fit the bigger of the two populations
depending on the nature of the distribution difference, it can be either to the benefit or the detriment of the minority population
no explicit human bias, either on the part of the algorithm designer or the data gathering process
the problem is exacerbated if we artificially force the algorithm to be group blind
well-intentioned “fairness” regulations prohibiting decision makers form taking sensitive attributes into account can actually make things less fair and less accurate at the same time
Simulate?
different varying proportions
effect due to variability
effect due to SAT coefficient
different number of times the blues get to take the test
etc.
12.5.8 Model example 2: Asset allocation
repeatedly run a model on a simulated outcome based on varying inputs
inputs are uncertain and variable.
output is a large set of results, creating a distribution of outcomes rather than a single point estimate.
easy to change the conditions of the models by varying the distribution type or properties of the inputs.