Chapter P2 — R & RStudio

Author

Single Cell RNA-seq Workshop

R1 is the statistical language that powers most of this workshop — Seurat, Bioconductor, DESeq2, and the tidyverse all live here. This chapter is a focused refresher on the R language, the RStudio interface, R’s core data structures, writing and calling functions, and the tidyverse grammar that you will see in every tutorial. The shell/command-line environment is covered in Chapter P1.

TipP2 reading

This is the reading for Chapter P2 — R & RStudio, the second hour of the optional 2-hour Tuesday refresher. It pairs with the P2 lecture slides. If you are new to R, read this before Day 1 so the hands-on tutorials assume a common foundation. Everything here is display-only — open an R session or RStudio and try the examples yourself.

Tip

Keep a cheat sheet open. The Cheat Sheets tab collects one-page Base R, dplyr/tidyverse, ggplot2, and Unix references that pair with this module. You’ll put all of this to work starting in Module 01.

Why R?

R1 is the lingua franca of statistical genomics, and the reason this workshop is built on it: the Seurat and Bioconductor ecosystems live here.

  • Good general scripting tool for statistics and mathematics
  • Powerful, flexible, and free
  • Runs on all computer platforms
  • New enhancements coming out all the time
  • Superb data management and graphics capabilities
  • Reproducibility — keep your scripts to see exactly what was done
  • You can write your own functions
  • Lots of online help available
  • Can use a nice GUI front end such as RStudio
  • Can embed your R analyses in dynamic, polished files using Quarto Markdown

RStudio — the four-pane interface

When RStudio opens you see four panes:

  • Source (top-left) — where you edit scripts (.R) and Quarto documents (.qmd). Run a line or selection with Ctrl/Cmd + Enter.
  • Console (bottom-left) — the live R session. Type here and press Enter to run immediately.
  • Environment / History (top-right) — every object (variable, data frame) you have created this session.
  • Files / Plots / Packages / Help (bottom-right) — file browser, your plots, installed packages, and help pages.

Write code in the Source pane so it is saved and reproducible; use the Console for quick throwaway experiments.

R Scripts and Quarto Markdown

  • Often we want to write scripts that can just be run — R scripts (.R files)
  • We can also embed code in Quarto Markdown files for richer annotations
  • This improves interpretability and reproducibility
  • You can insert R code chunks into Quarto Markdown documents
  • A series of R commands that will be executed, with comments using #
  • Can have pipes (|>) to connect one step to the next

R Basics

Basic Math in R

4 * 4
(4 + 3 * 2^2)
  • Commands can be submitted through the terminal, console, or scripts
  • R follows the normal priority of mathematical evaluation (PEMDAS)
  • Parentheses control order of operations just as in algebra

Assigning Variables

x <- 2
x * 3
y <- x * 3
y - 2
  • Variables are assigned values using the <- operator
  • Variable names must begin with a letter
  • R is case sensitive (x and X are different variables)
  • The result of calculations can be assigned to new variables
  • Names like 3y <- 3 will not work
Note🔑 Key concept — assignment and the R workspace

Every object you create with <- lives in the global environment for the duration of your R session. When you restart R, the workspace is cleared. This is why the top of every workshop tutorial has a library(...) block and file-loading lines — the session always starts fresh, and scripts must be self-contained.

Arithmetic Operations and Functions

# Common math functions
x + 2
x^2
log(x)        # natural log (built-in function)
sqrt(x)
log(x, base = 10)   # arguments modify behavior

# Assigning results
y <- 67
print(y)
x <- 124
z <- (x * y)^2
print(z)
  • log is a built-in function — the object goes in parentheses
  • Functions can take arguments that modify behavior (e.g., log(x, base = 10))
  • The outcome of calculations can be assigned to new variables
  • Use print() to display values explicitly

Getting Help in R

help(mean)           # open help page
?mean                # same as help()
example(mean)        # show examples from the help page
help.search("mean")  # search help pages
apropos("mean")      # find functions with "mean" in name
args(mean)           # show function arguments
  • Help pages exist for all functions explaining parameters and usage
  • The example() function is especially useful for seeing how functions work
  • When in doubt, check the help page!

Data Types and Structures

Strings

x <- "I Love"
print(x)
y <- "Bioengineering"
print(y)
z <- c(x, y)    # c() stands for concatenate
print(z)
  • Characters must be set off by quotation marks
  • The c() function concatenates values into a vector
  • Reusing variable names overwrites previous assignments

Vectors

Note🔑 Key concept — vectorization

R “thinks in vectors”: a vector is an ordered set of values of one type. Mathematical operations and most built-in functions apply element-wise to every value at once — no loop needed. This design choice makes single-cell code expressive: nFeature_RNA > 200 on a 14,000-element vector returns a 14,000-element logical mask in one expression, and you then pass that mask back to subset the data. Wherever you see code that processes thousands of genes or cells in one expression, vectorization is doing the work.

# Create a vector with c()
x <- c(2, 3, 4, 2, 1, 2, 4, 5, 10, 8, 9)
print(x)

# Operations work element-wise
y <- x^2
print(y)

# Plot vectors
plot(x, y)

Types of Vectors

Type Description Example
int Integers 1L, 2L, 3L
dbl Doubles (real numbers) 3.14, 2.718
chr Character strings "hello", "DNA"
lgl Logical (TRUE/FALSE) TRUE, FALSE, NA
fctr Factors (categorical variables) factor(c("a","b","a"))
dttm Date-times Sys.time()
date Dates Sys.Date()

Special values to know:

  • Logical vectors can take: FALSE, TRUE, or NA (not available)
  • Doubles have four special values: NA, NaN (not a number), Inf, -Inf
  • Integer and double vectors are collectively known as numeric vectors
  • In R, numbers are doubles by default
# Character and logical vectors — common in scRNA-seq metadata
genes    <- c("CD3D", "MS4A1", "LYZ", "NKG7")   # character
expressed <- c(TRUE, TRUE, FALSE, TRUE)           # logical

genes[expressed]    # keep only elements where TRUE — logical subsetting

Basic Statistics on Vectors

mean(x)
median(x)
var(x)
sd(x)        # standard deviation
log(x)
sqrt(x)
sum(x)
length(x)
sample(x, replace = TRUE)  # random sample with replacement
  • Many built-in functions operate on entire vectors
  • sample() takes an argument replace = TRUE — arguments modify function behavior
  • Use ?function_name to see the help page for any function

Creating Vectors with Sequences

# Ascending sequence
seq_1 <- seq(0.0, 10.0, by = 0.1)
print(seq_1)

# Descending sequence
seq_2 <- seq(10.0, 0.0, by = -0.1)

# Integer shorthand
1:10            # same as seq(1, 10, by = 1)

# Element-wise operations
seq_square <- (seq_2)^2
print(seq_square)
  • seq() creates sequences with a specified start, end, and step size
  • Operations on sequences work element-wise, just like simple vectors

Sampling from Distributions

# Draw from normal distribution
x <- rnorm(10000, 0, 10)
hist(x)

# Overlay the theoretical density curve
x <- rnorm(1000, 0, 100)
hist(x, xlim = c(-500, 500), probability = TRUE)
curve(dnorm(x, 0, 100), xlim = c(-500, 500), add = TRUE, col = 'red', lwd = 2)

# Draw uniform random integers
y <- sample(1:10000, 10000, replace = TRUE)

# Combine and plot
xy <- cbind(x, y)
plot(x, y)
  • rnorm(n, mean, sd) draws n random values from a normal distribution
  • dnorm() generates the probability density function for overlaying
  • curve() plots a function; add = TRUE overlays on an existing plot
  • cbind() binds columns together into a matrix

Data Frames

Note🔑 Key concept — what a data frame is

A data frame is the workhorse for tabular data in R. It is a rectangular table where each column is a vector (all one type) and each row is an observation. Unlike a matrix, different columns can hold different types — one column of cell barcodes (character), another of UMI counts (numeric), another of cluster labels (factor). In the single-cell world, per-cell metadata (seu@meta.data) is a data frame: one row per cell, columns for nCount_RNA, nFeature_RNA, percent.mt, cluster, sample, and so on. Every filter and summary in the QC tutorial operates on this data frame.

Creating Data Frames

# Build a data frame from individual vectors
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)
)

cells                        # print the whole thing
str(cells)                   # structure: column names, types, first values
summary(cells)               # per-column summary statistics
nrow(cells); ncol(cells)
colnames(cells)
  • data.frame() takes named vectors as columns
  • str() is your first diagnostic — it shows column types and a preview
  • summary() provides min, quartiles, mean, max, and NA counts

The hydrogel_concentration example below shows a data frame with a factor column — a common pattern for experimental design variables:

hydrogel_concentration <- factor(c("low", "high", "high", "high",
                                    "medium", "medium", "medium", "low"))
compression  <- c(3.4, 3.4, 8.4, 3, 5.6, 8.1, 8.3, 4.5)
conductivity <- c(0, 9.2, 3.8, 5, 5.6, 4.1, 7.1, 5.3)

mydata <- data.frame(hydrogel_concentration, compression, conductivity)
row.names(mydata) <- c("Sample_1", "Sample_2", "Sample_3", "Sample_4",
                        "Sample_5", "Sample_6", "Sample_7", "Sample_8")
print(mydata)

Reading and Writing Data

# Comma-separated files
YourFile <- read.table('yourfile.csv', header = TRUE, row.names = 1, sep = ',')
# OR use the convenience function:
YourFile <- read.csv('yourfile.csv')

# Tab-separated files
YourFile <- read.table('yourfile.txt', header = TRUE, row.names = 1, sep = '\t')

# Tidyverse versions (return tibbles — recommended)
YourFile <- read_csv('yourfile.csv')
YourFile <- read_tsv('yourfile.txt')

# Excel files
library(readxl)
YourFile <- read_excel('yourfile.xlsx')
Note⚙️ Key parameter — read_csv() column types

read_csv() (from readr) infers column types from the first 1,000 rows and prints a message showing what it guessed. To override — for example, to force a barcode column to stay as character rather than be coerced to something else — use the col_types argument:

read_csv("counts.csv", col_types = cols(barcode = col_character(),
                                         nCount_RNA = col_double()))

This matters whenever IDs look numeric (e.g., "1", "2", …) but should remain text.

# Write to CSV
write.table(YourFile, "yourfile.csv", quote = FALSE, row.names = TRUE, sep = ",")
# OR:
write.csv(YourFile, "yourfile.csv")

# Write to TSV
write.table(YourFile, "yourfile.txt", quote = FALSE, row.names = TRUE, sep = "\t")

# Tidyverse version
write_csv(YourFile, "yourfile.csv")
  • header = TRUE indicates the first row contains column names
  • row.names = 1 indicates the first column contains row names
  • sep specifies the delimiter (, for CSV, \t for TSV)
  • read_csv() / read_tsv() return tibbles — enhanced data frames with friendlier printing

Indexing Data Frames

# Access by column number
print(mydata[, 2])

# Access by column name (preferred)
print(mydata$compression)

# Access by row number
print(mydata[2, ])

# Access a specific cell
print(mydata[2, 3])

# Subset based on a condition
mydata[mydata$compression > 5, ]

# Subset using a character condition (common for metadata filtering)
cells[cells$sample == "STIM", ]

# Plot two columns against each other
plot(mydata$compression, mydata$conductivity)
  • [row, column] notation accesses specific elements
  • $ notation accesses columns by name (preferred — more readable and tab-completes in RStudio)
  • Leaving a dimension empty selects all (e.g., [, 2] = all rows, column 2)
  • Logical conditions return TRUE/FALSE vectors for subsetting — the same vectorization as above

Other R Data Structures

The earlier sections cover vectors and data frames, which carry most of the workflow. Three other structures appear regularly in scRNA-seq code; you should recognize them on sight.

Lists — the “pocket that holds anything”

A list is the most flexible R container: each slot can hold a different kind of thing — a vector of numbers, a string, another list, a data frame, a function. Seurat objects, SingleCellExperiment objects, and most output of lapply() / purrr::map() are technically lists under the hood.

experiment <- list(
  sample_id  = "donor_01_CTRL",
  age        = 42,
  cell_types = c("B cell", "CD4 T", "Monocyte", "NK"),
  qc_metrics = data.frame(
    metric = c("nCount_RNA", "nFeature_RNA", "percent.mt"),
    median = c(8500, 2300, 4.2)
  )
)

# Access by name with $:
experiment$sample_id
experiment$cell_types
experiment$qc_metrics$median

# Or by [[index]]:
experiment[[3]]              # third element (cell_types)
experiment[["cell_types"]]   # same, by name — clearer
Note

[] vs [[]] on a list. experiment[1] returns a length-1 list (a sub-list); experiment[[1]] returns the content of the first slot. The double-bracket form is what you usually want when accessing list elements in code.

Matrices — like data frames, but one type only

A matrix is a 2-D structure where every cell is the same type — typically all numeric. Faster than a data frame for math, more limited because you cannot mix text and numbers. Most scRNA-seq counts matrices are matrices — specifically sparse matrices of class dgCMatrix from the Matrix package, which is a memory-efficient format that only stores non-zero values.

m <- matrix(1:12, nrow = 3, ncol = 4)
m
#      [,1] [,2] [,3] [,4]
# [1,]    1    4    7   10
# [2,]    2    5    8   11
# [3,]    3    6    9   12

# Indexing — same [row, col] notation as data frames
m[2, 3]     # 8
m[, 2]      # column 2 as a vector
m[1, ]      # row 1 as a vector

# The 10x counts matrix is a (sparse) matrix:
# library(Seurat)
# counts <- Read10X("data/filtered_feature_bc_matrix/")
# class(counts)   # "dgCMatrix"
# dim(counts)     # ~33538 features × ~1222 cells, mostly zero
TipWhy scRNA-seq uses sparse matrices

A typical 10x dataset is ~95% zeros. A dense matrix of 30,000 genes × 14,000 cells in double precision would be ~3.4 GB. The same counts as a dgCMatrix (which stores only non-zero entries plus their coordinates) is ~150 MB. Every Seurat, Scanpy, and Bioconductor workflow uses sparse matrices internally for this reason.

Factors — categorical variables with fixed levels

Note🔑 Key concept — factors and reference levels

A factor is R’s representation of categorical data: a character vector plus a defined, ordered set of allowed values called levels. Without a factor, R treats condition labels as arbitrary text with no notion of a reference. With a factor, the first level is the reference in any statistical model. Getting the level order right is not cosmetic — in DESeq2, a model term ~ condition compares all other levels against the first one. Swap the levels and the sign of every fold-change flips.

# Plain character vector — no reference concept
condition_chars <- c("CTRL", "STIM", "CTRL", "STIM", "STIM")

# As a factor — explicit levels: CTRL is the reference
condition <- factor(condition_chars, levels = c("CTRL", "STIM"))
condition
# [1] CTRL STIM CTRL STIM STIM
# Levels: CTRL STIM

table(condition)
levels(condition)
nlevels(condition)
Note⚙️ Key parameter — factor(levels = )

factor(x, levels = c("CTRL", "STIM")) sets the reference to "CTRL". Omit levels and R sorts alphabetically, which may or may not be what you want. In downstream analysis:

# Change the reference after creation
condition <- relevel(condition, ref = "CTRL")

# Drop levels that are no longer present (after subsetting)
condition <- droplevels(condition)

# Ordered factor for ordinal data
dose <- factor(c("low", "med", "high"), levels = c("low", "med", "high"),
               ordered = TRUE)

In Seurat objects, Idents() is a factor; meta.data$stim is a factor; cluster assignments (seurat_clusters) are factors. Knowing how to manipulate them — relevel() to change the reference, droplevels() to remove unused levels, factor(..., ordered = TRUE) for ordinal data — is fundamental for downstream analysis.

NoteWhere factors matter most downstream

relevel() is used in Tutorial 06 — Pseudobulk DE to set "untreated" as the DESeq2 reference level — getting this wrong flips the sign of every fold-change. Factor levels on Idents() also control which cell type is compared by default in Tutorial 03 marker finding.

Tidy Data Principles

NoteRules of Thumb for Data Organization
  • Store data in nonproprietary formats (plain ASCII text / flat files)
  • Leave an uncorrected original file when doing analyses
  • Use descriptive names for your data files and variables
  • Include a header line with descriptive variable names
  • Maintain effective metadata about the data (data dictionary)
  • When you add observations, add rows
  • When you add variables, add columns
  • A column of data should contain only one data type

Types of Data

Class Subtype Example R type
Categorical Ordinal (ordered) small, medium, large ordered factor
Categorical Nominal (unordered) apples, oranges factor / character
Quantitative Ratio (true zero) kilograms, dollars numeric
Quantitative Interval (no true zero) temperature, calendar year numeric / integer

Factor is the R type for categorical variables with a fixed, known set of possible values.

Base R Visualization

Basic Plotting

seq_1 <- seq(0.0, 10.0, by = 0.1)
plot(seq_1,
     xlab = "space",
     ylab = "function of space",
     type = "p",        # "p" = points, "l" = lines, "b" = both
     col  = "red",
     main = "Basic R Plot")
  • plot() is the main high-level plotting function in base R
  • type options: "p" (points), "l" (lines), "b" (both), "h" (vertical spike lines — not a histogram; use hist() for that)
  • Most workshop graphics use ggplot2 (see below) for publication quality

Multi-Panel Figures

par(mfrow = c(2, 2))   # 2 rows, 2 columns
plot(seq_1,       xlab = "time", ylab = "pop 1",   type = "p", col = 'red')
plot(seq_2,       xlab = "time", ylab = "pop 2",   type = "p", col = 'green')
plot(seq_square,  xlab = "time", ylab = "pop 2 ^2", type = "p", col = 'blue')
plot(seq_1^2,     xlab = "time", ylab = "pop 1 ^2", type = "l", col = 'orange')
  • par(mfrow = c(rows, cols)) sets up a multi-panel layout
  • Subsequent plot() calls fill panels left-to-right, top-to-bottom

Summary Statistics and Figures

# Summary statistics
summary(mydata$compression)

# Histogram
hist(mydata$compression)

# Boxplot by group — formula notation: y ~ x
boxplot(compression ~ hydrogel_concentration, data = mydata,
        col  = "steelblue",
        ylab = "Compression",
        xlab = "Hydrogel Concentration",
        main = "Compression by Hydrogel Concentration")
  • The formula y ~ x means “y grouped by x” — used by boxplot(), lm(), DESeq2, and many others
  • Boxplots show median, IQR, and potential outliers

Functions in Depth

You have already used many R functions (mean(), c(), read.csv(), …). Two practical points are worth knowing.

Named vs. positional arguments

# Positional — values are assigned to arguments in declaration order
round(3.14159, 2)            # 3.14
round(2, 3.14159)            # 2 — argument order matters!

# Named — explicit, order-independent, far more readable
round(x = 3.14159, digits = 2)
round(digits = 2, x = 3.14159)   # same result

# Recommended in real code: positional for the first one or two obvious args,
# named for the rest
seq(from = 1, to = 10, by = 0.5)

# In Seurat / scRNA-seq code, almost everything uses named args
# because the function signatures are long:
# CreateSeuratObject(counts = mat, project = "study1",
#                    min.cells = 3, min.features = 200)

The rule: use named arguments whenever there is any ambiguity. They are self-documenting, survive package version bumps better, and are easier for someone — including future-you — to read.

Defining your own functions

# A function that computes the coefficient of variation of a numeric vector
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 (after dropping NA)

You will write helper functions all the time in scRNA-seq pipelines — typically as 5–15-line wrappers around Seurat/Bioconductor calls. Tutorial 06 — Pseudobulk DE defines a run_de(ct) function in exactly this pattern.

library() vs. install.packages()

This is the single most common point of confusion for new R users:

You install a package once; you load it every session.

  • install.packages("pkg") downloads the package onto your machine. Do it once (from CRAN); for Bioconductor packages use BiocManager::install("pkg").
  • library(pkg) makes that installed package’s functions available in your current R session. Do it every time you start R.
install.packages("tidyverse")   # ONCE — downloads and installs
library(tidyverse)              # EVERY SESSION — makes its functions usable

Restarting R does not require re-installing — but it does require re-library()-ing. That is why every workshop tutorial starts with a library(...) block. You can also call one function without loading the whole package via package::function(), e.g. muscData::Kang18_8vs8().

ImportantPackage install — source of truth

The canonical, up-to-date package install block (CRAN + Bioconductor + GitHub packages for all workshop modules) lives in Tutorial 00 — Get Up and Running. Do not install from memory — copy the block from that page so you get the correct setdiff() guard and the exact package versions the workshop expects.

The tidyverse — dplyr Data Manipulation

Most modern R for biology is written in tidyverse style2 — a coherent family of packages (dplyr, tidyr, ggplot2, readr, and more) that share a consistent, pipe-friendly grammar. Six dplyr verbs cover ~80% of what you will do with data frames:

Verb What it does
filter() Keep rows that match a condition
select() Keep / drop / reorder columns
mutate() Add or modify columns
arrange() Sort rows
summarise() Collapse to one row per group (paired with group_by())
group_by() Set the grouping for downstream summarise / mutate
Note🔑 Key concept — the pipe |>

The pipe |> (built into R since 4.1; the older %>% from magrittr also works) passes the result of one expression as the first argument of the next. This chains dplyr verbs into pipelines that read top-to-bottom — the same direction you think about data transformations:

result <- data |>
  filter(condition) |>
  mutate(new_col = ...) |>
  group_by(group_var) |>
  summarise(n = n())

Read it as: “take data, then filter, then add a column, then group, then summarise.” Without the pipe, you would either nest calls inside each other (hard to read) or create many intermediate variables (clutters the environment). Every tutorial from Module 01 onward uses this style.

library(tidyverse)

# Suppose `meta` is per-cell metadata: barcode, sample, cluster, nCount_RNA, nFeature_RNA
top10_clusters_per_sample <- meta |>
  filter(nFeature_RNA > 200, nFeature_RNA < 2500) |>     # QC filter
  group_by(sample, cluster) |>
  summarise(
    n_cells    = n(),
    median_umi = median(nCount_RNA),
    .groups    = "drop"
  ) |>
  arrange(sample, desc(n_cells)) |>
  group_by(sample) |>
  slice_head(n = 10)

Read top-to-bottom: “take meta, filter by QC thresholds, group by sample × cluster, summarise to one row per group, sort, take top 10 per sample.” This style appears throughout the workshop tutorials.

NoteWhere you will use these dplyr skills
Skill First used in
filter() on per-cell QC metadata Tutorial 01 QC filtering
mutate() to add derived columns Tutorial 01 percent.mt
group_by() + summarise() to aggregate counts per donor × condition Tutorial 06 pseudobulk DE
arrange(), select() Throughout Tutorials 01–08
The |> pipe Every tutorial from Tutorial 01 onward

For the canonical free reference: R for Data Science (2e) — Wickham, Çetinkaya-Rundel & Grolemund.

Visualization with ggplot2

The Base R plot() examples above work but are limited. ggplot2 is the standard for publication-quality figures and is what you will see throughout the workshop. The key insight is the grammar of graphics: a plot is built from layered components.

Component What it is Examples
Data The data frame mpg, seu@meta.data, …
Aesthetics (aes()) Mappings: which column to which visual property aes(x = pc1, y = pc2, color = celltype)
Geoms The visual mark geom_point(), geom_bar(), geom_violin(), geom_tile()
Stats Pre-plot transformations stat_smooth(), stat_summary()
Scales Color, axis, size scales scale_color_brewer(), scale_x_log10()
Facets Sub-plots by group facet_wrap(~sample), facet_grid(condition ~ celltype)
Themes Non-data display theme_minimal(), theme(legend.position = "bottom")

A minimum-viable ggplot:

library(ggplot2)

# A scatter — one point per cell, colored by cell type
ggplot(seu@meta.data, aes(x = nCount_RNA, y = nFeature_RNA, color = celltype)) +
  geom_point(alpha = 0.5, size = 0.6) +
  scale_x_log10() +
  scale_y_log10() +
  labs(title = "QC: counts vs features",
       x     = "Total UMI per cell (log)",
       y     = "Detected genes per cell (log)") +
  theme_minimal()

Build up by adding layers with +:

# Add a smoothed regression line and facet by sample
ggplot(seu@meta.data, aes(x = nCount_RNA, y = nFeature_RNA)) +
  geom_point(alpha = 0.4, size = 0.6, color = "#34495e") +
  geom_smooth(method = "lm", color = "#c0392b") +
  facet_wrap(~ stim) +
  scale_x_log10() +
  scale_y_log10() +
  labs(title = "Counts × features per sample") +
  theme_bw(base_size = 12)
TipFive geoms that cover most scRNA-seq plotting
  • geom_point() — UMAP / PCA scatter, QC scatter
  • geom_violin() + geom_jitter()VlnPlot() in Seurat is built on these
  • geom_tile() — heatmaps, e.g. cell-type × marker-gene dot plots
  • geom_bar(stat = "identity") / geom_col() — proportion bar plots
  • geom_smooth() — adding regression lines to QC scatters
Note

Parallel reading. scNotebooks Module 02 — Introduction to R + ggplot2 covers this same material as a runnable notebook (with R kernel) on Google Colab. Open it alongside this chapter if you want a runnable companion.

Prerequisites and Skills Summary

TipR skills used in later modules — prerequisite check
Skill covered here First used in
Assignment, vectors, basic math Every tutorial — vectors underpin all data manipulation
filter(), mutate() on per-cell metadata Tutorial 01 — QC filtering, adding percent.mt
group_by() + summarise() Tutorial 06 — pseudobulk aggregation per donor × condition
factor() / relevel() for reference levels Tutorial 06 — DESeq2 design; wrong level flips all fold-change signs
read_csv() / write_csv(), file paths Throughout Tutorials 01–08 (.rds hand-off files, result tables)
The |> pipe Every tutorial from Tutorial 01 onward
ggplot2: geom_point(), geom_violin(), facet_wrap() Tutorial 01 QC plots; used in every tutorial through 08
install.packages() once vs library() every session Tutorial 00 §1; required at the top of every tutorial

Additional Resources

The foundational citations for the tools this chapter covers are the R language itself1 and the tidyverse2.

References

1.
R Core Team. R: A Language and Environment for Statistical Computing. (R Foundation for Statistical Computing, Vienna, Austria, 2024).
2.
Wickham, H. et al. Welcome to the tidyverse. Journal of Open Source Software 4, 1686 (2019).
Back to top