Welcome to the first lab! The objective here is to get you
familiar with the programming language R
and thus
prepare you for the more advanced labs. If you work through these
exercises, things will be much easier later on.
With that being said, let’s get started!
NOTE: Instructions regarding how to hand in this report are found at the very end of this document.
We will first work through some - but far from all - of the most
basic elements of R
, including:
Stack Overflow and Google are your friends, and you should use them vigorously.
However, sometimes you don’t have to leave your R session to get the
answers you are looking for. You can get tons of information and
examples of usage for most functions in R
by typing:
?function_name
or help(function_name)
, e.g.
?sum
in your R console.
Variables is one of the most basic features of any programming
language; in short you can think of them as placeholders. \(pi\) is a great example of a real-life
variable, we use it whenever we want to indicate the value \(3.14159265...\). Similarly, we can assign
values or objects a specific name (e.g, fancy_variable
),
one huge benefit with this is that if we decide to change the value of
fancy_variable
from 3
to 4
, only
the instance where we define the value of fancy_variable
will have to be changed rather than all places where it is used.
In R, you have 2 options when you want to bind an object or value to
a name var_name <- value
and
var_name = value
. For assignment, the best practice is to
always use <-
, this is in line with Google’s R Style
Guide (widely used).
# bind the values 1337 to the variable fancy_name_1
fancy_name_1 <- 1337
# print the variable fancy_name_1
print(sprintf("fancy_name_1 is : %d",fancy_name_1))
## [1] "fancy_name_1 is : 1337"
One variable can be used in the assignment of a value to another variable, as you may see below:
# bind the sum of fancy_name_1 and 6671 to the variable fancy_name_2
fancy_name_2 <- fancy_name_1 + 6671
# print fancy_name_2
print(sprintf("fancy_name_2 is : %d",fancy_name_2))
## [1] "fancy_name_2 is : 8008"
Computation with and manipulations of numbers are integral parts of any programming language, hence it’s essential to know how to call for these kind of actions. Below you will see some examples of the more common mathemathical operations that you will use.
7 + 4 # => 11 (addition)
7 - 4 # => 3 (subtraction)
7 / 2 # => 3.5 (division)
7 * 2 # => 14 (multiplication)
2 ^ 3 # => 8 (exponents)
7 %% 3 # => 1 (modulo)
As in standard mathematics, parentheses are used to indicate the order by which these operations should be executed. For example:
ex_1 = 7 * 4 / 3 ^ 2
ex_2 = 7 * ( 4 / 3 ) ^ 2
ex_3 = ((7 * 4) / 3) ^ 2
print(sprintf("ex_1 : %f",ex_1))
## [1] "ex_1 : 3.111111"
print(sprintf("ex_2 : %f",ex_2))
## [1] "ex_2 : 12.444444"
print(sprintf("ex_3 : %f",ex_3))
## [1] "ex_3 : 87.111111"
There’s a plethora of different mathematical functions in ‘R’, usually their names resemble those of the mathematical operations they execute, for example:
log2(8) # => 3
abs(-9.12) # => 9.12
sqrt(4) # => 2
Once calculations have been performed using different arithmetic
operations, we usually want to assess and compare our results;
that’s when logical operators come in handy; when used, they return a
value that says either TRUE
or FALSE
depending
on the expression you wrote down. Some of the most useful logical
operators are:
==
: equal to - TRUE
if LHS is equal to
RHS>
: greater than - TRUE
if LHS is
greater than RHS>=
: greater than or equal to - TRUE
if
LHS is greater than or equal to RHS<
: less than - TRUE
if LHS is less
than RHS<=
: less than or equal to - TRUE
if
LHS is less than or equal to RHS!=
: not equal to - TRUE
if LHS is not
equal to RHSCOMMENT: LHS = Left hand side, RHS = Right hand side
For some examples:
3 == 3 # TRUE
2 > 1 # TRUE
9 < 7 # FALSE
4 != 5 #TRUE
5 < 5 #FALSE
5 <= 5 #TRUE
Of course we can also compare variables that we have assigned values to:
# Assign values to two variables
foo <- 3.141
bar <- (foo / 2)^3 - 2
# compare their values
foo_greater_than_bar <- foo > bar
# print the value of foo_greater_than_bar
print(foo_greater_than_bar)
## [1] TRUE
# fancy print
print(sprintf("Is foo greater than bar? Answer: %s!",
ifelse(foo_greater_than_bar,"Yes","No")))
## [1] "Is foo greater than bar? Answer: Yes!"
There are also ways of linking logical expressions, and thus creating a compound expression. For example, you could check whether two expressions are true at the same time. The two variants that you mainly will encounter in these labs are:
&
- AND : both expressions must be
TRUE
|
- OR : one of the two expressions must be
TRUE
What do we mean by expressions then? This is best illustrated by some examples:
# set variable values
expr_1 <- 4 < 3
expr_2 <- 9 < 13
# print values of expression 1 and 2
print(paste("The value of expr_1 is :",expr_1))
## [1] "The value of expr_1 is : FALSE"
print(paste("The value of expr_2 is :",expr_2))
## [1] "The value of expr_2 is : TRUE"
# make compound expressions
expr_3 <- expr_1 & expr_2 # is both expr_1 and expr_2 true? (AND operator)
expr_4 <- expr_1 | expr_2 # is either of expr_1 and expr_2 true? (OR operator)
# print values of expression 3 and 4
print(paste("The value of expr_3 is :",expr_3))
## [1] "The value of expr_3 is : FALSE"
print(paste("The value of expr_4 is :",expr_4))
## [1] "The value of expr_4 is : TRUE"
NOTE: if you want to link expressions rather than variables, it’s good practice to use parentheses
expr_1 <- 4 < 2 | 2 < 1 # bad practice
expr_1 <- (4 < 2) | (2 < 1 ) # good practice
Every (programming) language has it’s own classes of objects, and you have already encountered some of them in this lab! Just to get you familiar with the terminology, some of the atomic classes (that build up every other class in R) are:
3.0
, 3.14
and -1000
. More precisely \(x \in
\mathbb{R}\)1 + 0i
,
-4 + 3i
and 0 + -1i
"a"
,
"hello"
and "catfish"
TRUE
or
FALSE
A lot of errors that arise when coding can be traced back to combining the wrong classes. For example, if we were to try and add a character to a numeric:
test_1 <- 3 + "a"
Would give you the error message
Error in 3 + "a" : non-numeric argument to binary operator
R, being a high level programming language, implements automatic type coercion - meaning you don’t really have to pay too much attention to making sure you are using the correct types all the time. For example, if you add a numeric and complex value, the numeric is converted to a complex type and all is good in the hood:
a_numeric <- 3
a_complex <- 4 - 2i
print(paste("a_numeric + a_complex = ",a_numeric + a_complex))
## [1] "a_numeric + a_complex = 7-2i"
WARNING : while the programming becomes easier (e.g., compared to C/C++ or Java) with this automatic coercion, you should also be cautious - sometimes unexpected results and errors may be introduced. Take a look at what happens when you compare a character with a numeric:
## [1] "a > 3 is : TRUE"
Eh what…? This makes no sense at all, how can we even compare a
character with a number? Shouldn’t this, if something, raise an error?
It should indeed, but this is an example of R
trying to
help us a little too much.
What’s happening here is that the numeric 3
gets
coerced into the character "3"
which can
be compared to another character (if you, like in R, have assigned an
order to the characters).
# Remove all variables created in this section
rm(list = c(
"ex_1", "ex_2", "ex_3",
"foo", "bar", "foo_greater_than_bar",
"expr_1", "expr_2", "expr_3", "expr_4",
"a_numeric", "a_complex",
"result_1"
))
# Run garbage collection to free up memory
gc()
The next element to introduce is the array, this is another fundamental structures that you will use a lot. In a nutshell, arrays let us associate multiple values to a single object. We’ll be focusing on 1 and 2 dimensional arrays, let us start with 1d arrays first.
1d arrays come in two flavors, either as lists or as vectors. Which you create accordingly:
a_list <- list(19660206,
"Rick Astley",
"never",
0,
"no",
"you")
a_vector <- c("wind",
"1A",
"earth",
"3B",
"flat",
"water",
"5G",
"round")
To access the i:th element of a:
a_list[[i]]
(note the double
parenthesis),a_vector[i]
To exemplify, let us access the 4th element from our vector and list respectively:
a_list_4th_element <- a_list[[4]]
a_vector_4th_element <- a_vector[4]
print(paste("The fourth element of our list is : ",a_list_4th_element))
## [1] "The fourth element of our list is : 0"
print(paste("The fourth element of our vector is : ",a_vector_4th_element))
## [1] "The fourth element of our vector is : 3B"
We could also assign “names” to our array elements (note how we are
using =
and not <-
):
a_list <- list(birth_date = 19660206,
name = "Rick Astley",
occurance = "never",
problems = 0,
beef = "no",
whom = "you")
a_vector <- c(element_1 = "wind",
slot_1 = "A1",
element_2 = "earth",
slot_2 = "B2",
shape_1 = "flat",
element_3 = "water",
slot_3 = "5G",
shape_2 ="round")
We can still access our elements by specifying their position, but also by their names; sometimes, this allows you to write more readable code and makes it easier to access elements of your arrays.
To access an element by name, for lists use the
$
symbol or [['name']]
; for vectors only the
second option ([[name]]
) is valid. To illustrate:
# accessing the elements from our list
print(paste("His name is",
a_list$name,
"and he will",
a_list[["occurance"]],
"give",
a_list[["whom"]],
"up"
)
)
## [1] "His name is Rick Astley and he will never give you up"
# accessing the elements from our vector
print(paste("We all know that the",
a_vector[["element_2"]],
"is",
a_vector[["shape_1"]],
"and that",
a_vector[["slot_3"]],
"caused COVID"
)
)
## [1] "We all know that the earth is flat and that 5G caused COVID"
There’s a nice command called names
that we can use to
check the names of our list and vector elements respectively:
print("The names of our list elements are:")
## [1] "The names of our list elements are:"
print(names(a_list)) # here we use the names function to access the names of a_list
## [1] "birth_date" "name" "occurance" "problems" "beef"
## [6] "whom"
print("The names of our vector elements are:")
## [1] "The names of our vector elements are:"
print(names(a_vector)) # here we use the names function to access the names of a_vector
## [1] "element_1" "slot_1" "element_2" "slot_2" "shape_1" "element_3"
## [7] "slot_3" "shape_2"
If we want to subset our lists and vectors we could either do:
a_list[1:3]
- creates a new list including the first to
third elementa_list[c(name_1,name_2)]
- creates a new list with
elements “name_1” and “name_2”a_vector[1:3]
- creates a new vector including the
first to third elementa_vector[c(name_1,name_3)]
- creates a new vector with
elements “name_1” and “name_3”As an example:
a_list_subset <- a_list[1:3] #subset a_list
a_vector_subset <- a_vector[c("slot_1","slot_2","slot_3")] #subset a_vector
print('The subsetted list "a_list_subset" is:')
## [1] "The subsetted list \"a_list_subset\" is:"
print(a_list_subset)
## $birth_date
## [1] 19660206
##
## $name
## [1] "Rick Astley"
##
## $occurance
## [1] "never"
print('The subsetted vector "a_vector_subset" is:')
## [1] "The subsetted vector \"a_vector_subset\" is:"
print(a_vector_subset)
## slot_1 slot_2 slot_3
## "A1" "B2" "5G"
Lists and vectors seem pretty similar, but there is one - very important - feature that differs between them:
Vectors are homogeneous while lists can be heterogeneous.
This means that all elements in a vector must be of the same type, and if they aren’t they will be coerced to it, i.e., you can not have a vector of both numerics and characters. See what happens when we try to declare such a vector:
mixed_vector <- c(1, #numeric
"foo", #character
-2, #numeric
"bar" #character
)
# print the type of the first element in "mixed_vector"
print(paste("The type of the first element of mixed_vector is :",
class(mixed_vector[1])))
## [1] "The type of the first element of mixed_vector is : character"
If we use a list instead, the output will be different:
mixed_list <- list(1, #numeric
"foo", #character
-2, #numeric
"bar" #character
)
# print the type of the first element in "mixed_list"
print(paste("The type of the first element of mixed_list is :",
class(mixed_list[[1]])))
## [1] "The type of the first element of mixed_list is : numeric"
The elements of our lists are not restricted to singular values, but could also be another list or vector. For example:
a_list_of_mixed <- list(list_1 = list("crystals","heals",FALSE),
list_2 = list(6,"days",7,"hours","stuck",TRUE,"Ever Given"),
vector_1 = c("Person", "Woman", "Man", "Camera", "TV")
)
NOTE : This functionality does not exist for vectors. If you try to put vectors into a vector, they will be “flattened” and just appended to the vector.
If we want to access the second element in the vector that makes up
the third element of a_list_of_mixed
we would do:
# alternative 1
second_of_third_1 <- a_list_of_mixed[[3]][2]
# alternative 2
second_of_third_2 <- a_list_of_mixed$vector_1[2]
print(paste("(Alt 1) The second element of the third element is :",second_of_third_1))
## [1] "(Alt 1) The second element of the third element is : Woman"
print(paste("(Alt 2) The second element of the third element is :",second_of_third_2))
## [1] "(Alt 2) The second element of the third element is : Woman"
The object a_list_of_mixed
to some extent represent a 2d
array, we can traverse both a_list_of_mixed
itself (first
dimension) and then the objects that constitute its elements (second
dimension).
When the lists or vectors that we want to store are of different
length, using a list is the best option, BUT if they are of
equal length (structured arrays) there are better options to choose
from, being the matrix
and dataframe
types -
more on these soon.
There are many functions that can help us extract useful information from our vectors, just to expose you to a few:
# define a new vector
a_new_vector <- c(4.1,-2,9.19,18.19,5.21)
#some functions and vector operations
max(a_new_vector) # => max value : 18.19
min(a_new_vector) # => min value : -2
range(a_new_vector) # => min and max value :c(-2, 18.19)
a_new_vector + a_new_vector # => elementwise addition : c(8.20,-4.00,18.38,36.38,10.42)
a_new_vector * a_new_vector # => elementwise multiplication : c(16.8100,4.0000,84.4561,330.8761,27.1441)
a_new_vector %*% a_new_vector # => dot product : 463.2863
4.1 %in% a_new_vector # => check if 4.1 is in vector : TRUE
111 %in% a_new_vector # => check if 111 is in vector : FALSE
# Remove all arrays, lists, vectors, and related variables
rm(a_vector,
a_list_4th_element, a_vector_4th_element,
a_list_subset, a_vector_subset,
mixed_list, mixed_vector,
a_list_of_mixed,
second_of_third_1, second_of_third_2,
a_new_vector
)
# Force garbage collection
gc()
As eluded to above, when we have structured or tabular data there are two data structures that are particularly useful:
As a reference:
a_matrix <- matrix(data = c(4,1,2,9,-11,-8,1,3,4,21,0,1),
ncol = 4,
nrow = 3
)
a_df <- data.frame(birth_date = c(a_list$birth_date,19691294),
first_name = c(a_list$name,"Jay"),
problems = c(a_list$problems,99),
beef = c(a_list$beef,"yes"),
row.names = c("ind1","ind2")
)
print("this is a matrix")
## [1] "this is a matrix"
print(a_matrix)
## [,1] [,2] [,3] [,4]
## [1,] 4 9 1 21
## [2,] 1 -11 3 0
## [3,] 2 -8 4 1
print("this is a data frame")
## [1] "this is a data frame"
print(a_df)
## birth_date first_name problems beef
## ind1 19660206 Rick Astley 0 no
## ind2 19691294 Jay 99 yes
Analogously to lists and vectors, we can also extract the names of
the the columns and rows in our 2D arrays, but now we use the functions
colnames
and rownames
instead.
# print rownames and colnames of a_matrix
print("The row/colnames of a_matrix are:")
## [1] "The row/colnames of a_matrix are:"
print(rownames(a_matrix))
## NULL
print(colnames(a_matrix))
## NULL
# print rownames and colnames of a_df
print("The row/colnames of a_df are:")
## [1] "The row/colnames of a_df are:"
print(rownames(a_df))
## [1] "ind1" "ind2"
print(colnames(a_df))
## [1] "birth_date" "first_name" "problems" "beef"
As you can see, our matrix does not yet have any row/colnames, to change this we can do:
rownames(a_matrix) <- c("A","B","C")
colnames(a_matrix) <- c("a","b","c","d")
# print rownames and colnames of a_matrix
print("The row/colnames of a_matrix are:")
## [1] "The row/colnames of a_matrix are:"
print(rownames(a_matrix))
## [1] "A" "B" "C"
print(colnames(a_matrix))
## [1] "a" "b" "c" "d"
To access elements in a matrix we do
a_matrix[row_id,col_id]
, if we want to access all elements
in a specific column then we do a_matrix[,col_id]
and
similarly for a row a_matrix[row_id,]
. As an example:
# access the element in the second row and third column in a_matrix
a_matrix[2,3]
## [1] 3
# access all elements in the first column
a_matrix[,1]
## A B C
## 4 1 2
# access the first and fourth in the second row
a_matrix[2,c(1,4)]
## a d
## 1 0
The same conventions can be used for data frames to access elements,
but you can also use the $
operator to access columns in
data frames. However, if you do a_df[,"col_name"]
or
a_df$col_name
this will return a vector and not a data
frame. To extract a column and preserve its row/colnames do
a_df["col_name"]
.
To exemplify:
# Alt1. extract first columns by [,col_name]
first_col_1 <- a_df[,"birth_date"]
# Alt2. extract first column by $col_name
first_col_2 <- a_df$birth_date
# Alt3. extract first column by [col_name]
first_col_3 <- a_df["birth_date"]
print("(Alt1) : By [,col_name]")
## [1] "(Alt1) : By [,col_name]"
print(first_col_1)
## [1] 19660206 19691294
print("(Alt2) By $col_name :")
## [1] "(Alt2) By $col_name :"
print(first_col_2)
## [1] 19660206 19691294
print("(Alt3) By [col_name] :")
## [1] "(Alt3) By [col_name] :"
print(first_col_3)
## birth_date
## ind1 19660206
## ind2 19691294
We saw how we could apply certain functions to our vectors in order to obtain for example min and max values, we could do the same for our data frames and matrices.
a_matrix_max <- max(a_matrix)
a_matrix_min <- min(a_matrix)
print(paste("the max value of a_matrix is :", a_matrix_max))
## [1] "the max value of a_matrix is : 21"
print(paste("the min value of a_matrix is :", a_matrix_min))
## [1] "the min value of a_matrix is : -11"
that is convenient, but what if we wanted the max value in each column and minimum value in respective row, how do we do this?
One possible solution would be:
a_matrix_col_max <- c(max(a_matrix[,1]),max(a_matrix[,2]),max(a_matrix[,3]),max(a_matrix[,4]))
a_matrix_row_min <- c(min(a_matrix[1,]),min(a_matrix[2,]),min(a_matrix[3,]))
print("max in each column")
## [1] "max in each column"
print(a_matrix_col_max)
## [1] 4 9 4 21
print("min in each row")
## [1] "min in each row"
print(a_matrix_row_min)
## [1] 1 -11 -8
Whatever you do, please do not do this. In programming this is what
we call a “shitty ass solution”. The main issue here is scaleability,
this is not a feasible approach if you had a 1000x1000 matrix for
example. Instead of this disaster of a solution, we will use a handy
function called apply
.
Let’s see how this apply
function works:
?apply
The help section may a bit “icky” to interpret, but in short it says
that if we have an array X
and a function FUN
we can apply this function along any dimension (MARGIN
) by
using apply, it will then treat the rows/columns as individual vectors
to which FUN
is applied.
NOTE : rows are considered the first dimension, columns the second
Let’s say that we want to compute the max value of each column
(dimension 2) in a_matrix
, then we have:
X
: a_matrix
MARGIN
: 2FUN
: max
(note we do not call
max
here, only provide the name)in action this becomes:
a_matrix_col_max <- apply(a_matrix,2,max)
print(a_matrix_col_max)
## a b c d
## 4 9 4 21
COMMENT: For lists and vectors, similar functions to
apply
exist (check out lapply
and
sapply
).
Since programmers are inherently lazy, some operations - like getting
the column sums - have been included as standard functions in R, meaning
you don’t have to use apply
but can call them directly.
Some examples are:
{row,col}Sums
: compute and return the total sum for
each row or column{row,col}Means
: compute and return the mean for each
row or columnAs you can see the results are the same as if we had used
apply
:
a_matrix_row_mean_1 <- apply(a_matrix,1,mean)
a_matrix_row_mean_2 <- rowMeans(a_matrix)
print(paste("Alt1. rowmeans with apply"),a_matrix_row_mean_1)
## [1] "Alt1. rowmeans with apply"
print(a_matrix_row_mean_1)
## A B C
## 8.75 -1.75 -0.25
print(paste("Alt2. rowmeans with rowMeans"),a_matrix_row_mean_2)
## [1] "Alt2. rowmeans with rowMeans"
print(a_matrix_row_mean_2)
## A B C
## 8.75 -1.75 -0.25
# Remove all variables created in this section
rm(
a_matrix, a_df,
first_col_1, first_col_2, first_col_3,
a_matrix_max, a_matrix_min,
a_matrix_col_max, a_matrix_row_min,
a_matrix_row_mean_1, a_matrix_row_mean_2
)
gc()
Variables allow you to specify a certain value once instead of every time you use it, functions does the same but for actions. In short, you associate a certain action to a function name, and whenever you want to execute that action, you simply call the function.
We call the input to our functions “parameters” or “arguments”.
You have already used several “pre-defined” functions, like
apply
and max
- but sometimes (quite often) we
want to expand the repertoire of actions that we can call for, as well
as customizing them for our own needs, that’s when we define our own
functions.
The basic structure for a function is as follows:
Say for example that we want a function called
sphere_volume
that computes the volume of a sphere given
its radius, the mathematical formula for this is:
\[\begin{equation} V(r) = \frac{4\pi r^3}{3} \end{equation}\]
Let us use \(3.14\) as an approximation of \(\pi\), then our function would look like:
sphere_volume <- function(r) {
pi_approx <- 3.14
volume <- 4.0 / 3.0 * r ^3 * pi_approx
return(volume)
}
Let us now test our function
# set radii values
radius_1 <- 9
radius_2 <- 2
# compute volumes
volume_1 <- sphere_volume(radius_1)
volume_2 <- sphere_volume(radius_2)
# print results
msg <- "The volume of a sphere with radius %0.f is %f"
print(sprintf(msg,radius_1,volume_1))
## [1] "The volume of a sphere with radius 9 is 3052.080000"
print(sprintf(msg,radius_2,volume_2))
## [1] "The volume of a sphere with radius 2 is 33.493333"
Just as variables and functions, loops are key features of almost any
programming language. In R we have while
,
repeat
and for
loops, but here we’ll focus to
the for
loops.
Briefly summarized, a for loop iterates over some values in a 1d array and, at each iteration, executes a certain code block. The “skeleton” of a for loop is as follows:
# DO NOT RUN CHUNK
for (i in vec) {
# loop body
}
here vec
would be the array we iterate over. To give a
less abstract example, we can define a vector that contains all numbers
from 1 to 5, done by vec <- c(1:5)
and then in each
iteration print the value we are at:
vec <- c(1:5)
for (i in vec) {
print(i)
}
## [1] 1
## [1] 2
## [1] 3
## [1] 4
## [1] 5
the elements in our arrays do not have to be numbers or given in a sequential order:
vec <- c("gato","cat","katt","paka")
for (x in vec) {
print(x)
}
## [1] "gato"
## [1] "cat"
## [1] "katt"
## [1] "paka"
Of course, for loops are not only used to print stuff, we could for example use them to generate a Fibonacci series, defined as:
\[\begin{equation}\ F_n =\Bigg{\{} \begin{array}{cl} 0 & \textrm{if } n = 1 \\ 1 & \textrm{if } n = 2 \\ F_{n-1} + F_{n-2} & \textrm{else} & \\ \end{array} \end{equation}\]\end{equation}
NOTE: the above equation looks better in the knitted document
Implementing this in could for example look like:
# set desired number of elements in the Fibonacci series
n_elements <- 10
# create a vector F_ to store our elements
# we let F_[1] = 0 since F_1 = 0
# and
# F_[2] = 1 since F_2 = 1
# in the series
F_ <- c(0,1)
# iterate over n=3 to n=10
for (n in c(3:n_elements)) {
# compute the next element in the sequence
x <- F_[n-1] + F_[n-2]
# add element to the F_ vector
F_ <- c(F_,x)
}
# print result
print(sprintf("The first %d numbers of the Fibonacci series are: [%s]",
n_elements,
paste(F_,collapse = ", ")
)
)
## [1] "The first 10 numbers of the Fibonacci series are: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]"
Sometimes we want our actions to depend on the output of a function or the value of a certain variable; as an example from real life, the clothes you put on is (hopefully) fairly dependent on the temperature outside.
To implement these dependencies we use conditionals, which are :
if
, else if
and else
. The way we
structure our conditional statements is as follows:
# DO NOT RUN CHUNK
if (condition_1) {
# code to execute if condition_1 is true
} else if (condition_2) {
# code to execute if condition_1 is FALSE
# and condition_2 is TRUE
} else {
# code to exectue if none of the
# conditions above are TRUE
}
NOTE: You do not have to include an
else if
or else
statement, an if
statement can be used standalone, the reverse is not true.
NOTE: You can add as many else if
statements as you want, but only one if
and
else
NOTE: You should always follow the
order of the statements above: first if
, then
else if
(if you have any) and last else
To give you an example, run the chunk below a couple of times to see how the conditionals work:
# get a random number between -10 to 25
temperature <- runif(1,-10,25)
# standard text to print
txt <- sprintf("The temperature is %0.f, so",temperature)
# if temperature is below 5 degrees
if (temperature < 5) {
print(paste(txt,"put on a jacket!"))
# if temperature less than 12 degreees (but more than 5)
} else if ( temperature < 12) {
print(paste(txt,"bring a shirt!"))
# if temperature is more than 12 degrees
} else {
print(paste(txt,"it's summertime bby!"))
}
## [1] "The temperature is 14, so it's summertime bby!"
# Remove all variables created in this section
rm(
radius_1, radius_2, volume_1, volume_2, msg,
pi_approx, F_, n_elements, x,
vec, i, x, temperature, txt
)
# Optionally remove the function if you also want to clean it
rm(sphere_volume)
# Force garbage collection to free RAM
gc()
Now when we’ve walked through some of the basics of R, you are more than ready for some exercises. To pass the lab, all exercises must be correctly completed.
Create a data frame (named count.data
) with 3 samples
and 5 genes. Rows are samples, genes are columns. Make all count values
= 1 Hint: One way is to generate a matrix
with the
value one for all positions and then convert it to a
dataframe
.
#INSERT CODE HERE
count.data <- data.frame(sample_code = c("Sample_1", "Sample_2", "Sample_3"),
Gene_1 = c(1, 1, 1),
Gene_2 = c(1, 1, 1),
Gene_3 = c(1, 1, 1),
Gene_4 = c(1, 1, 1),
Gene_5 = c(1, 1, 1))
print(count.data)
## sample_code Gene_1 Gene_2 Gene_3 Gene_4 Gene_5
## 1 Sample_1 1 1 1 1 1
## 2 Sample_2 1 1 1 1 1
## 3 Sample_3 1 1 1 1 1
Make sample 2 have double the amount of counts for each
gene. Note that you should update the existing count.data
object here not generate a new dataframe.
#INSERT CODE HERE
count.data[2,-1 ] <- count.data[2,-1 ] * 2
print(count.data)
## sample_code Gene_1 Gene_2 Gene_3 Gene_4 Gene_5
## 1 Sample_1 1 1 1 1 1
## 2 Sample_2 2 2 2 2 2
## 3 Sample_3 1 1 1 1 1
Make sample 3 have the the following counts for each gene (32,16,8,4,2)
#INSERT CODE HERE
count.data[3,-1] <- c(32,16,8,4,2)
print(count.data)
## sample_code Gene_1 Gene_2 Gene_3 Gene_4 Gene_5
## 1 Sample_1 1 1 1 1 1
## 2 Sample_2 2 2 2 2 2
## 3 Sample_3 32 16 8 4 2
Cut the count values for gene 2 in half within sample 2 and 3, and add 3 counts to gene 4 for sample 1
#INSERT CODE HERE
count.data[c(2,3),3]<- count.data[c(2,3),3] / 2
count.data[1,5] <- count.data[1,5] + 3
print(count.data)
## sample_code Gene_1 Gene_2 Gene_3 Gene_4 Gene_5
## 1 Sample_1 1 1 1 4 1
## 2 Sample_2 2 1 2 2 2
## 3 Sample_3 32 8 8 4 2
Calculate the mean value of each gene and store this as a vector
named vec
#INSERT CODE HERE
vec <- sapply(count.data[, 2:6], mean)
print(vec)
## Gene_1 Gene_2 Gene_3 Gene_4 Gene_5
## 11.666667 3.333333 3.666667 3.333333 1.666667
Test run the code below (if no error/warning -> continue with lab)
source("checks.R")
check_exercise_1(vec)
## [1] "Great job! The answer is correct."
rm(count.data, vec) # remove objects no longer needed
gc() # reclaim RAM
In the code chunk below, a vector representing a genomic sequence is generated, each element in the vector is one base in the sequence. You do not have to pay attention to this particular code but should run it.
sequence <- sample(c("A","C","T","G"),
size = 200,
replace = TRUE
)
sequence <- as.vector(sequence)
print(sprintf("the sequence is : %s",paste0(sequence,collapse = "")))
## [1] "the sequence is : ACTCTGATAGCAAGCGAGAATGGGGCTTCAGCACTCCCGGCTTAGGGAATAGGTAAATACAGAATGCGACATTGGCACTGATGACAACGTACTCTCAAAAAGGGTGTTTGTAAGCATTGTGCATGGCACAATGTTGCCAGGATTCTCAACTTGGTCTCCATACAACGCGCGGCCAAGCCTTTCTTAGAGCTAGGCTCTCG"
your task is to write a function called
base_count
that counts the number of occurrences of each
base, i.e., how many A,C,T and G’s does sequence
contain,
to be more specific:
base_count(sequence)
should return a vector where the
elements are named “A,C,T,G” and their associated value represents the
number of each base in sequence
. This vector could, for
example, look like c(A = 11, C = 132, T = 761, G = 41)
.
# Possible long solution
base_count <- function(sequence){
a <- 0
g <- 0
c <- 0
t <- 0
for (i in sequence){
if (i == "A") {
a <- a + 1
}
else if (i== "T"){
t <- t + 1
}
else if (i== "C"){
c <- c +1
}
else {
g <- g + 1
}
}
vector_bases <- c("A" = a, "C" = c, "T" = t, "G" = g)
print(vector_bases)
return(vector_bases)
}
To test your solution, run the following code chunk:
source("checks.R")
answer <- base_count(sequence)
## A C T G
## 54 47 48 51
check_exercise_2(sequence,answer)
## [1] "Congrats you did it!"
rm(sequence) # remove objects no longer needed
gc() # reclaim RAM
We usually store data in external files, meaning we usually have to
read it from these files. If we have a comma or tab separated
file (extensions .csv
and .tsv
) one way of
doing this is by using the read.table
function.
If your fields are separated by commas (as in a .csv
file
) use the argument sep=','
, if they are tab separated use
the argument sep = '\t'
.
We will read our data from the file “reads.tsv”, it is a count matrix with genes as rows and samples as columns. To read this file the command looks as follows:
counts <- read.table("reads.tsv", sep = "\t")
You could now answer the following questions:
How many different genes have data been collected from? (help : use
nrow
)
How many patients have data been collected from? (help : use
dim
)
Which 5 genes are most highly expressed across all samples? (help : use
sort
ororder
)
Put your answers into a list ans_ex3
with the following
structure:
# THIS IS AN EXAMPLE, DO NOT EDIT
ans_ex3 <- list(a1 = NA, #answer to question 1 - numeric
a2 = NA, #answer to question 2 - numeric
a3 = NA, #answer to question 3 - character vector
)
Answer with the specified format (e.g, a numeric), not a text or written answer.
# WRITE CODE TO GENERATE ans_ex3 HERE
nrow(counts)
## [1] 19027
dim(counts)[2]
## [1] 864
gene_totals <- rowSums(counts)
top5 <- head(sort(gene_totals, decreasing = TRUE), 5)
# modify this vector
ans_ex3 <- list(a1 = nrow(counts),
a2 = dim(counts)[2],
a3 = names(top5))
print(ans_ex3)
## $a1
## [1] 19027
##
## $a2
## [1] 864
##
## $a3
## [1] "ENSG00000198712" "ENSG00000198938" "ENSG00000228253" "ENSG00000198886"
## [5] "ERCC-00130"
To check that your answers are correct, run the following code chunk:
source("checks.R")
check_exercise_3(ans_ex3)
## [1] "well done, all answers are correct!"
Sooner or later you will come to a point in your work where you want to visualize your results. Visualization is usually helpful for you and others to interpret the results. We will therefore explore how we may plot data in R.
There are several different built in functions that allow you to plot your data, you find some examples of these below:
# Define a vector with 5 values
c0 <- c(1, 3, 6, 4, 9)
# Graph the vector c0
plot(c0)
# Set title of the current plot
title(main="Plot of single vector c0")
# Define 2 new vectors
c1 <- c(1, 3, 6, 4, 9)
c2 <- c(2, 5, 4, 5, 12)
# Plot c1 using a y axis that ranges from -1 to 13
plot(c1, # data to plot
col="blue", # color
ylim=c(-1,13) # y-axis range
)
# Graph c2 with red dashed line and square points
lines(c2, # data to plot
type = "b", #type of plot
pch=22, # marker type
lty=2, # line type
col="red" # color
)
# Create a title with a red, bold/italic font
title(main="Two Lines", col.main="black")
# We can also make a histogram of our data
hist(c1)
We can use a histogram to assess our count data; by plotting the
rowsums (total observed gene expression across all samples) in a
histogram we are able to see how these (the sums) are distributed. We
will log
the sums in order to reduce the variance and the
dynamic range of our data.
#Distribution of gene counts
hist(log(rowSums(counts)),
main = "Logged Total Gene Counts"
)
# cleanup
rm(c0, c1, c2)
gc()
## used (Mb) gc trigger (Mb) max used (Mb)
## Ncells 668621 35.8 1343211 71.8 1343211 71.8
## Vcells 9548119 72.9 32825413 250.5 41018138 313.0
Bioconductor is a repository of R-packages specifically for biological analyses. It has relatively strict requirements for submission, including installation on every platform and proper documentation with a required tutorial (called a vignette) explaining how the package should be used. You will need this in the upcoming labs.
TLDR: “Bioconductor is a collection of bioinformatic tools”.
Installs from Bioconductor is handled by the BiocManager library. BiocManager should already be installed in your environment.
There are a lot of “packages” within bioconductor
that you can install and use. To install a package from bioconductor use
BiocManager::install(PACKAGE_NAME)
where you replace
PACKAGE_NAME
with the package of interest,
e.g. BiocManager::install("SummarizedExperiment")
.
NB! Notation like this
BiocManager::install(PACKAGE_NAME)
allow us to use a certain function (install
) from a specific library (BiocManager
) without importing said library into the namespace (callinglibrary(BiocManager)
). As some libraries will share functions, function could become shadowed, therefore this notation is useful to make sure that you are requesting a function from a particular library.
Again, “SummarizedExperiment” should already be installed.
The SummarizedExperiment class is used to store matrices containing experimental results, which are commonly produced by sequencing experiments. Each object stores observations of one or more samples, along with additional meta-data describing both the observations (features) and samples (phenotypes).
A key aspect of the SummarizedExperiment
class is its
coordination of the meta-data and assay when sub-setting. For example,
if you want to exclude a given sample you should do this both in the
count and meta-data in one operation, which ensures the meta-data and
observed data will remain synchronized.
Bad coordination of meta-and count data has resulted in a number of incorrect results and paper retractions, hence why this is a very desirable property.
Lets try it out!
We begin by loading the required package, the
SummarizedExperiment
class is contained in - unsurprisingly
- the package with the name SummarizedExperiment
. Go ahead
and load it:
library(SummarizedExperiment)
Next we load a second package that contains same data, this package
is called airway
:
library(airway)
# Load data
data(airway)
# Bind object to new identifier
se <- airway
se
## class: RangedSummarizedExperiment
## dim: 63677 8
## metadata(1): ''
## assays(1): counts
## rownames(63677): ENSG00000000003 ENSG00000000005 ... ENSG00000273492
## ENSG00000273493
## rowData names(10): gene_id gene_name ... seq_coord_system symbol
## colnames(8): SRR1039508 SRR1039509 ... SRR1039520 SRR1039521
## colData names(9): SampleName cell ... Sample BioSample
Within the summarizedExperiment, you can store all kinds of data.
Type colData(se)
to view all sample data included. Type
rowData(se)
to view all gene data.
To access a particular sample’s data, just type
se$sample
.
You can also store meta data for the entire data set, type
metadata(se)
to view it.
In this exercise, you will apply your newly acquired R skills to a dataset you create yourself. The goal is to practice data manipulation, writing functions, looping/applying operations, and plotting, while making your own choices and justifying them. You are encouraged to search online for functions, examples, or packages that may help you complete the task.
You are designing a small experiment in R. You can choose the type of dataset you want to create. For example:
Your dataset should be small enough to handle manually but big enough to practice the skills from this lab.
data.frame
or tibble
.# Experiment: RNA-seq counts for 6 genes across 4 samples (2 Control, 2 Treatment)
df <- data.frame(
gene = rep(paste0("gene", 1:6), times = 4),
sample = rep(c("S1","S2","S3","S4"), each = 6),
condition = factor(rep(c("Control","Control","Treatment","Treatment"), each = 6),
levels = c("Control","Treatment")),
counts = c(
88000, 78000, 28000, 6000, 1800, 110, # S1 (Control)
92000, 81000, 30000, 6400, 2100, 90, # S2 (Control)
90000, 80000, 31000, 11000, 3900, 750, # S3 (Treatment)
93000, 82000, 32000, 12000, 4200, 850 # S4 (Treatment)
)
)
df
print(df)
data.frame
).mean_by_condition <- function(df) {
control_vals <- df$counts[df$condition == "Control"]
treatment_vals <- df$counts[df$condition == "Treatment"]
control_mean <- mean(control_vals)
treatment_mean <- mean(treatment_vals)
data.frame(
condition = c("Control", "Treatment"),
mean_counts = c(control_mean, treatment_mean)
)
}
mean_by_condition(df)
apply
function
conds <- c("Control", "Treatment")
means <- numeric(length(conds))
for (i in seq_along(conds)) {
means[i] <- mean(df$counts[df$condition == conds[i]]) # add na.rm=TRUE if needed
}
res2 <- data.frame(condition = conds, mean_counts = means)
res2
barplot
, boxplot
,
dotplot
, ggplot
, etc.) that best conveys your
findings.ggpubr
, plotly
,
ComplexHeatmap
).barplot(res2$mean_counts,
names.arg = res2$condition,
xlab = "Condition", ylab = "Mean counts",
main = "Mean expression by condition")
apply
, filtering, plotting).How: First, go to the top of this document, on line
23 set GRADE_MODE = TRUE
. Next, knit this document, and
hand in the resulting html-file on Canvas. Make sure you have solved all
the questions. If the document doesn’t compile, there is something wrong
with the code.
Deadline: Your report is due 23:59 one week after the scheduled lab session; if working in pairs - each of you should hand in two (identical) reports where the names of both authors are clearly stated. For further information see the guidelines on the course web-page.
sessionInfo()