T cell receptor (TCR) profiling and Cellular Indexing of Transcriptomes and Epitopes by Sequencing (CITE-Seq) are pivotal techniques in single-cell research, offering unparalleled insights into the adaptive immune system and cellular heterogeneity. TCR profiling enables a deep dive into the repertoire and diversity of T cell populations, highlighting the specificity and uniqueness of T cell responses. On the other hand, CITE-Seq facilitates the concurrent assessment of transcriptomic data and protein expression within individual cells, creating a comprehensive portrayal of cellular states. In this module, participants will explore the profound implications of TCR profiling in understanding immune responses and the synergies it can achieve when coupled with CITE-Seq. We'll initiate with core concepts and theories, and swiftly transition into practical applications using advanced computational tools. Through this hands-on approach, attendees will master the nuances of TCR profiling and CITE-Seq, equipping them with valuable tools for their immunological and single-cell research pursuits.
## Function to run shell commands in Google Colab with R kernel
shell_call <- function(command, ...) {
result <- system(command, intern = TRUE, ...) # Runs the shell command and captures output
cat(paste0(result, collapse = "\n")) # Prints the output to the console
}
## Function to load multiple R packages
loadPackages = function(pkgs){
myrequire = function(...){
suppressWarnings(suppressMessages(suppressPackageStartupMessages(require(...))))
}
ok = sapply(pkgs, require, character.only=TRUE, quietly=TRUE) # Checks if each package is installed
if (!all(ok)){
message("There are missing packages: ", paste(pkgs[!ok], collapse=", ")) # Prints missing packages
}
}
## Download the script to add R2U (fast package installation system)
download.file("https://github.com/eddelbuettel/r2u/raw/master/inst/scripts/add_cranapt_jammy.sh",
"add_cranapt_jammy.sh")
Sys.chmod("add_cranapt_jammy.sh", "0755") # Change permissions to allow execution
## Run the script to set up R2U
shell_call("./add_cranapt_jammy.sh")
## Enable BSPM package manager for installing system-wide packages
bspm::enable()
options(bspm.version.check=FALSE)
## Remove the setup script to keep the workspace clean
shell_call("rm add_cranapt_jammy.sh")
## Define the list of packages to install
cranPkgs2Install = c("dplyr", "ggpubr", "Seurat", "cowplot",
"Rtsne", "hdf5r", "patchwork")
## Install all packages without prompting the user
install.packages(cranPkgs2Install, ask=FALSE, update=TRUE, quietly=TRUE)
## To simplify package loading, we created the loadPackages() function.
## But, if you don't have the function, you should use 'library(name_of_package)'
pkgs = c("Seurat", "dplyr", "patchwork") # List of packages to load
loadPackages(pkgs) # Load packages using previously defined function
## Define the directory where data will be stored and accessed
## IMPORTANT: The user must change "scw01" to match their actual working directory
mydir <- "/content"
In this notebook, we will delve into the analysis of multi-modal single-cell data. Our focus will be on a non-small cell lung cancer (NSCLC) sample that was processed using the 10X 5' immune profiling technology. This advanced technology captures both RNA and T cell receptor sequences for each individual cell, providing a comprehensive view of the cellular landscape.
You can download the dataset from the 10x Genomics website here.
The single-cell TCR sequencing approach enables high-resolution analysis of T-cell receptor diversity, identifying specific clonotypes and their roles in the immune system. Unlike bulk sequencing, which combines information from multiple cells, the single-cell method preserves the individual identity of each T cell, allowing for the correlation between transcriptomic profile and TCR repertoire.
In this notebook, we will briefly go through the key steps of single-cell analysis. Since this module focuses on TCR sequence analysis, we will provide an overview of the main processes. For more detailed explanations of each step, please refer to the other available modules.
# Download a filtered gene-barcode matrix from 10X Genomics
# This command uses curl to download a file from the internet. (-O) Saves the file with the same name it has on the server.
shell_call("curl -O https://cf.10xgenomics.com/samples/cell-vdj/5.0.0/vdj_v1_hs_nsclc_multi_5gex_t_b/vdj_v1_hs_nsclc_multi_5gex_t_b_count_filtered_feature_bc_matrix.tar.gz")
## Extract the downloaded file (decompress the dataset)
# tar -xf instructs tar to extract (-x) the specified file (-f) and decompress it.
shell_call("tar -xf /content/vdj_v1_hs_nsclc_multi_5gex_t_b_count_filtered_feature_bc_matrix.tar.gz")
# Read the filtered feature-barcode matrix into a matrix object
# This command reads the filtered feature-barcode matrix from a 10X Genomics dataset located in the specified
# directory and assigns the resulting data to the variable counts.
counts <- Read10X(paste0(mydir, "/filtered_feature_bc_matrix/"))
Next, we will use the count matrix and metadata to create a Seurat object. The Seurat object acts as a comprehensive container that holds not only the raw count data and metadata but also any downstream analysis results, such as PCA (Principal Component Analysis) and clustering outcomes. This centralized container allows for streamlined data manipulation and analysis, providing an organized structure for our scRNA-Seq dataset.
By creating a Seurat object, we can efficiently manage and analyze our data, making it easier to perform complex operations and visualize the results.
# Create a Seurat object using the raw count matrix
seurat.raw <- CreateSeuratObject(counts = counts)
# Show the contents of Seurat object
seurat.raw
The first step in our analysis is to familiarize ourselves with the dataset now stored in the Seurat object. Seurat provides powerful tools that allow us to explore and visualize our count data alongside the associated metadata. This enables us to gain a comprehensive understanding of our scRNA-Seq data.
By leveraging Seurat's capabilities, we can perform various exploratory analyses, such as:
By conducting these initial explorations, we set the stage for more advanced analyses, such as dimensionality reduction, clustering, and differential expression analysis. This foundational step is crucial for understanding the nuances of our dataset and making informed decisions throughout our analysis pipeline.
# How many cells and genes do we currently have?
print(paste0("The number of genes is ", dim(seurat.raw)[1], " and the number of cells is ", dim(seurat.raw)[2]))
# The print() function displays the result in the console.
# The dim() function returns the dimensions of an object, usually a matrix or data frame.
# In the case of Seurat objects, the gene expression count matrix has genes as rows and cells as columns.
# The paste0() function concatenates (joins) strings without adding any spaces between them.
# The sentences inside the parentheses (") will also be printed
# View a subset (a slice) of the count matrix
# Remember: rows are genes, columns are cells/barcodes)
GetAssayData(seurat.raw, slot = "counts")[8:10,13:14]
NOTE: the dots('.') reflect a zero value. The count table is stored in sparse matrix format which explicitly stores only non zero values to save space.
## Display metadata columns available in the Seurat object
# What metadata columns are available in the Seurat object?
print(colnames(seurat.raw@meta.data))
# (@) Accesses the meta.data slot of the seurat.raw object.
# The meta.data slot typically contains metadata associated with cells, such as cell type, sample ID, or other annotations.
# The colnames() function retrieves the column names of the metadata data frame stored in the meta.data slot
# Create a violin plot showing the distribution of number of UMIs per cell
options(repr.plot.width=7, repr.plot.height=7) # This command sets the width and height of the plot output.
VlnPlot(seurat.raw, features = c("nCount_RNA"),y.max=2e4)
# creates a violin plot
# seurat.raw: The Seurat object containing your single-cell data.
# features = c("nCount_RNA"): It is plotting the number of RNA molecules (nCount_RNA) for each cell.
# y.max=2e4: Sets the maximum value for the y-axis to 20,000 (2e4 is scientific notation for 20,000)
We have already observed that two of the primary quality control (QC) metrics — number of UMIs (Unique Molecular Identifiers) and number of genes detected per cell — were automatically computed by Seurat. The next important QC metric to consider is the percentage of mitochondrial genes. Mitochondrial genes can indicate cell stress or apoptosis, so it's crucial to monitor their expression levels.
To calculate the percentage of mitochondrial genes, we will use Seurat's PercentageFeatureSet method. This method calculates the percentage of UMIs originating from genes that match a specified pattern. For mitochondrial genes, we typically look for genes starting with "MT-" (common prefix for mitochondrial genes).
# Human mitochondrial gene names start with "MT-" so we'll calculate the percentage of genes matching the pattern "^MT-"
# This function calculates the percentage of counts for features (genes) that match a given pattern.
# Also creates a new metadata column in the seurat.raw object named percent.mt and assigns the calculated percentages to it. (seurat.raw[["percent.mt"]])
seurat.raw[["percent.mt"]] <- PercentageFeatureSet(seurat.raw, pattern = "^MT-")
# Now we can see that the % mitochondrial gene expression has been calculated for each cell
head(seurat.raw$percent.mt) # Display the first few rows of the calculated mitochondrial percentages
## Violin plot for three quality metrics: UMI count, gene count, mitochondrial gene percentage
options(repr.plot.width=12, repr.plot.height=6)
VlnPlot(seurat.raw, features = c("nCount_RNA", "nFeature_RNA", "percent.mt")) # We can visualize all three of the cell quality metrics together using Seurat's VlnPlot method
It is often helpful to visualize these QC metrics in conjunction, as cells that are outliers in multiple dimensions, are more likely to be low quality cells. Seurat's FeatureScatter creates a scatter plot of two given columns from our metadata.
## Scatter plot: total UMI counts vs percentage of mitochondrial genes
options(repr.plot.width=6, repr.plot.height=6)
FeatureScatter(seurat.raw, feature1 = "nCount_RNA", feature2 = "percent.mt") # Here we visualize the number of UMI vs the percentage of mito genes
# Once we've visualized the metrics we can select the thresholds that we want to use to filter.
# We use R's subset method to filter
seurat.raw <- subset(
seurat.raw,
subset = ## Apply filtering criteria:
nFeature_RNA > 200 & # Remove cells with fewer than 200 genes
nCount_RNA > 400 & # Remove cells with fewer than 400 UMIs
nFeature_RNA < 6000 & # Remove cells with more than 6000 genes (potential doublets)
percent.mt < 40) # Remove cells with more than 40% mitochondrial RNA (low-quality cells)
After removing unwanted cells from the dataset, the next step is to normalize the data. Normalization is essential to adjust for differences in sequencing depth across cells, ensuring meaningful comparisons.
1.Normalization Method: LogNormalize
2.Normalization by Total Expression:
3.Multiplication by a Scaling Factor:
4.Logarithmic Transformation:
5.Data Storage:
# Normalize data using LogNormalization
seurat.raw <- NormalizeData(seurat.raw, normalization.method = "LogNormalize", scale.factor = 10000)
# Identify the most variable genes
seurat.raw <- FindVariableFeatures(seurat.raw, selection.method = "vst", nfeatures = 2000)
# Scale the data
seurat.raw <- ScaleData(seurat.raw, features = VariableFeatures(seurat.raw), do.scale = T, do.center = T)
# Run PCA (Principal Component Analysis)
seurat.raw <- RunPCA(seurat.raw, features = VariableFeatures(seurat.raw))
# Find cell neighbors
seurat.raw <- FindNeighbors(seurat.raw, dims = 1:20, k.param = 20)
# Identify clusters at low resolution
seurat.raw <- RunUMAP(seurat.raw, dims = 1:20, reduction = "pca", seed.use = 1)
# Find clusters and use a low resolution (0.1 is a good start) so that we can easily identify all of the T cells later
seurat.raw <- FindClusters(seurat.raw, resolution = 0.1)
## Visualize UMAP with cluster labels
DimPlot(seurat.raw, reduction = "umap", label = T,group.by = "seurat_clusters")
Look at the feature plot of common cell type markers to figure out which cluster is T cells
# Feature plot of specific marker genes to identify cell types
FeaturePlot(seurat.raw, features = c("CD3D", "CD8A", "GNLY", "CD79A", "FCGR3A","KRT7"), min.cutoff = "q1")
In this section we will read in files generated by the 10X CellRanger software which describe the TCR sequences found in individual cells.
#Download files with curl
shell_call("curl -O https://cf.10xgenomics.com/samples/cell-vdj/5.0.0/vdj_v1_hs_nsclc_multi_5gex_t_b/vdj_v1_hs_nsclc_multi_5gex_t_b_vdj_t_all_contig_annotations.csv")
shell_call("curl -O https://cf.10xgenomics.com/samples/cell-vdj/5.0.0/vdj_v1_hs_nsclc_multi_5gex_t_b/vdj_v1_hs_nsclc_multi_5gex_t_b_vdj_t_clonotypes.csv")
# Read in TCR information for each cell
tcr <- read.csv(paste0(mydir,"/vdj_v1_hs_nsclc_multi_5gex_t_b_vdj_t_all_contig_annotations.csv"))
# Read in clonotype info (remember many cells can share the same clonotype, or TCR sequence)
clono <- read.csv(paste0(mydir,"/vdj_v1_hs_nsclc_multi_5gex_t_b_vdj_t_clonotypes.csv"))
# Remove the -1 at the end of each barcode.
# Subsets so only the first line of each cell barcode is kept, so that we ony analyze one clonotype for each cell.
tcr$barcode <- gsub("-1", "", tcr$barcode)
tcr <- tcr[!duplicated(tcr$barcode), ]
# Also remove the -1 at the end of the line of the Seurat object
seurat.raw = RenameCells(seurat.raw,new.names = gsub("-1", "", colnames(seurat.raw)))
# Only keep the barcode and clonotype columns. Adjust the name from "raw_clonotype_id" to "clonotype_id" so
tcr <- tcr[,c("barcode", "raw_clonotype_id")]
names(tcr)[names(tcr) == "raw_clonotype_id"] <- "clonotype_id"
# Merge the TCR and clonotype tables so we have TCR amino acid sequence for each cell
tcr <- merge(tcr, clono[, c("clonotype_id", "cdr3s_aa")])
# Reorder so barcodes are first column, set them as rownames, and remove the unnecessary extra column of barcodes
tcr <- tcr[, c(2,1,3)]
rownames(tcr) <- tcr[,1]
tcr[,1] <- NULL
# Add to the Seurat object's metadata.
seurat.raw <- AddMetaData(object=seurat.raw, metadata=tcr)
# Confirm we have TCR information in the metadata
head(seurat.raw@meta.data)
# Generate a histogram of clone frequencies from clono table
# What is a good threshold to distinguish expanded from non-expanded clones?
barplot(table(clono$frequency),xlab="Number of cells in clone")
# The barplot() function creates a bar plot.
# The table() function creates a contingency table of the counts at each unique value of clono$frequency.
# clono$frequency accesses the frequency column in the clono data frame
# Identity expaned clones, flag in metadata, and label in UMAP
# How many expanded clones are there? Try changing the threshold and compare the results
Nexpand = 1
# This is the threshold we will use to distinguish expanded from non-expanded clones
length(which(clono$frequency>1))
# which(clono$frequency > 1): Returns the indices of the rows where the frequency is greater than 1.
# length(which(clono$frequency > 1)): Counts the number of elements (clones) that have a frequency greater than 1. This gives you the number of expanded clones.
expanded_clones = clono$clonotype_id[clono$frequency>1]
# clono$clonotype_id[clono$frequency > 1]: Selects the clonotype_id values where the corresponding frequency is greater than 1.
#expanded_clones: Stores the clonotype IDs of the expanded clones in a new variable.
# Add a new metadata column indicating which cells are part of an expanded clone
seurat.raw = AddMetaData(seurat.raw,metadata = rep("no",ncol(seurat.raw)),col.name="TCR_expanded")
# This command adds a new metadata column to the seurat.raw Seurat object.
# in this new metadata where every cell is "no", and then changing the value for the expanded cells to "yes"
seurat.raw@meta.data$TCR_expanded[seurat.raw@meta.data$clonotype_id %in% expanded_clones] = "yes"
#This command updates the TCR_expanded metadata column.
# seurat.raw@meta.data$TCR_expanded: Accesses the TCR_expanded column in the metadata.
# seurat.raw@meta.data$clonotype_id %in% expanded_clones: Checks which cells have clonotype_id values that are in the expanded_clones list.
# = "yes": Sets the value to "yes" for cells that belong to expanded clones
# Look at the UMAP to see if expanded clones have similar gene expression
DimPlot(seurat.raw, reduction = "umap", group.by = "TCR_expanded", label = T)
# Do expanded T cells have different genes expressed compared to non-expanded T cells?
# To do ask this question, let's first separate the T cells from the rest of the object
# Check your clustering to see which one looks like it contains T cells
Idents(seurat.raw) = "seurat_clusters" # This means that subsequent operations will use the cluster identities assigned to each cell to seurat_clusters
seurat.t = subset(seurat.raw, idents = "1") #is the new Seurat object containing only the cells from cluster 1.
seurat.t # Check the number of samples (cells) to see how many T cells we have to work with
Idents(seurat.t) = "TCR_expanded" # This command sets the active identity class in the seurat.t object to TCR_expanded
deg_expanded = FindMarkers(seurat.t,ident.1="yes",ident.2="no",logfc.threshold = 0.25,min.pct = 0.1)
# This command identifies differentially expressed genes between two groups of cells
# Those marked as "yes" in the TCR_expanded metadata and those marked as "no".
# logfc.threshold: Minimum log2 fold-change threshold for identifying differentially expressed genes.
# min.pct: Minimum percentage of cells in which the gene is detected.
# Visualize the expression of the top DE genes for each annotated cell subset.
top30genes <- deg_expanded %>% filter(avg_log2FC > 0) %>% top_n(30, avg_log2FC)
# deg_expanded %>% filter(avg_log2FC > 0): This filters the differentially expressed genes (deg_expanded)
# to include only those with a positive average log2 fold-change (avg_log2FC > 0).
# top_n(30, avg_log2FC): From the filtered genes, this selects the top 30 genes with the highest average log2 fold-change.
genes <- rownames(top30genes)
seurat.t <- ScaleData(seurat.t, features = genes, do.center = T, do.scale = T)
# Scales the expression data for the selected genes (features = genes) in the seurat.t object.
# do.center = T: Centers the data by subtracting the mean expression for each gene.
# do.scale = T: Scales the data by dividing by the standard deviation for each gene
DoHeatmap(seurat.t, features = genes)
# Creates a heatmap of the expression data