Warning: package 'formatR' was built under R version 4.6.1
Warning: package 'webexercises' was built under R version 4.6.1
Modesto
August 16, 2024
August 18, 2026
Warning: package 'formatR' was built under R version 4.6.1
Warning: package 'webexercises' was built under R version 4.6.1
As you already know, R is a GNU Software project with a GPL license. Because R is freely available and widely used, a large community of users has developed many extensions called R packages.
The basic idea behind packages is simple: if many people need to solve similar problems, it is useful to collect the relevant functions, documentation, examples and data in a reusable unit. When you install a package, you add new functions or datasets to your R installation.
Packages are the basic units of reusable and reproducible R code. They can contain:
With RStudio, you can also create your own packages to organise and reuse frequent analyses. This is beyond the scope of this course, but it is important to know that packages are not only something you download: they are also a way of organising your own code.
Packages can be installed from public R repositories. In this course we will mainly refer to two repositories: CRAN and Bioconductor.
CRAN stands for Comprehensive R Archive Network and contains a large collection of packages that can be used free of charge. You can find the complete list of CRAN packages by name or grouped by broad topic in the CRAN Task Views.
As we will see in more detail later in Lesson R8, Bioconductor is a repository focused on bioinformatics and computational biology applications.
Packages in CRAN and Bioconductor are reviewed, but they may change, become archived, or stop working after updates in R, RStudio or external software. You can also find packages on GitHub, GitLab, Bitbucket or other repositories, but these packages may be less stable or less carefully reviewed.
In this course, try to install packages from CRAN or Bioconductor unless there is a clear reason to do otherwise.
Installing a package and loading a package are different operations:
install.packages() downloads and installs the package in your computer.library() loads an installed package into the current R session.You normally install a package only once, but you need to load it again in each new R session before using its functions.
As an example, we could install the package xlsx, which can be used to import and export Excel files. However, this package may require Java in some computers, so it is useful as a teaching example of package requirements.
Some packages have additional system requirements outside R or RStudio. For example, packages that read Excel files may depend on Java or other external libraries. If a package fails to install or load, the problem may not be your code: it may be an external dependency.
After testing with students, we found that xlsx may require the path of your Java executable. For routine Excel import/export, simpler alternatives such as openxlsx or readxl are often easier to use.
You must load a package before using its functions directly. There are two common ways to load an installed package: library() and require().
# Two ways of loading a package:
# 1. library() loads the package or returns an error if it cannot be loaded.
library(xlsx)
# 2. require() tries to load the package and returns TRUE or FALSE.
# It usually gives a warning rather than stopping immediately with an error.
require(xlsx)
# Open the help page for the package, if it is available.
help(xlsx)starting httpd help server ... done
# You can also search help pages from the console with:
# ??xlsx
# A common pattern is to install a package only if it is not already available.
# This is useful in personal scripts, but in teaching materials it may be better
# to ask students to install packages explicitly before class.
if(!require(xlsx)){
install.packages("xlsx")
library(xlsx)
}
# Optionally, unload the package from memory.
# This is not usually needed in short scripts, but it can be useful
# when testing package conflicts.
detach("package:xlsx", unload=TRUE)library() versus require()
library() and require() are often used for similar purposes, but they behave differently when a package cannot be loaded.
library(pkg) is usually preferred in scripts because it clearly fails if the package is missing.require(pkg) returns TRUE or FALSE, which makes it useful inside conditional statements.For beginners, a good rule is: use library() when you simply want to load a package; use require() only when you need to test whether loading succeeded.
In the following example, we try to load a package called uam, which is not expected to exist. Compare the behaviour of library() and require().
Error in `library()`:
! no hay paquete llamado 'uam'
Cargando paquete requerido: uam
Warning in library(package, lib.loc = lib.loc, character.only = TRUE,
logical.return = TRUE, : no hay paquete llamado 'uam'
Which command installs a package called report from CRAN?
Select the right answer
Which command loads an already installed package called report?
Select the right answer
package::function()Sometimes you may want to use a function from a package without loading the entire package with library(). You can do this with the package::function() syntax.
This is also useful when two different packages contain functions with the same name. In that case, the namespace prefix makes your code explicit.
# Example: install a small package from CRAN.
# This chunk is not evaluated when rendering because it installs software.
install.packages("getwiki")
# Open the vignette, if available.
vignette("getwiki")
# Use one function from the getwiki package without loading the package.
DNA <- getwiki::search_wiki("DNA")
str(DNA)
DNA$titlesThe syntax package::function() makes the origin of a function explicit. This is useful in scripts, reports and teaching materials, especially when a function name may exist in more than one package.
In the examples below, we use the ggplot() function from the ggplot2 package. Given the complex syntax of this package (see lesson R5), this syntax does not look very convenient in this case. A complete plot requires several functions, not only ggplot(). This means that using package::function() only once may not be enough.
# Example using the built-in iris dataset.
# No need to load iris: it is already available as a built-in dataset.
# Option 1: no package loaded and no namespace prefix.
# Intentional error: ggplot() and aes() are not available unless ggplot2 is loaded
# or we use the ggplot2:: prefix.
ggplot(iris, aes(Petal.Length, Petal.Width, colour = Species)) +
geom_point()Error in `ggplot()`:
! no se pudo encontrar la función "ggplot"
Error in `aes()`:
! no se pudo encontrar la función "aes"


Note that iris is a built-in R dataset. As we mentioned in Lesson R3, R has some built-in datasets that can be used as examples for plots or analysis. You can check all of them using the function data().
You want to call the function geom_point() from ggplot2 without loading the whole package. Which command uses the correct namespace notation?
Select the right answer
Many CRAN packages include a reference manual, and some also include one or more vignettes. A vignette is a practical guide to a package. You can see the installed vignettes with browseVignettes() or open a specific vignette with vignette().
Error in `find.package()`:
! no hay paquete llamado 'seqinr'
Note that in some cases there may be no official vignette at CRAN. However, as R is an open language, you will often find information about packages or functions in websites such as rdrr.io, RDocumentation or Stack Overflow. Try to distinguish official documentation from user-contributed answers.
As there are many packages, it sometimes takes time to test different packages for the same analysis. Also, quite often, some packages become obsolete after a new R or RStudio version. Thus, it is also useful to know how to uninstall packages with remove.packages().
A reproducible script should make clear which packages are required. At the beginning of a script, load the packages you need with library(). Avoid loading many packages “just in case”, because this can make scripts slower, less clear and more prone to function-name conflicts.
You are reading a script and find this line:
What is the main advantage of using dplyr::filter() instead of just filter()?
Select the right answer
Throughout the previous lessons, we have seen how R can help us save time when analysing and plotting experimental data. However, many analyses involve repeating the same sequence of operations many times, sometimes with slightly different inputs.
Creating your own functions is useful because it allows you to:
In R, functions are also objects. This means that functions can be assigned to names, passed as arguments, returned by other functions, or used inside other functions. We will see useful examples of this in Lesson R6, for example with aggregate(), lapply() or sapply().
The general structure of an R function is:
my_function <- function(argument1, argument2, ...){
statements
return(object)
}
A function usually has:
return().Before writing a function, ask yourself:
A simple function can help us repeat a calculation without copying the same expression many times.
[1] 65.33333
[1] 0
[1] 1943.333
[1] 37
In the previous example, x is a local variable. It only exists inside the function while the function is running.
Consider this function:
What is the output of myFunction(0)?
Select the right answer
We can include conditions, loops and any other R expressions inside a function. The next example checks whether a number is even or odd.
[1] "even"
[1] "odd"
You may sometimes see one-line if statements without curly braces. They can work, but they are easier to misread or break when the code grows. In teaching materials and scripts, curly braces are usually safer.
There is also a quick way to write a function, for small ones or functions that we will use only once, for example when passing it as an argument to another function. In those cases, we can write a short inline function.
We want to write an inline function that transforms concentrations from millimolar (mM) to micromolar (µM). How would you do it?
Select the right answerA slightly harder check: what should f(0.25) return if f converts mM to µM?
Select the right answer
A function can receive more than one input. The next function calculates the area of a rectangle and prints a formatted message.
# A simple R function to calculate the area of a rectangle.
area <- function(length, width){
# Calculate the area.
area <- length * width
# Print a formatted message.
# paste() combines text and values into one character string.
print(paste("The area of the rectangle is", length, "x", width, "=", area, "cm²"))
}
# Call the function.
area(2, 3)[1] "The area of the rectangle is 2 x 3 = 6 cm²"
Printing a message is not the same as returning an object. If you want to reuse the result later, it is usually better to return it.
# Now we calculate area and perimeter of a rectangle.
Rectangle <- function(length, width){
area <- length * width
perimeter <- 2 * (length + width)
# Create a list containing both results.
result <- list("Area" = area, "Perimeter" = perimeter)
# Return the list so it can be stored and reused.
return(result)
}
Rectangle(2, 3)$Area
[1] 6
$Perimeter
[1] 10
Like in any R function, you can call the arguments by position or by name. You can also define default values.
[1] 6
[1] 32
[1] 20
Consider this function:
What is the output of Rectangle(width = 8, length = 4)?
Select the right answer
What is the output of Rectangle()?
Select the right answer
Now, we want a function that helps us with a basic chemistry calculation that you have probably used many times:
\[C_{1} \cdot V_{1} = C_{2} \cdot V_{2}\]
Write a function named dilution that returns \(V_{1}\), given \(C_{1}\), \(C_{2}\) and \(V_{2}\). Test it with an example in which you have a concentrated NaCl solution of 250 mM and you need to prepare 10 mL of buffer containing 10 mM NaCl. How much NaCl stock do you need?
A possible answer is:
When we write functions, we should not only think about what happens when users provide correct input. We also need to think about wrong, unexpected or impossible inputs.
A checkpoint is a line of code that verifies whether an input makes sense before continuing. If the input is not valid, the function can stop with an informative error message.
When designing a function, consider whether inputs should be:
Now we are going to try a longer example. Imagine that a diagnostic service calculates the price of COVID PCR tests depending on the number of samples and the priority category.

How many arguments should this function have?
A first possible version is:
# First version: it works for some inputs, but it has weak input checking.
price_calculator <- function(samples, category){
# Multiplication factors for each category.
categories <- c(1, 1.15, 2)
names(categories) <- c("normal", "priority", "urgent")
# Price per sample depends on the total number of samples.
if(samples < 10){
price <- 19 * samples * categories[which(names(categories) == category)]
} else if (samples < 50){
price <- 14 * samples * categories[which(names(categories) == category)]
} else if (samples >= 50){
price <- 10 * samples * categories[which(names(categories) == category)]
}
paste("The price is", price, "euros.")
}
# Test examples.
price_calculator(samples = 10, category = "normal")[1] "The price is 140 euros."
[1] "The price is 280 euros."
[1] "The price is euros."
which() and switch()
which() gives you the index or indices of a vector, not the actual values. That is why the previous example uses categories[...] to obtain the value of the vector element that matches the selected category.
An alternative is to use switch(), which can directly convert strings such as "normal", "priority" or "urgent" into numerical values. You can find more examples of switch() in this tutorial.
The previous version seems to work, but wrong category names are not handled clearly. We can improve it by adding explicit checkpoints.
# New version with a checkpoint for category.
# This version intentionally avoids match.arg() so that the logic is fully explicit.
price_calculator2 <- function(samples, category = "normal"){
# Define allowed categories.
allowed_categories <- c("normal", "priority", "urgent")
# Check that category is one of the allowed values.
if(!category %in% allowed_categories){
stop("Incorrect category. Use: normal, priority or urgent.")
}
# Convert category into a multiplication factor.
category_factor <- switch(category,
normal = 1,
priority = 1.15,
urgent = 2)
if(samples < 10){
price <- 19 * samples * category_factor
} else if (samples < 50){
price <- 14 * samples * category_factor
} else if (samples >= 50){
price <- 10 * samples * category_factor
}
return(price)
}
# Test examples.
price_calculator2(10, "normal")[1] 140
[1] 280
Error in `price_calculator2()`:
! Incorrect category. Use: normal, priority or urgent.
[1] 100.7
We just noticed that the function calculated the price for 5.3 samples, which does not make sense. We should introduce a second checkpoint for the format of samples.
# Alternative with checkpoints for category and number of samples.
# Again, we use explicit if statements because they are easier to read
# when learning how checkpoints work.
price_calculator3 <- function(samples, category = "normal"){
# Define allowed categories.
allowed_categories <- c("normal", "priority", "urgent")
# Check that category is one of the allowed values.
if(!category %in% allowed_categories){
stop("Incorrect category. Use: normal, priority or urgent.")
}
# Check that samples is numeric.
if(!is.numeric(samples)){
stop("Number of samples must be numeric.")
}
# Check that there is only one value.
if(length(samples) != 1){
stop("Please provide a single number of samples.")
}
# Check that samples is a positive integer.
# samples %% 1 is 0 for integer-like numbers such as 5, 10 or 50.
if(samples <= 0 || samples %% 1 != 0){
stop("Number of samples must be a positive integer.")
}
# Convert category into a multiplication factor.
category_factor <- switch(category,
normal = 1,
priority = 1.15,
urgent = 2)
# Calculate the base price according to the number of samples.
if(samples < 10){
price <- 19 * samples * category_factor
} else if (samples < 50){
price <- 14 * samples * category_factor
} else {
price <- 10 * samples * category_factor
}
return(price)
}
# Test again.
price_calculator3(50, "urgent")[1] 1000
Error in `price_calculator3()`:
! Incorrect category. Use: normal, priority or urgent.
Error in `price_calculator3()`:
! Number of samples must be a positive integer.
Error in `price_calculator3()`:
! Number of samples must be a positive integer.
Error in `price_calculator3()`:
! Please provide a single number of samples.
match.arg()
R includes a useful function called match.arg() that can check whether a character argument matches one of several allowed values. It is very common in R functions, but in this lesson we use explicit if statements with %in% because they make the logic easier to see.
For example, a more compact version of the category checkpoint would be:
Here, match.arg(category) checks that category is one of the allowed values in c("normal", "priority", "urgent"). If the user writes an invalid value, the function stops with an error. This is elegant, but it hides some of the checking logic, so we keep the explicit version in the main example.
In price_calculator3(), which input should produce an error because the number of samples is not a positive integer?
Select the right answer
You want to check whether category is one of "normal", "priority" or "urgent". Which condition is correct?
Select the right answer
When creating functions, you can include any R functionality, including reading and parsing external data files. Let’s do a short exercise in the context of molecular biology.
Can you write a function that converts a codon into its corresponding amino acid?
You can use the table genetic_code.csv, located in the data folder. The table contains the conversion between codons and amino acids in IUPAC one-letter code.
Try it with some examples, including correct and incorrect codons, such as "ATG", "TAA", "CAT", or "AXG".
# The molecular biology dogma with R.
codon2aa <- function(inputCodon) {
# Read the genetic code table.
# stringsAsFactors = FALSE prevents character columns from being converted
# to factors in older versions of R.
code <- read.csv2("data/genetic_code.csv", stringsAsFactors = FALSE)
# Find the amino acid corresponding to the input codon.
aa <- code$AA[code$Codon == inputCodon]
# Return the result.
return(aa)
}
# Try it with valid and invalid codons.
codon2aa("ATG")[1] "M"
[1] "*"
[1] "H"
character(0)
Can you also check the value of the variable aa outside the function?
Error:
! objeto 'aa' no encontrado
What happened?
character(0).aa is local to the function and cannot be called outside it.However, proteins are made up of more than one amino acid, so it would be useful if the function could accept a vector of several codons instead of a single codon.
The next version adds a loop and a checkpoint.
# Version 2: translate a vector of codons.
codon2aa_2 <- function(codons) {
# Create an empty vector to store the amino acids.
aa <- c()
# Read the genetic code table.
code <- read.csv2("data/genetic_code.csv", stringsAsFactors = FALSE)
# Loop over all elements of the codons vector.
for (i in 1:length(codons)){
# Check that the current codon exists in the genetic code table.
stopifnot("One or more codons are not valid. Translation stopped." = codons[i] %in% code$Codon)
# Store the corresponding amino acid in position i.
aa[i] <- code$AA[code$Codon == codons[i]]
}
return(aa)
}
# Try it.
codon2aa_2(c("ATG", "TGA"))[1] "M" "*"
Error in `codon2aa_2()`:
! One or more codons are not valid. Translation stopped.
[1] "M" "H" "H" "K" "*"
In this second example, aa is not a scalar value but a vector, so we define it before filling it inside the loop. We also used stopifnot() to stop the function if a codon is not found.
print(), return() and <<-
Objects created inside a function are usually local variables. This means that they exist only while the function is running.
For example:
Here, area is created inside the function. The function can return its value, but the object area does not automatically appear in the Global Environment:
The first line works because the function returns the value of area. The second line gives an error because area was local to the function.
A useful distinction is:
print() shows something in the console.return() sends an object back from the function so that it can be reused.<<- can modify an object outside the function, but it should be used with care.Compare:
Both functions show a result when we run them:
But if we want to store and reuse the result, return() is the safer choice:
In this course, we will normally write functions that receive inputs, calculate something and return an output.
You may sometimes see <<-:
This modifies counter outside the function. Although this can be useful in special cases, it makes code harder to follow and debug. For beginner scripts, prefer the clearer pattern:
This version is longer, but clearer: the function receives an input, calculates a new value and returns it.
Why does print(aa) fail after running codon2aa("ATG")?
Select the right answer
Write a function that calculates your approximate age in months.
Hints:
A first possible solution is:
# Quick version using Sys.Date() and as.Date().
age <- function(x){
# Check whether x can be interpreted as a date in YYYY-MM-DD format.
if (!is.na(as.Date(as.character(x), format="%Y-%m-%d"))){
# Difference between today and the birth date, in days.
age <- as.numeric(Sys.Date() - as.Date(x, format="%Y-%m-%d"))
# Approximate months by dividing by 30.
print(paste("You are about", round(age/30, 1), "months old"))
} else {
stop("The input date must be quoted and in format YYYY-MM-DD")
}
}
# Test examples.
age(18)Error in `age()`:
! The input date must be quoted and in format YYYY-MM-DD
[1] "You are about 264.8 months old"
A second possible solution can ask the user for input when the date is missing.
# Alternative using interactive input and difftime().
df <- function(x){
# If x is missing, ask the user for input.
# This is interactive, so it is more useful in the console than in a rendered page.
if (missing(x)){
x <- readline(prompt="Introduce your birthdate in the format YYYY-MM-DD: ")
}
# Check whether x can be interpreted as a date.
if (!is.na(as.Date(as.character(x), format="%Y-%m-%d"))){
df <- difftime(Sys.Date(), as.Date(x, format="%Y-%m-%d"))
print(paste0("If you were born on ", x, ", you are about ", round(as.numeric(df)/30, 1), " months old."))
} else {
stop("The input date must be in format YYYY-MM-DD")
}
}
df("2004-11-17")[1] "If you were born on 2004-11-17, you are about 264.8 months old."
Error in `df()`:
! The input date must be in format YYYY-MM-DD
Introduce your birthdate in the format YYYY-MM-DD:
Error in `df()`:
! The input date must be in format YYYY-MM-DD
Write a function that translates any nucleotide sequence into a protein sequence.
This review exercise extends the codon exercise above. The goal is not only to obtain the translated sequence, but also to practise how to design a robust function with clear checkpoints.
Your function should:
A, C, G and T.substr().warning() when one or two nucleotides at the end of the sequence are not used because the sequence length is not a multiple of three.stop() message when the sequence contains invalid characters.Hints:
nchar() to count the number of nucleotides.substr() to divide the sequence into codons.read.csv2() to read the genetic code table.paste(..., collapse = "") to combine amino acids into a single sequence.readLines() to read the file lacz.fa, which contains the nucleotide sequence of the E. coli lacZ gene in FASTA format.Suggested tests:
Part II:
Check the following links for more short exercises about functions in R.
R version 4.6.0 (2026-04-24 ucrt)
Platform: x86_64-w64-mingw32/x64
Running under: Windows 11 x64 (build 26200)
Matrix products: default
LAPACK version 3.12.1
locale:
[1] LC_COLLATE=Spanish_Spain.utf8 LC_CTYPE=Spanish_Spain.utf8
[3] LC_MONETARY=Spanish_Spain.utf8 LC_NUMERIC=C
[5] LC_TIME=Spanish_Spain.utf8
time zone: Europe/Madrid
tzcode source: internal
attached base packages:
[1] stats graphics grDevices utils datasets methods base
other attached packages:
[1] ggplot2_4.0.3 webexercises_1.1.0 formatR_1.14 knitr_1.51
loaded via a namespace (and not attached):
[1] vctrs_0.7.3 cli_3.6.6 rlang_1.2.0 xfun_0.57
[5] otel_0.2.0 generics_0.1.4 S7_0.2.2 jsonlite_2.0.0
[9] rJava_1.0-18 labeling_0.4.3 glue_1.8.1 htmltools_0.5.9
[13] xlsxjars_0.9.0 scales_1.4.0 rmarkdown_2.31 grid_4.6.0
[17] tibble_3.3.1 evaluate_1.0.5 fastmap_1.2.0 yaml_2.3.12
[21] lifecycle_1.0.5 compiler_4.6.0 dplyr_1.2.1 RColorBrewer_1.1-3
[25] pkgconfig_2.0.3 htmlwidgets_1.6.4 rstudioapi_0.19.0 farver_2.1.2
[29] digest_0.6.39 R6_2.6.1 tidyselect_1.2.1 pillar_1.11.1
[33] magrittr_2.0.5 withr_3.0.2 tools_4.6.0 gtable_0.3.6