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.

Basic R operations

We will first work through some - but far from all - of the most basic elements of R, including:

  • Help commands
  • Arithmetics
  • Logical operators
  • Objects
  • Data Types
  • Arrays (Lists and vectors)
  • Array operators
  • DataFrames
  • DataFrame operators
  • Functions (Defining them)
  • For loops
  • Conditionals
  • Plotting
  • SummarizedExperiment

Looking for help

Stack Overflow and Google are your friends, and you should use them vigorously.

google-so
google-so

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.

Objects and variables

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"

Arithmetics

Arithmetic operations

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"

Mathematical operators

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

Logical operators

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 RHS

COMMENT: 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

Data Classes

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:

  • numerics : real numbers - e.g, 3.0, 3.14 and -1000. More precisely \(x \in \mathbb{R}\)
  • complex : complex numbers - e.g, 1 + 0i, -4 + 3i and 0 + -1i
  • character : characters and strings - e.g., "a", "hello" and "catfish"
  • logicals : boolean values - either 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()

Arrays

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 - lists and vectors

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:

  • list - do : a_list[[i]] (note the double parenthesis),
  • vector - do : 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 element
  • a_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 element
  • a_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"

Lists of Lists and so forth

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.

Vector functions

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()

Structured 2d Arrays

As eluded to above, when we have structured or tabular data there are two data structures that are particularly useful:

  • Matrices - for homogeneous data
  • Data frames - for heterogeneous data

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

Functions for 2D arrays

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 applyfunction 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 : 2
  • FUN : 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 column

As 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()

Functions

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"

For loops

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]"

Conditionals

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()

Exercises

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.

Exercise 1

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

Exercise 2

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

Exercise 3

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 .csvfile ) 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:

Q1

How many different genes have data been collected from? (help : use nrow)

Q2

How many patients have data been collected from? (help : use dim)

Q3

Which 5 genes are most highly expressed across all samples? (help : use sort or order)

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!"

Plotting

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

R/Bioconductor

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 (calling library(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.

Working with SummarizedExperiment

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.

Exercise 4 – Challenge yourself!

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.

Scenario

You are designing a small experiment in R. You can choose the type of dataset you want to create. For example:

  • Gene expression data: expression values of 5–10 genes across 3–5 samples or conditions.
  • Cell counts or measurements: e.g., counts of different cell types under several treatments.
  • Any other simple numeric dataset: as long as it includes at least one categorical variable (condition, treatment, group) and one numeric variable (measurement, expression, score).

Your dataset should be small enough to handle manually but big enough to practice the skills from this lab.

Tasks

  1. Create your dataset
    • Decide on the type of experiment and dataset structure.
    • Create it as a data.frame or tibble.
    • Make sure you have at least one categorical variable and one numeric variable.
    • Assign realistic values for your scenario (you can make them up or use an online resource for inspiration).
# 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)
  1. Write a function that operates on your dataset
    • Your function should take at least one argument and return a meaningful output (numeric or data.frame).
    • You may look online to see which data wrangling or summary operations are commonly applied for the type of dataset you choose, and build your workflow using existing R packages.
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)
  1. Apply your function using a loop or apply function
    • For example, apply your function to each gene, sample, or group.
    • Interpret the results of your 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
  1. Visualize your results
    • Choose the type of plot (barplot, boxplot, dotplot, ggplot, etc.) that best conveys your findings.
    • Label axes and add a title.
    • Optional: use an R package you find online to enhance your plot (e.g., ggpubr, plotly, ComplexHeatmap).
barplot(res2$mean_counts,
        names.arg = res2$condition,
        xlab = "Condition", ylab = "Mean counts",
        main = "Mean expression by condition")
  1. Explain your choices
    • Why did you choose this dataset type and structure? I used a tiny RNA-seq count table (6 genes, 4 samples) in table form (gene, sample, condition, counts). It’s the most common setup in genomics, it includes a categorical variable (control,treatment) and a numeric one (gene expression level) and it’s easy to filter/summarize.
    • Why this function and threshold (if applicable)?
      I took the mean of all RNA-seq data per condition because it’s the simplest, beginner-friendly measurement to compare in the control vs treatment overall.
    • Why this type of plot? A bar plot with two bars is the most direct way to show the average control vs the average treatment. Additionally, it’s minimal and clear, although not so informative.
    • What biological or experimental insight can you extract from your dataset? In my toy data, the treatment mean is higher than the control, driven by big increases in a few genes (ex: gene4-gene6), while other genes (ex: gene1–gene3) barely change. That suggests that treatment induces certain genes.

Guidelines

  • The dataset is synthetic, so there is no “wrong” answer, but you should justify your choices (expression values, threshold, plot type). The purpose is to combine skills from this lab in a coherent workflow.
  • Try to reuse functions and techniques you learned previously (loops, apply, filtering, plotting).
  • You are encouraged to search online for R functions, plotting ideas, or small example datasets to help you complete the task.
  • This exercise is meant to encourage critical thinking and R practice, so spend time explaining your decisions clearly

Hand In Guidelines

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()