Aims
This is not a course to learn R. The aim of this tutorial is to offer a very short introduction to the parts of R that you will need to follow the other tutorials in the stocnet packages.
By the end of this tutorial, you should be able to:
If you would like to develop your skills further (not such a bad idea) there are plenty of excellent online courses and resources available. Recommended elsewhere are the following:
- R for Data Science, free online
- An Introduction to R, the official manual
- Posit cheatsheets, for quick reference
- swirl, which teaches R interactively inside R
These sites can help you learn or refresh your memory. But you can also expect to search the web or ask your favoured chatbot often as you go and type “R …” as a query. That’s fine, and totally normal, and I encourage you to use the learning tools that help you upskill. We return to how to ask for help towards the end of this tutorial.
Software
For this course, you will need to download and install two pieces of software, R and RStudio, to your system. Since you are completing this tutorial, we assume you have already done so, but here we briefly explain the purpose of each.
What is R?
R is a programming language and environment for statistical computing and graphics. R is available as Free Software under the terms of the Free Software Foundation’s GNU General Public License, and provides a wide variety of statistical (linear and nonlinear modelling, classical statistical tests, time-series analysis, classification, …) and graphical techniques, and is highly extensible. This means that anybody can write extensions to R and make them publicly available, such as in the stocnet group of packages…
What is RStudio?

RStudio is an integrated development environment (IDE) for R and Python, enabling researchers to interact with R (and/or Python) through a fully-functional editor with syntax highlighting, direct code execution, autocomplete, and various tools for plotting, history, debugging, package development, and workspace management. Positron is a newer IDE from the same company that works in much the same way; either is fine for these tutorials.
In the end, although you will need to make sure R has been downloaded and installed correctly on the system you are using, in practice you may never open it directly. Instead you will typically be using RStudio (or Positron) to interact with R. Think of R as the internals of the calculator, and RStudio as a good case with all the buttons you need. Let’s start the calculator by opening the ‘calculator case’ app, RStudio.
Getting started
RStudio and R scripts
If we open and take a look around RStudio, we should see a window of four (4) panes. Among them there should be a console: this is where RStudio executes commands in R. You can type commands yourself (RStudio may help by suggesting autocompletions), but we usually write code in an R script instead (File > New File > R Script), and then tell RStudio when to execute one or more lines from the script. There are basically three reasons for using a script: editing, repetition, sharing. You can run a command in RStudio by moving the cursor to the line or lines you want to run and then press Cmd-Enter (Mac) or Ctrl-Enter (Windows). You can try this with the following lines:
1 + 5 # This will print the result
105 * 99 + 6 # An asterisk is used for multiplication
Note that R won’t execute anything after a comment #.
Remove the hash symbol at the start of this line to run it:
# 1/5 # this will still be commented...
In an R script you can toggle commenting for one or more lines using Cmd-Shift-C/Ctrl-Shift-C. If you try to run a commented out line, it pass it and look further down the script until it finds the next uncommented command line.
Beyond a calculator
Ok, wow, R is a calculator! But it is also much, much more than that… Try the following command:
print("Hello World")
You’ve told R to print a string of text (identified by the quotation marks) to the console. Much more flexible than a high school calculator!
It is important to note that R is case-sensitive,
i.e. Print("Hello World") will not work. Try it!
Print("Hello World")
This means that james is not the same as
JAMES (and Hollway is not the same as
Holloway…). In R, we can write such logical statements
as:
"James" == "james" # Try also "James" != "james"
# Other logical statements include: ">", ">=", "<=", "<".
# 1 < 5 # Try also "1 <= 5"
Logical values are always either TRUE or
FALSE. You may see these abbreviated as T or
F in older code, but please try to always write them out in
full.1 Why do we have to use two equals
signs and quotation marks? Quotation marks tell R you are referring to a
string of text and not a named object. And a single equals
sign, as we will see next, is used for something else entirely.
TandFare ordinary objects that can be overwritten, whileTRUEandFALSEcannot.↩︎
Objects
Values
An object is a placeholder R uses for one or more numbers, strings,
or other things. You can assign such things to an object using one
= sign, but it’s better to use <- to avoid
confusion with == in logical statements.
city <- "Geneva"
lakeside <- TRUE
tram_lines <- 6
# Note that these objects then appear in RStudio's environment pane
# (by default the top right)
You can then recover this information by simply calling these objects:
city
lakeside
tram_lines
And even operate on them:
tram_lines * 3
# Try multiplying the other objects by 3. What happens?
Types
Multiplying city by 3 fails, but multiplying
lakeside by 3 works. That is because every object has a
type (or class), and the type determines what you can
do with it. You can check the type of any object with
class():
class(city)
class(lakeside)
class(tram_lines)
TRUE behaves like 1 and FALSE
like 0 in arithmetic, which is why
lakeside * 3 returned 3. You can convert
between types with the as.*() family of functions:2
as.numeric("42") # text to number
as.character(42) # number to text
as.numeric("forty-two") # this cannot be converted...
as.numeric(TRUE)
A special type for categories is the factor. Factors look like text, but R remembers the set of possible categories (the “levels”):
languages <- as.factor(c("French", "German", "French", "Italian"))
languages
levels(languages)
Note that manynet coercion functions begin with
as_notas..↩︎
Vectors
We can also concatenate multiple values together using the function
c():
lived <- c("New Zealand", "UK", "New Zealand", "Germany", "UK", "Switzerland")
All the values in a vector must be of the same type. If you mix them,
R converts them all to the most flexible type. Try
c(1, "a", TRUE) to see what happens.
There are several shortcuts for making a series of values. For example, consecutive numbers can be produced with:
teenageyrs <- 13:19
teenageyrs
teenageqrtrs <- seq(13, 19.99, by = 0.25)
teenageqrtrs
length(teenageqrtrs)
Missing data
Real data is rarely complete. Someone skipped a survey question, a
record was lost, or a value simply was not observed. R marks such
unknown values with NA (“not available”).
It is important to understand that NA is not
the same as zero, and not the same as an empty string "".
Zero siblings is a known answer; NA siblings means we do
not know.
siblings <- c(1, 0, NA, 3)
siblings
is.na(siblings)
Because R does not know what an NA is, it is careful:
most calculations involving an NA also return
NA.
siblings <- c(1, 0, NA, 3)
NA + 1
mean(siblings)
If you are happy to ignore the missing values, many functions accept
an argument na.rm = TRUE (“NA remove”):
siblings <- c(1, 0, NA, 3)
mean(siblings, na.rm = TRUE)
sum(is.na(siblings)) # how many values are missing?
Note that you cannot find missing values with
siblings == NA. Try it: the answer is itself unknown!
Always use is.na() instead.
Indexing
By position
Where was the fourth place I lived? We use square brackets
[ ] for indexing, or extracting, parts of an object:
lived[4]
lived[2:3]
lived[-1] # a minus sign drops elements
By logic
We can also index with a vector of
TRUE/FALSE values. R then keeps only the
elements that are TRUE. This is very powerful, because
logical statements return exactly such vectors:
lived == "UK"
lived[lived != "UK"]
lived[lived %in% c("Germany", "Switzerland")] # %in% checks membership
which(lived == "New Zealand") # which positions are TRUE?
By name
Elements can also have names, and then we can index them by name as
well. The $ and [[ operators extract a single
named element:
ages <- c(Anna = 34, Ben = 29, Chiara = 41)
ages["Ben"]
ages[["Ben"]] # note that this drops the name
The difference between [ and [[ is subtle
for vectors, but will matter more for lists (see below). Think of
[ as returning a smaller box of the same kind, and
[[ as taking the contents out of the box.
Data structures
So far we have worked with single vectors. Data can be combined in R into several structures. Here we introduce three of the most common only briefly, together with how to index them. You will meet them again (and see how they relate to networks) in later tutorials.
Matrices
A matrix is a table of values of a single type, created by populating
a given number of rows and columns with data. Assigning,
<-, doesn’t print any output unless you wrap the line in
parentheses:
(my_matrix <- matrix(data = 1:9, nrow = 3, ncol = 3))
We can index cells of a matrix using square brackets with a comma
[ , ]. Left of the comma is the row, right of the comma is
the column. Leave one side empty to get a whole row or column:
my_matrix[2, 3]
my_matrix[2, ]
my_matrix[, 3]
We can even overwrite particular cells of the matrix by assigning a new value to those indexed cells:
my_matrix[my_matrix == 6] <- 600
my_matrix
Data frames
Data frames are like matrices, but each column can hold a different type of variable, such as logical, numeric, or character variables. Each column is a vector, and all columns must be the same length. Replace the missing data (the NAs) with your details:
mydf <- data.frame(Surname = c("Hollway", NA),
Born = c("New Zealand", NA),
Siblings = c(1, NA))
Can you call the data frame and print it to the console?
mydf
We can index a data frame in the same way as the matrix above,
e.g. mydf[2, 2], but we can also call a named variable
using the $ sign:
mydf$Surname
mydf[mydf$Surname == "Hollway", ]
Note that NAs are propagated in logical statements, so the second line above returns also the row with the NA Surname. You may also come across tibbles. These are a modern kind of data frame that print more compactly, but otherwise work in much the same way.
Lists
Lists are the most flexible structure: each element can be anything, of any length, even another list. Many functions return their results as lists.
mylist <- list(Surname = "Hollway",
Siblings = 1,
Lived = c("New Zealand", "UK", "New Zealand",
"Germany", "UK", "Switzerland"))
mylist
Here the difference between [ and [[
becomes clear:
mylist["Lived"] # a list containing one element
mylist[["Lived"]] # the element itself, a character vector
mylist$Lived # the same as [[ ]]
mylist$Lived[4] # and we can keep indexing from there
Note that we’ve been using parentheses, (), for things
like list() and c(), and not brackets,
[], as we did when we were indexing. Parentheses are used
for functions.
Functions
Functions are sets of actions or algorithms that are applied to values, vectors, or objects.
exp(0.09855)
mean(c(1, 5, 8, 7, 6, 4, 22, 1, 0.9))
Arguments
Usually every function must be followed by (). Some
functions work without any “arguments” though; that is, with empty
parentheses.
ls() # This tells you what objects are in your environment
Sys.Date() # This tells you today's date
Most functions take one or more arguments though. Arguments can be given by name, or by position:
round(x = 3.14159, digits = 2)
round(3.14159, 2) # the same, by position
round(digits = 2, x = 3.14159) # the same, by name in any order
round(3.14159) # digits has a default value of 0
Many arguments are switches that take TRUE or
FALSE, like the na.rm argument we saw above.
Functions include defaults so that they work even if you do not specify
all the possible arguments. It is usually fine to give the first (main)
argument by position, but good practice to name the others, to avoid
unexpected results and make your code easier to read.
Help
How do you know which arguments a function accepts? Every function
has a help file, which you can access by putting a ? before
the function name:
?round # Forgot the exact name of the function? Use ?? to search...
Help files always follow the same structure. The most useful sections are:
- Usage: how to call the function, with each argument’s default value
- Arguments: what each argument means and what type of value it expects
- Value: what the function returns
- Examples: code you can copy and run; often the quickest way to understand a function
Pipes
When applying several functions one after another, code can quickly become hard to read, because nested functions must be read from the inside out:
scores <- c(3.14159, 2.71828, 1.41421, 1.61803, 0.57722)
head(sort(round(scores, 1), decreasing = TRUE), 3)
The pipe operator |> lets us write the same thing as
a chain, from left to right. It takes the result of the code on its left
and uses it as the first argument of the function on its right. When
piping over multiple lines, put the pipe operator at the end of each
line:
scores <- c(3.14159, 2.71828, 1.41421, 1.61803, 0.57722)
scores |>
round(1) |>
sort(decreasing = TRUE) |>
head(3)
You can read |> as “and then”: take the scores,
and then round them, and then sort them, and
then take the first three. Try removing the last line (and the pipe
before it) to see the intermediate result.
|> is R’s native pipe operator, available since R
v4.1.0, which most of you will have installed by now. You may still come
across %>% in older code and tutorials, which does much
the same thing but comes from either the {magrittr} or
{dplyr} packages, and so requires that package to be loaded
first.
Changing data frames
Two operations you will need again and again are adding (or changing)
a variable, and keeping only some observations. The {dplyr}
package offers two functions for this: mutate() and
filter(). We write dplyr:: before each
function to tell R which package it comes from (more on packages at the end).
cities <- data.frame(city = c("Geneva", "Zurich", "Basel", "Bern"),
population = c(203, 421, 173, 134),
lakeside = c(TRUE, TRUE, FALSE, FALSE))
cities
If the population variable is in thousands, we can add a
new variable for population in millions using mutate().
mutate() can add or change a variable, including
from other variables in the same data frame:
cities |>
dplyr::mutate(population_millions = population / 1000)
filter() keeps only the rows for which a logical
statement is TRUE:
cities |>
dplyr::filter(lakeside == TRUE)
Note that inside these functions we can refer to variables by name
directly, without cities$. And they can be chained together
with pipes. Can you keep only the cities with more than 150 thousand
inhabitants, and then add the population in millions?
cities
cities |>
dplyr::filter(population > 150) |>
dplyr::mutate(population_millions = population / 1000)
Note that neither function changes cities itself; it
just prints it with the changes. To keep the result, assign it on the
first line to an object with <-,
e.g. cites <- cities |> ....
Random numbers
Some functions give a different answer every time you run them,
because they involve chance. For example, sample() draws
values at random, like rolling a die:
sample(1:6, size = 1) # roll one die
sample(1:6, size = 10, replace = TRUE) # roll ten dice
Run this code chunk a few times: the results change. The same is true
of rnorm(), which draws random numbers from a
normal (bell-curve) distribution:
rnorm(5)
This randomness is not a problem (indeed it is crucial for many statistical methods), but it can be surprising when your results differ slightly from someone else’s, or from the results expected in a tutorial.
Computers do not really produce truly random numbers. They produce long sequences of numbers that only look random, starting from a number called a seed. If you set the same seed, you get the same sequence every time:
set.seed(1234)
sample(1:6, size = 10, replace = TRUE)
set.seed(1234)
sample(1:6, size = 10, replace = TRUE) # the same rolls again!
So whenever your code involves chance, it is good practice to put
set.seed() (with any number you like) near the top of your
script. That way you, and anyone you share your script with, will get
exactly the same results each time.
Errors and getting help
Reading errors
Everyone who writes R code gets errors, all the time. Errors are R’s way of telling you what went wrong, so it pays to actually read them. An error message usually tells you which function failed and some information that might help you understand why. Sometimes a little bit of knowledge helps inference about the cause of the error though.
Each of the following lines contains a common mistake. Uncomment one line at a time, run it, read the error, and then fix it. (If you uncomment them all at once, a missing bracket or quotation mark can confuse R about where the next line starts, and the error message will point to the wrong place.)
Mean(c(1, 2, 3))
# mean(c(1, 2, 3)
# print("Hello World)
# tram_lines
mean(c(1, 2, 3)) # R is case-sensitive
mean(c(1, 2, 3)) # every ( needs a )
print("Hello World") # every " needs a "
tram_lines <- 6 # objects must be created before they are used
tram_lines
Other common mistakes include:
- using
=when you meant==in a logical statement - forgetting a comma between arguments, or between rows and columns in
[ , ] - misspelling an object or function name (RStudio’s autocomplete helps here)
- forgetting to load a package before using one of its functions
Note that warnings are different from errors. A warning
means R did something, but perhaps not what you expected. Recall
as.numeric("forty-two") from above.
Asking for help
If you cannot fix an error yourself, search the web for the error message, ask on StackOverflow or CrossValidated, ask a colleague, TA, or your favourite LLM. AI assistants can also be helpful for explaining errors, but always check that their suggestions actually work and make sense.
If you think you have found a bug in one of the stocnet packages, please open an issue on the package’s GitHub page, e.g. github.com/stocnet/migraph/issues. These packages are made for you, and your feedback adds value for everyone.
Reproducible examples
Whoever you ask, you will get a faster and better answer if you include a reproducible example (sometimes called a “reprex”). A reproducible example is the smallest piece of code that someone else can copy, paste, and run to see the same problem you see. It includes:
- the packages you use, loaded with
library() - a small dataset, created in the code itself (not a file on your computer that nobody else has)
set.seed(), if your code involves chance- the code that causes the problem, and nothing more
- the full error or warning message
- your R and package versions, from
sessionInfo()
For example:
# I expected the mean to be 2, but I get NA. Why?
scores <- data.frame(id = 1:3,
score = c(1, NA, 3))
mean(scores$score)
#> [1] NA
sessionInfo()
To share your own data in such an example, dput() prints
R code that recreates an object exactly. Try it, and then copy the
output into a new line and assign it to an object:
dput(head(mtcars, 3))
The {reprex} package can also help: copy your code and
run reprex::reprex(), and it will run the code and format
the result ready to paste into a question.
Very often, the act of cutting your problem down to a minimal example helps you find the solution yourself!
Files and projects
This section is optional, but useful once you start working with your own data.
Projects and working directories
R reads and writes files relative to a working directory on your computer.
getwd() # This tells you the directory R is working in
list.files() # This tells you what files are in that directory
Rather than setting the working directory by hand with
setwd(), we recommend using RStudio projects (File
> New Project…). A project is a folder for all the scripts and data
of one piece of work; when you open the project, RStudio sets the
working directory to that folder automatically. This means your scripts
will also work on someone else’s computer.
Reading and writing files
Data is often exchanged as comma-separated values (.csv) files. You
can write a data frame out to a file with write.csv(), and
read it back in with read.csv(). Here we use
tempfile() to make a temporary file path, but normally you
would give a file name like "mydata.csv":
cities <- data.frame(city = c("Geneva", "Zurich"),
population = c(203, 421))
path <- tempfile(fileext = ".csv")
write.csv(cities, file = path, row.names = FALSE)
read.csv(path)
See ?read.csv for the many arguments these functions
accept, for example for files that use semicolons or other
separators.
Task
Create and fill in a matrix of “whom you already know” in the class. There are other ways to do this, but for this task I’d like you to do it in R. You can follow my example below (copy to a new script and extend it):
mynetwork <- matrix(0, nrow = 2, ncol = 2) # this creates an empty matrix for 2 people
# Next I'm going to name the matrix rows and columns:
rownames(mynetwork) <- c("James Hollway", "Tommaso Fonti")
colnames(mynetwork) <- c("James Hollway", "Tommaso Fonti")
mynetwork[1, 2] <- 1 # this means I know Tommaso already
mynetwork[2, 1] <- 1 # I think I can say Tommaso knows me already too...
mynetwork["James Hollway", "Tommaso Fonti"] <- 1 # I could also do this by name
# mynetwork[mynetwork > 0] <- 0 # Just in case you make a mistake, this wipes it!
mynetwork
Where next
Packages
Everything in this tutorial (apart from {dplyr}) is part
of “base” R. But much of R’s power comes from packages:
collections of functions, data, and documentation written by others. CRAN, R’s main
repository, hosts many thousands of them, for almost any task you can
imagine.
Packages are installed once, with install.packages(),
and then loaded in each session (typically at the top of each script)
with library():
install.packages("migraph") # only needed once
library(migraph) # needed in every new session
Installing a package also installs the other packages it depends on.
For example, installing {migraph} also installs
{manynet}, {netrics},
{autograph}, {dplyr}, and others, so you
already have a lot at your fingertips!
Once a package is loaded, you can use its functions directly.
Alternatively, you can use a function from an installed package without
loading it by writing the package name and two colons first, as we did
with dplyr::mutate(). This also makes it clear where a
function comes from.
More tutorials
You are now ready to move on to the other stocnet tutorials. To see which tutorials are available, run:
library(migraph)
run_tute()
We recommend continuing with the {manynet} tutorial on
making network data, where you will see how matrices like the one you
just made become networks:
run_tute("manynet1")
Summary
Along the way, you have learned to use these operators and functions:
| Operator or function | What it does |
|---|---|
<- |
assign a value to an object |
==, !=, <,
>, %in% |
compare values, returning TRUE or
FALSE |
class(), as.numeric(),
as.character(), as.factor() |
check and convert types |
NA, is.na(),
na.rm = TRUE |
mark, find, and ignore missing data |
[ ], [[ ]], $,
which() |
index vectors, matrices, data frames, and lists |
c(), matrix(), data.frame(),
list() |
create data structures |
? |
open a function’s help page |
\|> |
pipe a result into the next function |
dplyr::mutate(), dplyr::filter() |
add variables and keep rows of a data frame |
sample(), rnorm(),
set.seed() |
draw random numbers reproducibly |
dput(), sessionInfo() |
share data and versions in a reproducible example |
read.csv(), write.csv() |
read and write data files |
install.packages(), library(),
:: |
install, load, and use packages |