Before you start
- Be comfortable with the basics first. See R: Level 1 (Basics).
- Any term new? The Glossary has it.
Learning objectives
By the end of this lesson you will be able to: select data precisely; use the apply family; write your own functions; read real data files; and reshape tables with the 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.
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.
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.
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
R prefers you not to write loops. The apply family runs a function across a whole structure at once.
4. Writing your own function
When you repeat a calculation, wrap it in a function. The last line is what the function returns.
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. We host two small practice files for you, a gene-by-sample count table (practice-counts.csv) and a sample sheet (practice-samples.csv). The numbers are made up for practice, but the shape is exactly what a real RNA-seq experiment gives you. R can read them straight from the web:
For a file on your computer, give just its name. That works when the file sits in the folder R is looking in, its working directory:
getwd() # where is R looking right now? counts <- read.csv("counts.csv") # a CSV inside that folder tsv <- read.delim("data.tsv") # tab-separated: read.delim()
Skip setwd(), use an RStudio Project
Older tutorials start with setwd("C:/Users/me/..."). That path only exists on one computer, so the script breaks for anyone else (and for you, once you move the folder). Instead, open your work as an RStudio Project: R then looks in the project folder automatically. First steps in RStudio shows how in two minutes.
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
Two tidyverse moves you will use constantly: reshaping a wide table into the long form that ggplot likes, and joining two tables on a shared column. Order matters here: the count table has no sample column until you reshape it, so you reshape first, then join the sample sheet onto it.
library(tidyverse) counts <- read_csv("https://bionexushub.com/practice-counts.csv") samples <- read_csv("https://bionexushub.com/practice-samples.csv") # 1. wide (one column per sample) into long (one row per gene per sample) long <- pivot_longer(counts, cols = -gene, names_to = "sample", values_to = "count") # 2. attach each sample's condition and batch, matched on "sample" joined <- left_join(long, samples, by = "sample") head(joined)
Try it in your browser (loads dplyr and tidyr the first time, so allow up to a minute):
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 and joined, a small plot per group is one line away with facet_wrap, and you can colour by the sample information you just joined.
ggplot(joined, aes(sample, count, fill = condition)) + geom_col() + facet_wrap(~ gene, scales = "free_y") + # one mini-plot per gene, each with its own y-axis theme_minimal()
Try it: which genes go up in the treated samples? Then delete , scales = "free_y" and Run again to see why it is there.
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 the scratchpad below or 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. New idea: if (condition) { A } else { B } runs A when the condition is TRUE and B otherwise, and length(x) tells you how many values x holds.
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
Run each one in the scratchpad above (or in RStudio) and compare.
| You type | You should see |
|---|---|
| sapply(c("ATG","GG","ATATAT"), nchar) | 3 2 6 (each labelled with its word) |
| apply(m, 1, sum) | 12 9 (m from section 3) |
| mean_or_zero(c(2,4,6)) (after writing it in Exercise 3) | 4 |
Check your understanding
left_join() do?group_by splits rows into groups (one per chromosome) and summarise collapses each group to one value, here the mean.expr[expr > 10] return?expr > 10 is a TRUE/FALSE vector; indexing with it keeps the elements marked TRUE.apply(m, 2, mean) compute?pivot_longer do?