P2 — R & RStudio

Bill Cresko and Shannon Snyder

Where this fits

  • Day 0 (optional Tuesday refresher) — Module 2 of 2, follows P1 (Computer Systems)
  • P1 covered the shell and filesystem — this session is entirely R
  • Goal: just enough R to be productive on Day 1, not a full course
  • Pairs with the reading: P2 — R & RStudio

What you’ll walk away with

  • Work in RStudio (or VS Code + R extension)
  • Assign variables, work with vectors, data frames, lists, and factors
  • Write simple functions
  • Understand install.packages() vs library()
  • Chain operations with the tidyverse pipe |>
  • Read and write tabular data with read_csv / write_csv
  • See how these tools appear in Modules 00–08

Why R?

Why R for single-cell analysis?

  • Free, open-source, cross-platform — runs the same on Mac, Linux, and Windows
  • The Bioconductor ecosystem — 2,000+ packages for genomics (DESeq2, scran, scater, …); Seurat itself is on CRAN
  • Superb data wrangling, statistics, and publication-quality graphics with ggplot2
  • Reproducibility — your scripts are a complete record of every step
  • Quarto turns those scripts into the rendered tutorials you’ll work through

Tip

Everything in this workshop — QC, clustering, DE, enrichment — is R code you can read, adapt, and re-run.

Your two IDE options

The comfortable default for R work:

  • Source (top-left) — scripts and Quarto docs you edit and save
  • Console (bottom-left) — where R commands actually run
  • Environment / History (top-right) — every object you’ve created
  • Files / Plots / Packages / Help (bottom-right) — everything else

Write code in the Source pane so it’s saved; use the Console for quick experiments.

A general coding alternative, especially good for SSH (remote) work:

  • Install the R extension (search “REditorSupport” in the Extensions panel)
  • Open an R terminal: Terminal → New Terminal, then type R
  • Ctrl/Cmd + Enter runs the current line or selection into the terminal
  • Used in Module 09 when connecting to Talapas over SSH

Both IDEs run the same R — pick the one you prefer for local work.

R Basics

Assignment & arithmetic

x <- 2               # assign with <-; names start with a letter
x * 3                # 6
y <- x * 3           # store a result

(4 + 3 * 2^2)        # R obeys PEMDAS; parentheses control order
log(16)              # natural log — built-in function; argument in ()
sqrt(81)             # 9
log(1000, base = 10) # named argument — clearer and order-independent
  • Assign with <-; R is case-sensitive (xX)
  • Normal math precedence; parentheses clarify
  • Comment with #

Vectors — R’s fundamental unit

counts <- c(8, 3, 0, 12, 5, 1, 0, 9)   # c() = concatenate
counts * 2           # every element doubled — no loop needed
counts + counts      # element-wise addition

sum(counts); mean(counts); median(counts)
sd(counts); max(counts); min(counts)
length(counts)       # how many elements

seq(0, 10, by = 2)   # 0 2 4 6 8 10
1:10                 # shorthand integer sequence
  • A vector holds values of a single type: dbl, int, chr, lgl, or fctr
  • Math applies element-wise — this is how R processes thousands of cells at once
  • NA marks a missing value; most summary functions accept na.rm = TRUE

Vectors of other types

genes    <- c("CD3D", "MS4A1", "LYZ", "NKG7")   # character vector
expressed <- c(TRUE, TRUE, FALSE, TRUE)           # logical vector

genes[expressed]          # keep where TRUE — logical subsetting
genes[c(1, 3)]            # keep elements 1 and 3
  • Logical subsetting — make a TRUE/FALSE mask, then use it to filter
  • counts > 4 returns a logical vector; counts[counts > 4] keeps the hits
  • This “mask then subset” pattern is everywhere in QC filtering

Data Structures

Data frames — tabular data

cells <- data.frame(
  barcode    = c("AAAC", "AAAG", "AAAT", "AACA"),
  sample     = c("CTRL", "CTRL", "STIM", "STIM"),
  nCount_RNA = c(8500, 12000, 6400, 9100),
  nFeature   = c(2300, 3100, 1900, 2500)
)

str(cells)                           # structure: columns, types, values
summary(cells)                       # per-column summary stats
nrow(cells); ncol(cells)             # dimensions
colnames(cells)                      # column names
  • The workhorse for tabular data — columns are vectors, can be different types
  • Seurat stores per-cell QC metrics in a data frame (obj@meta.data)
  • str() and summary() are your first-look tools; nrow() / ncol() give dimensions

Indexing & subsetting

cells[1, ]                            # row 1, all columns
cells[, "nCount_RNA"]                 # column by name
cells$nCount_RNA                      # same column — $ is the usual way
cells[cells$sample == "STIM", ]       # rows where sample is STIM

mean(cells$nCount_RNA)                # 9000
  • [row, column] — leave either blank to mean “all”
  • $name is the shorthand for a single column — you’ll use this constantly
  • Condition in the row position filters rows — this is how QC cutoffs work

Factors — categorical variables with levels

condition <- factor(c("CTRL", "STIM", "CTRL", "STIM"),
                    levels = c("CTRL", "STIM"))
condition
levels(condition)    # "CTRL" "STIM" — CTRL is the reference
table(condition)     # counts per level
  • A factor encodes categories with a fixed, ordered set of levels
  • The first level is the reference in any statistical model
  • levels = c("CTRL", "STIM") → contrast is “STIM vs CTRL”

Lists — the pocket that holds anything

experiment <- list(
  sample_id  = "donor_01_CTRL",
  age        = 42,
  cell_types = c("B cell", "CD4 T", "Monocyte", "NK")
)

experiment$sample_id        # access a named slot with $
experiment[["cell_types"]]  # [[ ]] returns the slot's contents
experiment[3]               # [ ] returns a length-1 sub-list
  • Each slot can hold a different kind of object — even another list
  • Seurat and SingleCellExperiment objects are lists under the hood
  • [[ ]] extracts contents; [ ] returns a sub-list

Functions & Packages

Writing simple functions

# coefficient of variation — a small helper function
cv <- function(x, na.rm = TRUE) {
  if (na.rm) x <- x[!is.na(x)]
  sd(x) / mean(x)
}

cv(c(1, 2, 3, 4, 5))         # 0.527
cv(c(1, 2, NA, 4, 5))        # 0.609 (NA dropped via na.rm)

# named vs positional arguments
round(3.14159, 2)             # positional — order matters
round(x = 3.14159, digits = 2) # named — clearer, order-independent
  • Structure: name <- function(args) { body }; last expression is returned
  • Use named arguments — scRNA-seq functions have long signatures
  • You’ll write small wrappers around Seurat/Bioconductor calls routinely

Packages: install.packages() vs library()

install.packages("tidyverse")   # download & install — ONCE per machine
library(tidyverse)              # load — EVERY new R session
  • install.packages() = get it onto your computer (do once)
  • library() = make it available this session (do every time)
  • Bioconductor packages use BiocManager::install() instead of install.packages()
  • Call one function without loading a package: muscData::Kang18_8vs8()

Note

Workshop package installation is fully covered in Module 00 — Setup. That’s the canonical install block for everything through Module 08. Today’s goal is just to understand the install vs library distinction.

The Tidyverse

The tidyverse & the pipe |>

library(tidyverse)

cells |>
  filter(nCount_RNA > 7000) |>                      # keep cells with > 7000 UMIs
  mutate(genes_per_umi = nFeature / nCount_RNA) |>  # add a derived column
  group_by(sample) |>                               # group by condition
  summarise(
    n_cells    = n(),
    median_umi = median(nCount_RNA),
    .groups    = "drop"
  ) |>
  arrange(desc(median_umi))                         # sort highest first
  • The pipe |> feeds the left side into the first argument of the right side
  • Read top-to-bottom: filter → add column → group → summarise → sort
  • No intermediate objects needed — concise and readable

The six dplyr verbs

Verb What it does Workshop use
filter() Keep rows matching a condition QC cutoffs (Module 01)
select() Keep / drop columns Tidy up result tables
mutate() Add or modify columns percent.mt, genes_per_umi (Module 01)
arrange() Sort rows Top DE genes by p-value
group_by() Define groups for summaries Pseudobulk aggregation (Module 06)
summarise() Reduce groups to one row Counts per donor × condition (Module 06)

These six verbs cover the majority of data-frame work in every tutorial.

A real-workshop pipeline

# Module 06 flavour: per-donor QC summary before pseudobulk DE
meta |>
  filter(nFeature_RNA > 200, nFeature_RNA < 2500,
         percent.mt < 20) |>
  group_by(donor, condition) |>
  summarise(
    n_cells      = n(),
    median_umi   = median(nCount_RNA),
    median_genes = median(nFeature_RNA),
    .groups      = "drop"
  ) |>
  arrange(condition, desc(n_cells))
  • meta is the per-cell metadata data frame from a Seurat object
  • This exact pattern appears in the QC and pseudobulk DE modules
  • The |> pipe is used in every tutorial from Module 01 onward

Plotting with ggplot2

ggplot2 — the grammar of graphics

Almost every figure in this workshop is a ggplot. You build a plot in layers: data → aesthetic mapping (aes) → geoms → scales/themes.

library(ggplot2)

ggplot(meta, aes(x = nCount_RNA, y = nFeature_RNA, colour = condition)) +
  geom_point(size = 0.4, alpha = 0.5) +
  scale_x_log10() +
  labs(x = "UMIs per cell", y = "Genes per cell") +
  theme_minimal()
  • aes() maps columns to visual properties; geom_*() adds a layer (points, violins, bars)
  • + chains layers — the same additive idea as the |> pipe, but for graphics
  • Seurat’s DimPlot/FeaturePlot/VlnPlot all return ggplot objects you can extend with +
  • The P2 chapter has the full ggplot2 walkthrough (scales, facets, themes)

Reading & Writing Data

Reading & writing tabular data

# write our data frame, then read it back
write_csv(cells, "cells_demo.csv")          # tidyverse — no row names
back <- read_csv("cells_demo.csv")          # returns a tibble
back

# base R equivalents
write.csv(cells, "cells_demo.csv", row.names = FALSE)
back2 <- read.csv("cells_demo.csv")

# other separators and formats
df_tsv <- read_tsv("data.txt")              # tab-separated
# library(readxl); xl <- read_excel("file.xlsx")
  • read_csv() / write_csv() (tidyverse) return tibbles and are faster and friendlier
  • Store data in plain text (.csv, .tsv) — it outlasts any software version
  • Keep an untouched original; always write results to a new file
  • One column = one data type

Putting It Together

How R skills map to Modules 00–08

Skill First used in
filter() on per-cell QC metadata Module 01 — QC filtering
mutate() to add derived columns Module 01 — percent.mt
factor(), relevel() for reference levels Module 06 — DESeq2 design
group_by() + summarise() Module 06 — pseudobulk aggregation
arrange() on DE results Modules 03, 06, 07 — sorting by p-value / LFC
read_csv() / write_csv() Throughout Modules 01–08
The |> pipe Every module from 01 onward
Lists / [[ ]] indexing Seurat object internals — Modules 01–08

Recap — what you now have

  • Assignment & arithmetic<-, math, log(), sqrt()
  • Vectorsc(), element-wise ops, logical subsetting, seq()
  • Data frames — build, inspect with str() / summary(), index with [row, col] and $
  • Factors — categorical levels; first level = reference in models (Module 06)
  • Lists — flexible containers; $ and [[ ]] access
  • Functionsfunction(args) { body }; named arguments
  • Packagesinstall.packages() once; library() every session; Module 00 installs everything
  • Tidyverse pipefilter, mutate, group_by, summarise, arrange
  • I/Oread_csv(), write_csv()

What’s next

  • Module 00 — Setup: install every workshop package (the canonical install block lives there); confirm RStudio and Quarto render correctly
  • Module 01 — QC & Preprocessing: filter() and mutate() on live single-cell data, immediately
  • Module 02 — Dimension Reduction & Clustering: data frames of UMAP coordinates
  • Keep a cheat sheet open — fluency comes from doing. See you tomorrow!