Prerequisites:
tidyverse
Collection of statements orchestrated together to perform a specific operation.
A function takes input (arguments), applies a method (code) to those arguments and returns the output.
Hopefully everyone in this session has experience of using functions when they code in R.
E.g. sum()
or mean()
.
Note: Functions are objects, just as vectors are objects.
We want the average Petal Length/Petal Width ratio for the Sepal Lengths of less than 5, for each species separately (setosa, versicolor, virginica).
setosa <- iris[iris$Species == "setosa", ]
setosa <- setosa[setosa$Sepal.Length < 7, ]
setosa$petal_length_width_ratio <- setosa$Petal.Length / setosa$Petal.Width
mean(setosa$petal_length_width_ratio)
versicolor <- iris[iris$Species == "versicolor", ]
versicolor <- versicolor[versicolor$Sepal.Length < 7, ]
versicolor$petal_length_width_ratio <-
versicolor$Petal.Length / versicolor$Petal.Width
mean(versicolor$petal_length_width_ratio)
virginica <- iris[iris$Species == "virginica", ]
virginica <- virginica[virginica$Sepal.Length < 7, ]
virginica$petal_length_width_ratio <-
virginica$Petal.Length / virginica$Petal.Width
mean(virginica$petal_length_width_ratio)
iris_function <- function(data, species) {
data <- data[(data$Species == species) & (data$Sepal.Length < 7), ]
data$petal_length_width_ratio <- data$Petal.Length / data$Petal.Width
mean(data$petal_length_width_ratio)
}
iris_function(iris, "setosa")
iris_function(iris, "versicolor")
iris_function(iris, "virginica")
Arguments
Controls how you call the function.
Can check it with: formals(function_name)
Body
The code inside a function.
Can check it with: body(function_name)
Environment (Advanced)
The data structure that determines how the function finds the values associated with the names.
Can check it with: environment(function_name)
A pipeline consists of multiple steps (involving functions) to achieve a desired output, just like how a cake recipe has steps/instructions to make a cake.
Let’s simplify that and say that each step of a pipeline can a function a.k.a. a step of the recipe.
##### Standard syntax
function_name <- function(argument) {
code
}
# If you don't want to assign the output of the function to a variable:
function_name(argument = "value")
function_name("value")
# If you want to assign the output of the function to a variable:
function_output <- function_name(argument = "value")
function_output <- function_name("value")
return()
In the syntax above, I have just used code
as a stand in for the body of the function.
When running a function, the code chunk will run, but only the last line will be output into the environment/saved when assigned to a variable. Some like to explicitly use return()
to show what is being returned at the end of the function, but it is not necessary.
Also, print()
statements will be output into the console.
It is worth being aware that any variables that are assigned inside a function, don’t automatically exist in the outside environment (can’t be retrieved after the function has run).
Would recommend playing around with assignment with your functions.
There is a way to save more to the environment but that hasn’t been covered here.
alphabet <- function() {
var_1 <- "a" # var_1 doesn't exist
var_2 <- "b" # is not output because it is
print(var_1) # because paste is used, var_1/a is output
# but not saved when the variable is assigned
"c" # This is what is saved and assigned to the variable
}
alphabet()
[1] "a"
[1] "c"
# The print is still printed to console, but what is the last line is not
out <- alphabet()
[1] "a"
# The last line is saved into the environment in the "out" variable
out
[1] "c"
# This doesn't exist outside the function, only within the function
# So cannot be retrieved
var_1
Error in eval(expr, envir, enclos): object 'var_1' not found
Before you can use a function that doesn’t exist in base r
, you need to either:
library()
the package that contains the function which loads the function into your environmentpackage_name::function_name()
We recommend using package_name::function_name()
because it keeps your environment clearer and there is no potential confusion if there are multiple functions with the same name.
Plus, it makes it easier for someone reading the code and to look into the correct documentation if necessary.
This works for existing/published packages or your own custom packages. However, for this training, as the functions aren’t packaged, we will rarely use that notation.
## Example 1: Addition
sum_two_numbers <- function(number_1, number_2) {
number_1 + number_2
}
sum_two_numbers(10, 3)
[1] 13
sum_two_numbers(number_1 = 11, number_2 = 22)
[1] 33
## Example 2: Temperature Conversions
# Fahrenheit to Celsius
degree_F_to_C <- function(temp_F) {
(temp_F - 32) * 5 / 9
}
# Freezing point of water
degree_F_to_C(32)
[1] 0
# Boiling point of water
degree_F_to_C(212)
[1] 100
# Celsius to Kelvin
degree_C_to_K <- function(temp_C) {
temp_C + 273.15
}
# Freezing point of water in Kelvin
degree_C_to_K(0)
[1] 273.15
Most functions will be wrapper functions of some sort. They will use existing published functions and/or custom functions to do more complex operations.
## Example 1: Addition
double_sum_two_numbers <- function(number_1, number_2) {
sum_two_numbers(number_1, number_2) * 2
}
double_sum_two_numbers(6, 5)
[1] 22
## Example 2: Temperature Conversions
# Can nest related functions to run one after another
# Freezing point of water in Kelvin
degree_C_to_K(degree_F_to_C(32))
[1] 273.15
# Or combine the functions/processes into a single wrapper function
# This looks like more effort/code lines, but calling it is easier,
# it reads easier and you can add more processes to the function,
# not just nest the two function
degree_F_to_K <- function(temp_F) {
temp_C <- degree_F_to_C(temp_F)
degree_C_to_K(temp_C)
}
# And you can nest it within the function, so it is short, clear, easy to call
degree_F_to_K_nested <- function(temp_F) {
degree_C_to_K(degree_F_to_C(temp_F))
}
# Freezing point of water in Kelvin
degree_F_to_K(32)
[1] 273.15
degree_F_to_K_nested(32)
[1] 273.15
By default, R will match arguments in the order they are used to the order they are in the function syntax.
simple_function <- function(arg_1, arg_2) {
print(paste(arg_1, arg_2, sep = " - "))
}
# Note the first argument relates to arg_1 as the argument name was not specified
# And vice versa
simple_function("this will be first", "this will be second")
[1] "this will be first - this will be second"
# Note the first argument relates to arg_2 as it is specified and vice versa
simple_function(arg_2 = "this will not be first",
arg_1 = "this will not be second")
[1] "this will not be second - this will not be first"
# Another example
minus_number <- function(starting_value, take_away) {
starting_value - take_away
}
minus_number(10, 5)
[1] 5
minus_number(5, 10)
[1] -5
# This is the same because you are specifying the arguments
minus_number(take_away = 10, starting_value = 5)
[1] -5
If an argument that is used in the function is missing, it will error.
However, R executes functions in a lazy fashion. If arguments not required in the function are missing, the function will still be executed. Even though it doesn’t error, best practice is to make sure only arguments used in the code body are included.
## Example 1: Addition
# Function has 3 arguments, even though only 2 are used
double_sum_two_numbers_three <- function(number_1, number_2, number_3) {
sum_two_numbers(number_1, number_2) * 2
}
# All three arguments are provided, even though number3 isn't used, but no error
double_sum_two_numbers_three(6, 94, 20)
[1] 200
# This takes these two arguments as the first two arguments and
# as number3 isn't used, it doesn't matter that it is missing
# Still no error
double_sum_two_numbers_three(88, -4)
[1] 168
# Number_1 is missing and there is no default
double_sum_two_numbers_three(number_2 = -5, number3 = 12)
Error in double_sum_two_numbers_three(number_2 = -5, number3 = 12): unused argument (number3 = 12)
In all the examples above, we have only used arguments that require user inputs.
However, there will be some arguments which have a default value so they do not need to be user defined every time when calling the function.
Defaults can be set when the argument rarely need changing when running the function.
For very bespoke functions, these defaults can be very specific to their use-case.
NA
NULL
TRUE
/FALSE
## Example 1: Addition
double_sum_two_numbers_default <- function(number_1, number_2 = 0) {
sum_two_numbers(number_1, number_2)*2
}
# Just doubles the given argument (takes it as number_1)
# as number_2 is 0 by default
double_sum_two_numbers_default(33)
[1] 66
# This is still missing number_1 as only number_2 had a default so errors
double_sum_two_numbers_default(number_2 = 33)
Error in sum_two_numbers(number_1, number_2): argument "number_1" is missing, with no default
## Example 2: Temperature Conversions
# Here the default is 32 degree F aka 0 degree C
degree_F_to_K_default <- function(temp_F = 32) {
degree_C_to_K(degree_F_to_C(temp_F))
}
# So to use the default, can leave arguments blank
degree_F_to_K_default()
[1] 273.15
Which of these functions has a default argument?
function(first_name, surname) {
function(age) {
function(film_name, year = NA) {
function(first_arg, default) {
Which of these functions has a default argument?
function(first_name, surname) {
function(age) {
function(film_name, year = NA) {
function(first_arg, default) {
When assigning something inside a function, the object will not be saved to the external environment, so you cannot access it after the function has finished running.
Only the final printout of the function (or return()
) or anything that is run in print()
is run.
##### Example 1: Addition
sum_two_numbers_assign <- function(number_1, number_2) {
sum <- number_1 + number_2
}
# Nothing printed out
sum_two_numbers_assign(number_1 = 1, number_2 = 6)
##### Example 2: Temperature Conversions
degree_F_to_K <- function(temp_F) {
temp_C <- degree_F_to_C(temp_F)
degree_C_to_K(temp_C)
}
# Freezing water in Kelvin returns the output
degree_F_to_K(temp_F = 32)
[1] 273.15
degree_F_to_K_assign <- function(temp_F) {
temp_C <- degree_F_to_C(temp_F)
temp_K <- degree_C_to_K(temp_C)
}
# Freezing water in Kelvin will now not print the output
degree_F_to_K_assign(temp_F = 32)
As seen in previous examples, if no argument names are given, the function takes arguments in the order they are given.
When writing your own (and is seen in most published functions), first should always be data.
This is especially important when using in dplyr
pipes.
Unfortunately, there are some older cases where that may not be the case, so it may be worth checking before you use them.
More information about this can be found in the piping section.
##### Exercise 1 -
# Create a function that will divide any number by 15
##### Exercise 2 -
# Create a function that will divide any vector, by any number, with the
# default denominator value as 10.
# Try assigning within the function and outside the function
# to cause the output to be printed vs just being assigned
# Hint: Think about the "Environment"
##### Exercise 3 -
# Create a function that computes weighted average using a vector containing
# all values and a second vector containing their respective weights.
# * there is already function in R that does it, but create your own
data <- c(15, 35, 50, 10)
weighting <- c(3, 5, 3, 9)
# Hint: the formula for weighted average is:
# divide the sum of (data times weighting) by the sum of the weights
##### Exercise 4 -
# Create a function that will multiply selected column name by a given number.
# This should return just the multiplied dataframe column.
# Default multiplier should be 5.
df <- data.frame(
a = 1:3,
b = 4:6,
c = 7:9)
library(dplyr)
##### Answer 1 -----
#' Divide any number/numeric vector by 15
#'
#' @description Takes input of single number or vector of numbers and divides
#' each element by 15.
#'
#' @param x numeric vector
#'
#' @return numeric vector
divide_15 <- function(x) {
x / 15
}
divide_15(150)
divide_15(c(15, 45))
##### Answer 2 -----
#' Divide any numeric vector by any number.
#'
#' @description Divides a numeric vector by any number, default is 10.
#'
#' @param x numeric vector
#' @param denominator numeric, default is 10
#'
#' @return numeric vector
divide <- function(x, denominator = 10) {
x / denominator
}
divide(150)
divide(x = 150, denominator = 15)
divide(150, 15)
# Output does not print to console when this is used - AVOID
divide <- function(x, denominator = 10) {
divided_num <- x / denominator
}
# Can save it as an intermediate and then return it but this is not best
# practice
divide <- function(x, denominator = 10) {
divided_num <- x / denominator
divided_num
}
# When the output of a function is saved as a variable
# the output is not printed to the console
divided <- divide(450)
# But you can now retrieve it with the variable name
divided
##### Answer 3 -----
#' Computes weighted average.
#'
#' @description Computes weighted average from a vector of values,
#' using a vector of respective weights.
#'
#' @param values numeric vector of values/data to be weighted
#' @param weights numeric vector of weights
#'
#' @return numeric weighted average
compute_weighted_avg <- function(values, weights) {
sum(values * weights) / sum(weights)
}
compute_weighted_avg(c(30, 50, 110, 40), weights = c(9, 8, 7, 6))
data <- c(15, 35, 50, 10)
weighting <- c(3, 5, 3, 9)
compute_weighted_avg(data, weighting)
##### Answer 4 -----
df <- data.frame(
a = 1:3,
b = 4:6,
c = 7:9)
#' Multiply selected dataframe column by given number
#'
#' @description Multiply selected column by a given number.
#' Default multiplier is 5.
#'
#' @param df dataframe with column to multiply
#' @param col_name string name of column to multiply
#' @param multiply_by numeric of magnitude to multiply column by, default is 5
#'
#' @return selected dataframe column multiplied by the given number
select_col_multiply <- function(df, col_name, multiply_by = 5) {
df[col_name] * multiply_by
}
select_col_multiply(df, col_name = "b")
select_col_multiply(df, "c", 9)
There are many ways that you could write a function that technically would work. Code within a function should follow standard best practice style (see Tidyverse Style Guide).
However, there are some best practice guidance to follow.
Key point regardless of style you choose to follow: Consistency.
Note: There are packages available to help you adhere to good style:
We recommend lintr
. It doesn’t edit your code directly (as some others do, e.g. styler
) but makes a descriptive list of where it recommends style improvements.
This package may not be perfect and is not a replacement for writing good code in the first place, or being critical about style in general.
install.packages("lintr")
# This is the main function to check style in R scripts. Others exist too.
lintr::lint("filename_filepath.R")
_
If the lines get too long (>79 characters), you can put the arguments on separate lines.
Curly brackets: {
should be the last thing on its line, }
should be the first thing on its line.
Code should be between the {
and }
, not on the same line.
=
, <-
)(
(
and first argument)
)
and {
Code should be indented 2 spaces (plus any further indents depending on the code).
Ctrl+i
in RStudio manages this indentation for you.
R doesn’t need an explicit return()
.
The last line (idea/phrase) of a function is automatically returned.
Tidyverse style guide suggests not including the return()
, but it has been used in some examples for clarity.
Consistency is key.
Furthermore, anything that is inside print()
and warnings/errors will be returned to the console, but not saved as an output of the function.
Use =
inside a functions arguments (i.e. defining a default argument and specifying an argument when calling it).
Use <-
as normal best-practice inside the function code.
Argument names should follow similar rules to function names:
df
for a dataframe argument, x
for an integer)If the argument requires a string, use “string”, rather than ‘string’. Unless there is quote within a string: ‘this is a “quote” string’.
For general arguments, you don’t have to explicitly name the most obvious arguments (e.g. data
) but it is recommended for custom functions and especially optional arguments. It can be very helpful for clarity in complex pipelines.
If using docstrings, minimal comments should be necessary for sub-functions.
Only comments explaining why things are being done should be included, instead of what is being done (same as general commenting style).
If docstrings are not used, should still add general comments on the function. At least for a title, description, any parameters and what is returned.
%>%
style/dplyr
(Advanced)This relates to pipes (%>%
) in code within and outside of functions.
# If there is only one dplyr function, no pipes
dplyr::filter(iris, Petal.Length > 6.5)
Sepal.Length Sepal.Width Petal.Length Petal.Width Species
1 7.6 3.0 6.6 2.1 virginica
2 7.7 3.8 6.7 2.2 virginica
3 7.7 2.6 6.9 2.3 virginica
4 7.7 2.8 6.7 2.0 virginica
# If there are pipes, only one pipe per line and it is the last thing on the line
iris %>%
dplyr::filter(Petal.Length > 6.5) %>%
dplyr::select(Species, Petal.Length, Petal.Width)
Species Petal.Length Petal.Width
1 virginica 6.6 2.1
2 virginica 6.7 2.2
3 virginica 6.9 2.3
4 virginica 6.7 2.0
What are good function and argument names?
DoctorHouse(Patient1, Patient2)
doctorHouse(1stpatient, 2ndpatient)
doctorhouse(patientone, patienttwo)
doctor_house(first_patient, second_patient)
What are good function and argument names?
DoctorHouse(Patient1, Patient2)
doctorHouse(1stpatient, 2ndpatient)
doctorhouse(patientone, patienttwo)
doctor_house(first_patient, second_patient)
Even if you don’t do technical Roxygen headers, you should still have correct level of comments describing what the function does: a title, description, any parameters and what is returned.
However, we recommend giving any custom function the below Roxygen structure as it adds consistency, readability and can save time if you decide to package in the future.
roxygen2
Standard format of headers to add to functions that automate creation of the function documentation. In RStudio, put the cursor inside the function and either:
Ctrl+Shift+Alt+R
Some parts are required, some are optional.
There are many other optional tags.
Note: Best practice is to put sub-functions below the wrapper where it is used.
##### Example 1 - Addition #####
#' Sums two numbers
#'
#' @description Adds two numbers together and returns the sum.
#' The numeric vectors need to be a multiple of each other (or the same).
#'
#' @param number_1 numeric vector
#' @param number_2 numeric vector
#'
#' @return integer
sum_two_numbers <- function(number_1, number_2) {
number_1 + number_2
}
##### Example 2 - Temperature Conversions #####
#' Converts degrees Fahrenheit to degrees Celsius
#'
#' @description Converts degrees Fahrenheit to degrees Celsius,
#' can be used on single number or numeric vector
#'
#' @param temp_F numeric vector of temperature in Fahrenheit
#'
#' @return numeric vector of temperature in Celsius
degree_F_to_C <- function(temp_F) {
(temp_F - 32) * 5 / 9
}
#' Converts degrees Fahrenheit to Kelvin
#'
#' @description Converts degrees Fahrenheit to Kelvin,
#' can be used on single number or numeric vector
#'
#' @param temp_F numeric vector of temperature in Fahrenheit
#'
#' @return numeric vector of temperature in Kelvin
degree_F_to_K <- function(temp_F) {
temp_C <- degree_F_to_C(temp_F)
degree_C_to_K(temp_C)
}
#' Converts degrees Celsius to Kelvin
#'
#' @description Converts degrees Fahrenheit to Kelvin,
#' can be used on single number or numeric vector
#'
#' @param temp_C numeric vector of temperature in Celsius
#'
#' @return numeric vector of temperature in Kelvin
degree_C_to_K <- function(temp_C) {
temp_C + 273.15
}
What parts of Roxygen header you should ALWAYS include?
What parts of Roxygen header you should ALWAYS include?
%>%
The pipe operator %>%
(originally found in package magrittr
) can be used as “and then” to call functions sequentially. It is most commonly found connecting dplyr
functions together.
This replaces the need to save out intermediate results or nest functions.
These all do the same thing:
# Make random vector
x <- runif(100)
# Intermediate saving - Should be avoided as much as possible
out <- sd(x)
out <- mean(out)
sqrt(out)
[1] 0.5306422
[1] 0.5306422
[1] 0.5306422
As mentioned in a previous section, the order of arguments for dplyr
piping is quite important. Data should be the first.
When the functions are called in a pipe, it is assumed that the data from the previous level of the pipe is the default first argument, so it doesn’t need to be specified in each function.
# Using a pre-loaded example dataset called iris
head(iris)
Sepal.Length Sepal.Width Petal.Length Petal.Width Species
1 5.1 3.5 1.4 0.2 setosa
2 4.9 3.0 1.4 0.2 setosa
3 4.7 3.2 1.3 0.2 setosa
4 4.6 3.1 1.5 0.2 setosa
5 5.0 3.6 1.4 0.2 setosa
6 5.4 3.9 1.7 0.4 setosa
select_species_petal_width <- function(data, species_interest, petal_width) {
data %>%
dplyr::filter(Species == species_interest) %>%
dplyr::filter(Petal.Width < petal_width)
# These two filters do not have to be on separate lines
# but here they are split to demonstrate multiple pipes
}
head(select_species_petal_width(iris, "versicolor", 1.5), 10)
Sepal.Length Sepal.Width Petal.Length Petal.Width Species
1 7.0 3.2 4.7 1.4 versicolor
2 5.5 2.3 4.0 1.3 versicolor
3 5.7 2.8 4.5 1.3 versicolor
4 4.9 2.4 3.3 1.0 versicolor
5 6.6 2.9 4.6 1.3 versicolor
6 5.2 2.7 3.9 1.4 versicolor
7 5.0 2.0 3.5 1.0 versicolor
8 6.0 2.2 4.0 1.0 versicolor
9 6.1 2.9 4.7 1.4 versicolor
10 5.6 2.9 3.6 1.3 versicolor
You can combine custom functions with existing functions, but you must be aware of the data argument. You may need to explicitly define that the data is to be used, using: .
, followed by a comma to separate it from the next argument.
iris %>%
select_species_petal_width("versicolor", 1.5) %>%
dplyr::mutate(sepal_width_to_length = Sepal.Width / Sepal.Length) %>%
utils::head(.)
Sepal.Length Sepal.Width Petal.Length Petal.Width Species
1 7.0 3.2 4.7 1.4 versicolor
2 5.5 2.3 4.0 1.3 versicolor
3 5.7 2.8 4.5 1.3 versicolor
4 4.9 2.4 3.3 1.0 versicolor
5 6.6 2.9 4.6 1.3 versicolor
6 5.2 2.7 3.9 1.4 versicolor
sepal_width_to_length
1 0.4571429
2 0.4181818
3 0.4912281
4 0.4897959
5 0.4393939
6 0.5192308
It can often be useful to add warnings and errors into your functions to help usability. Combine conditionals (if
, else
, ifelse
or dplyr::case_when
e.t.c.) with warnings/errors to only show in certain cases.
Using paste
(or paste0
) allows crafting of bespoke warning/error messages, such as what case you are testing.
Doesn’t stop the code from running, but prints the warning to the console.
degree_F_to_C <- function(temp_F) {
if (temp_F < -460) {
warning(paste("Provided temperature (",
temp_F,
") is below absolute zero and thus, is impossible."))
}
(temp_F - 32) * 5 / 9
}
degree_F_to_C(-666)
[1] -387.7778
Will stop the code running at that point and print the error to the console.
degree_F_to_C <- function(temp_F) {
if (class(temp_F) != "numeric") {
stop(paste("Provided temperature in fahrenheit is not numeric. It is a",
class(temp_F),
"object."))
}
(temp_F - 32) * 5 / 9
}
degree_F_to_C("200")
Error in degree_F_to_C("200"): Provided temperature in fahrenheit is not numeric. It is a character object.
What are the correct functions to create a warning and an error? (select 2 answers)
error()
stop()
warnings()
SOMETHING_IS_VERY_WRONG()
halt()
warning()
What are the correct functions to create a warning and an error? (select 2 answers)
error()
stop()
warnings()
SOMETHING_IS_VERY_WRONG()
halt()
warning()
##### Exercise 5 -
# Create a function that will add a new column to existing data frame by
# multiplying another column of that data frame by 3. Default name for the new
# column should be "multiplied".
##### Exercise 6 -
# Create a function that will compute average speed for each car brand in the
# data frame based on distance driven and time it took. You can assume that
# column names will be always the same for this dataset.
cars <- data.frame(
brand = c("Audi", "BMW", "Toyota"),
distance = 4:6, # in miles
travel_time = 7:9) # in hours
# Hint: speed equals distance over time
# EXTRA: Create a function that will tell which car is the fastest?
# Try adding a warning and an error to your functions
#' Add new column to dataframe which is 3x multiplication of an existing column
#'
#' @description Adds a new column to existing data frame by multiplying another
#' column of that data frame by 3.
#' Default name for the new column should be "multiplied".
#'
#' @param df dataframe with at least one numeric column to be multiplied
#' @param col_name string name of numeric column to be multiplied
#' @param new_col_name string name of new column, default is "multiplied"
#'
#' @return dataframe with extra column
new_col_mulitplied <- function(df, col_name, new_col_name = "multiplied") {
df[new_col_name] <- df[col_name] * 3
df
}
new_col_mulitplied(df, col_name = "b")
new_col_mulitplied(df, col_name = "a", "any_colname")
##### Extra - Answer 6 -----
cars <- data.frame(
brand = c("Audi", "BMW", "Toyota"),
distance = 4:6, # in miles
travel_time = 7:9) # in hours
#' Calculate car speed
#'
#' @description Calculates speed of car from distance and travel time
#'
#' @param df dataframe of cars with at least columns of distance and travel_time
#'
#' @return dataframe with new column for speed
car_speed <- function(df) {
df$speed <- df$distance / df$travel_time
df
}
car_speed(cars)
# EXTRA
#' Find the name of the fastest car
#'
#' @description Using variables of distance and travel_time, speed is calculated
#' and car brand name of the fastest speed is returned.
#'
#' @param df dataframe with columns called distance, travel_time and brand
#'
#' @return name of fastest car as a string
#' @export
find_fastest_car <- function(df) {
df <- car_speed(df)
df$brand[df$speed == max(df$speed)]
}
find_fastest_car(cars)
For more information see:
Note that LearningHub resources are only available for colleagues from ONS and other government departments.