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.
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
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
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 type | You 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
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?Project: explore a real dataset with R →
Level 3 (Advanced): statistics in R, data.table performance, and reproducible reports is coming next.
