R: Level 2 (Intermediate)

The next step up in R: selecting data precisely, the apply family, writing your own functions, reading real data files, and reshaping tables for analysis, all taught gently and at your own pace.

🟡 Intermediate ⏱️ ~2 hr 📊 R · tidyverse

You are ready for this if...

You have finished R Basics and you are comfortable with vectors, a data frame, and a first ggplot. If that still feels new, there is no rush: go back, play, and come here whenever you like. Nothing here is a race.

No memorizing needed. Open RStudio, type each block into a script, and run it line by line (Ctrl/Cmd+Enter). Change a value, run again. That is the whole method, and you can use this page as a reference any time.

1. Picking out exactly the data you want

In R Basics you pulled a column with $. Two more ways to select are everywhere in real analysis: by a name and by a condition.

expr <- c(TP53 = 12.4, BRCA1 = 8.1, EGFR = 15.7, MYC = 3.3)

expr["EGFR"]        # by name
expr[expr > 10]       # by condition: keep values above 10
names(expr[expr > 10])   # which genes are those?

This is how filtering works under the hood

expr > 10 makes a TRUE/FALSE vector, and R keeps the positions marked TRUE. The tidyverse filter() you met is a friendlier wrapper around exactly this idea.

2. Matrices and lists: the other building blocks

A matrix is a grid of one type (think an expression matrix). A list can hold a mix of things (a number here, a vector there), which is how most R results are returned.

m <- matrix(1:6, nrow = 2, byrow = TRUE)
t(m)                 # transpose: swap rows and columns

res <- list(gene = "TP53", scores = c(2.1, 3.4))
res$gene             # pull a named piece
res[["scores"]][2]    # second score: 3.4

Vector vs list, in one line

A vector holds many values of the same type; a list holds any mix of objects. A data frame is really just a list of equal-length vectors, which is why $ works on both.

3. Do something to every row, column, or item

2 4 6 1 3 5 apply(m, 1, sum) 12 9 one sumper row
The apply family runs a function (here sum) across each row or column at once, with no loop to write.

R prefers you not to write loops. The apply family runs a function across a whole structure at once.

m <- matrix(c(2,4,6, 1,3,5), nrow = 2, byrow = TRUE)

apply(m, 1, sum)        # 1 = each row
apply(m, 2, mean)       # 2 = each column

sapply(c(4, 9, 16), sqrt)  # apply to each item, get a vector back

4. Writing your own function

When you repeat a calculation, wrap it in a function. The last line is what the function returns.

zscore <- function(x) {
  (x - mean(x)) / sd(x)        # standardise a set of numbers
}

zscore(c(2, 4, 6, 8))

Why bother

One tested function beats copy-pasting the same formula ten times. It is also the first step toward sharing code and building your own small toolkit.

5. Reading your own data files

Most data arrives as a CSV or a tab-separated table. Tell R where to look, then read it in one line.

getwd()                       # where am I right now?
setwd("~/my_project")          # move to your project folder

counts <- read.csv("counts.csv")
tsv    <- read.table("data.tsv", header = TRUE, sep = "\t")
head(counts)

Excel files too

For .xlsx files, install the readxl package once and use readxl::read_excel("file.xlsx"). Same idea, one extra package.

6. tidyverse, going deeper: joins and reshaping

counts id | count s1 | 40 samples id | tissue join on id joined id | count | tissue s1 | 40 | liver
left_join lines up two tables on a shared column (here id) and glues their columns together.

Two tidyverse moves you will use constantly: joining two tables on a shared column, and reshaping a wide table into the long form that ggplot likes.

library(tidyverse)

# attach sample information onto a counts table by a shared id
joined <- left_join(counts, samples, by = "sample_id")

# wide (one column per sample) into long (one row per value)
long <- pivot_longer(counts, cols = -gene,
                     names_to = "sample", values_to = "count")

Why "long" data matters

ggplot wants one row per observation. Reshaping a wide expression matrix into long form with pivot_longer is the step that unlocks faceted, grouped plots.

7. Cleaner plots: facets and themes optional on first pass

Once your data is long, a small plot per group is one line away with facet_wrap.

ggplot(long, aes(sample, count)) +
  geom_col() +
  facet_wrap(~ gene) +          # one mini-plot per gene
  theme_minimal()

For a deeper, opinionated take on making honest, readable figures, see Plots that tell the truth.

8. Bioconductor: where the biology packages live optional on first pass

Everyday packages come from CRAN (install.packages). Most bioinformatics packages live in a separate home called Bioconductor, installed through one helper.

install.packages("BiocManager")      # the gateway, installed once
BiocManager::install("DESeq2")        # then any Bioconductor package

Good to know

DESeq2, edgeR, limma, and most RNA-seq and genomics tools are on Bioconductor, not CRAN. You will meet DESeq2 properly in the RNA-seq differential expression lesson.

Practice exercises

Type each into RStudio and run it. Peek at the solution only after you have tried.

Exercise 1: filter by condition. Using the named vector expr from section 1, keep only the genes with expression above 10.

Show a worked solution
expr[expr > 10]
# or, to get just the gene names:
names(expr[expr > 10])

Exercise 2: apply to each item. Use sapply to get the number of characters in each of c("ATG", "GG", "ATATAT").

Show a worked solution
sapply(c("ATG", "GG", "ATATAT"), nchar)
# 3 2 6

Exercise 3: a safe function. Write mean_or_zero(x) that returns the mean of x, or 0 when x is empty.

Show a worked solution
mean_or_zero <- function(x) {
  if (length(x) == 0) {
    0
  } else {
    mean(x)
  }
}

mean_or_zero(c(2, 4, 6))   # 4
mean_or_zero(c())             # 0

Check yourself

R and the terminal do not run in this page, so here is what correct output looks like. Type each one and compare.

You typeYou should see
sapply(c("ATG","GG","ATATAT"), nchar)3 2 6
apply(m, 1, sum)12 9 (m from section 3)
mean_or_zero(c(2,4,6))4

Check your understanding

What does left_join() do?
Right. A join lines up rows by a shared id (for example sample_id) and glues the columns together.
Where do most bioinformatics R packages, such as DESeq2, come from?
Correct. Everyday packages come from CRAN, but DESeq2, edgeR, limma and most genomics tools live on Bioconductor.
How does a list differ from a vector in R?
Right. A vector is all-one-type; a list is a flexible container, which is why a data frame is really a list of equal-length vectors.
You want the average expression per chromosome from a data frame. Which tidyverse pattern does it?
Right. group_by splits rows into groups (one per chromosome) and summarise collapses each group to one value, here the mean.
What does expr[expr > 10] return?
expr > 10 is a TRUE/FALSE vector; indexing with it keeps the elements marked TRUE.
What does apply(m, 2, mean) compute?
The second argument picks the margin: 1 means rows, 2 means columns. So this is the mean of each column.
What does pivot_longer do?
It turns many value-columns into two columns (a name and a value), giving the one-row-per-observation shape ggplot expects.
Put these skills to work on a real dataset

Project: explore a real dataset with R →

Level 3 (Advanced): statistics in R, data.table performance, and reproducible reports is coming next.