|
| 1 | +--- |
| 2 | +title: "Quality Control for scATAC-Seq" |
| 3 | +author: "Harvard Chan Bioinformatics Core" |
| 4 | +date: "`r Sys.Date()`" |
| 5 | +params: |
| 6 | + max_PRF: !r Inf |
| 7 | + min_PRF: 4200 |
| 8 | + max_FRiP: !r Inf |
| 9 | + min_FRiP: 40 |
| 10 | + min_TSS: 2 |
| 11 | + max_NS: 2 |
| 12 | + max_blacklistratio: 0.02 |
| 13 | + data_dir: !r file.path("data") |
| 14 | + results_dir: !r file.path("results") |
| 15 | +output: |
| 16 | + html_document: |
| 17 | + code_folding: hide |
| 18 | + df_print: paged |
| 19 | + highlights: pygments |
| 20 | + number_sections: true |
| 21 | + self_contained: true |
| 22 | + theme: default |
| 23 | + toc: true |
| 24 | + toc_float: |
| 25 | + collapsed: true |
| 26 | + smooth_scroll: true |
| 27 | +editor_options: |
| 28 | + chunk_output_type: console |
| 29 | +params: |
| 30 | + params_file: params_qc.R |
| 31 | +--- |
| 32 | + |
| 33 | + |
| 34 | +```{r source_params, echo = F} |
| 35 | +metadata_fn='' |
| 36 | +se_object='' |
| 37 | +``` |
| 38 | + |
| 39 | +```{r load_libraries, cache = FALSE, message = FALSE, warning=FALSE} |
| 40 | +library(Seurat) |
| 41 | +library(Signac) |
| 42 | +library(tidyverse) |
| 43 | +library(ggplot2) |
| 44 | +library(ggrepel) |
| 45 | +library(cowplot) |
| 46 | +library(kableExtra) |
| 47 | +library(qs) |
| 48 | +library(bcbioR) |
| 49 | +ggplot2::theme_set(theme_light(base_size = 14)) |
| 50 | +opts_chunk[["set"]]( |
| 51 | + cache = FALSE, |
| 52 | + cache.lazy = FALSE, |
| 53 | + dev = c("png", "pdf"), |
| 54 | + error = TRUE, |
| 55 | + highlight = TRUE, |
| 56 | + message = FALSE, |
| 57 | + prompt = FALSE, |
| 58 | + tidy = FALSE, |
| 59 | + warning = FALSE, |
| 60 | + fig.height = 4) |
| 61 | +``` |
| 62 | + |
| 63 | + |
| 64 | +```{r subchunkify, echo=FALSE, eval=FALSE} |
| 65 | +#' Create sub-chunks for plots |
| 66 | +#' |
| 67 | +#' taken from: https://stackoverflow.com/questions/15365829/dynamic-height-and-width-for-knitr-plots |
| 68 | +#' |
| 69 | +#' @param pl a plot object |
| 70 | +#' @param fig.height figure height |
| 71 | +#' @param fig.width figure width |
| 72 | +#' @param chunk_name name of the chunk |
| 73 | +#' |
| 74 | +#' @author Andreas Scharmueller \email{andschar@@protonmail.com} |
| 75 | +#' |
| 76 | +subchunkify = function(pl, |
| 77 | + fig.height = 7, |
| 78 | + fig.width = 5, |
| 79 | + chunk_name = 'plot') { |
| 80 | + pl_deparsed = paste0(deparse(function() { |
| 81 | + pl |
| 82 | + }), collapse = '') |
| 83 | + |
| 84 | + sub_chunk = paste0( |
| 85 | + "```{r ", |
| 86 | + chunk_name, |
| 87 | + ", fig.height=", |
| 88 | + fig.height, |
| 89 | + ", fig.width=", |
| 90 | + fig.width, |
| 91 | + ", dpi=72", |
| 92 | + ", echo=FALSE, message=FALSE, warning=FALSE, fig.align='center'}", |
| 93 | + "\n(", |
| 94 | + pl_deparsed, |
| 95 | + ")()", |
| 96 | + "\n```" |
| 97 | + ) |
| 98 | + |
| 99 | + cat(knitr::knit( |
| 100 | + text = knitr::knit_expand(text = sub_chunk), |
| 101 | + quiet = TRUE |
| 102 | + )) |
| 103 | +} |
| 104 | +``` |
| 105 | + |
| 106 | + |
| 107 | +```{r sanitize-datatable} |
| 108 | +sanitize_datatable = function(df, ...) { |
| 109 | + # remove dashes which cause wrapping |
| 110 | + DT::datatable(df, ..., rownames=gsub("-", "_", rownames(df)), |
| 111 | + colnames=gsub("-", "_", colnames(df))) |
| 112 | +} |
| 113 | +``` |
| 114 | + |
| 115 | + |
| 116 | +```{r not_in} |
| 117 | +`%!in%` = Negate(`%in%`) |
| 118 | +``` |
| 119 | + |
| 120 | + |
| 121 | +# Overview |
| 122 | + |
| 123 | +- Project: `r project` |
| 124 | +- PI: `r PI` |
| 125 | +- Analyst: `r analyst` |
| 126 | +- Experiment: `r experiment` |
| 127 | +- Aim: `r aim` |
| 128 | + |
| 129 | + |
| 130 | +# Samples and metadata |
| 131 | + |
| 132 | +```{r load_metadata} |
| 133 | +meta_df=read_csv(metadata_fn) %>% mutate(sample = tolower(description)) %>% |
| 134 | + dplyr::select(-description) |
| 135 | +
|
| 136 | +ggplot(meta_df, aes(sample_type, fill = sample_type)) + |
| 137 | + geom_bar() + ylab("") + xlab("") + |
| 138 | + scale_fill_cb_friendly() |
| 139 | +``` |
| 140 | + |
| 141 | + |
| 142 | +```{r show-metadata} |
| 143 | +se <- readRDS(se_object) #local |
| 144 | +
|
| 145 | +
|
| 146 | +metrics <- metadata(se)$metrics %>% |
| 147 | + full_join(meta_df , by = c("sample" = "sample")) |
| 148 | +
|
| 149 | +meta_sm <- meta_df %>% |
| 150 | + as.data.frame() %>% |
| 151 | + column_to_rownames("sample") |
| 152 | +
|
| 153 | +meta_sm %>% sanitize_datatable() |
| 154 | +
|
| 155 | +``` |
| 156 | + |
| 157 | +```{r create_or_read_seurat_obj} |
| 158 | +filename <- 'data/scATAC_seurat.rds' |
| 159 | +if (!file.exists(filename)){ |
| 160 | + |
| 161 | +}else{ |
| 162 | + seuart <- readRDS(filename) |
| 163 | +} |
| 164 | +``` |
| 165 | + |
| 166 | + |
| 167 | +# ATAC-Seq {.tabset} |
| 168 | + |
| 169 | +We use some of the results from cellranger outputs and the peaks called using MACS2 for QCing the scATAC-Seq data. |
| 170 | + |
| 171 | +## Read counts per cell |
| 172 | + |
| 173 | +```{r warning=FALSE, message=FALSE, results='asis', fig.width=12} |
| 174 | +VlnPlot(seurat, features = "nCount_MACS2peaks", ncol = 1, pt.size = 0) + |
| 175 | + scale_fill_cb_friendly() + |
| 176 | + xlab("") + |
| 177 | + ylab("Reads") |
| 178 | +
|
| 179 | +cat('\n\n') |
| 180 | +``` |
| 181 | +## Detected peaks per cell |
| 182 | + |
| 183 | +```{r warning=FALSE, message=FALSE, fig.width=12} |
| 184 | +VlnPlot(seurat, features = "nFeature_MACS2peaks", ncol = 1, pt.size = 0) + |
| 185 | + scale_fill_cb_friendly() + |
| 186 | + xlab("") + |
| 187 | + ylab("UMI") |
| 188 | +
|
| 189 | +# VlnPlot(seurat, features = "nFeature_CellRangerPeaks", ncol = 1, pt.size = 0) + |
| 190 | +# scale_fill_cb_friendly() + |
| 191 | +# xlab("") + |
| 192 | +# ylab("UMI") |
| 193 | +``` |
| 194 | + |
| 195 | +## QC metrics {.tabset} |
| 196 | + |
| 197 | +### Total number of fragments in peaks {.tabset} |
| 198 | + |
| 199 | +This metric represents the total number of fragments (= reads) mapping within a region of the genome that is predicted to be accessible (= a peak). It's a measure of cellular sequencing depth / complexity. Cells with very few reads may need to be excluded due to low sequencing depth. Cells with extremely high levels may represent doublets, nuclei clumps, or other artefacts. |
| 200 | + |
| 201 | + |
| 202 | +```{r results='asis', warning=FALSE, message=FALSE, fig.width=12, fig.height=8} |
| 203 | +DefaultAssay(seurat) <- "MACS2peaks" |
| 204 | +
|
| 205 | +cat('#### Histogram \n\n') |
| 206 | +seurat@meta.data %>% |
| 207 | + ggplot(aes(x = atac_peak_region_fragments)) + |
| 208 | + geom_histogram() + |
| 209 | + facet_wrap(~sample, scale = 'free') + |
| 210 | + geom_vline(xintercept = params$min_PRF) |
| 211 | +``` |
| 212 | + |
| 213 | + |
| 214 | +```{r results='asis', warning=FALSE, message=FALSE, fig.width=12} |
| 215 | +cat('\n#### Violin plot\n\n') |
| 216 | +VlnPlot( |
| 217 | + object = seurat, |
| 218 | + features = 'atac_peak_region_fragments', |
| 219 | + pt.size = 0 |
| 220 | +) + ylab('Total number of fragments in peaks') + |
| 221 | + NoLegend() + |
| 222 | + geom_hline(yintercept = params$min_PRF) + xlab("") + |
| 223 | + scale_fill_cb_friendly() |
| 224 | +
|
| 225 | +cat('\n\n') |
| 226 | +``` |
| 227 | + |
| 228 | +### Fraction of fragments in peaks |
| 229 | + |
| 230 | +It represents the fraction of all fragments that fall within ATAC-seq peaks. Cells with low values (i.e. <15-20%) often represent low-quality cells or technical artifacts that should be removed. Note that this value can be sensitive to the set of peaks used. |
| 231 | + |
| 232 | +```{r results='asis'} |
| 233 | +VlnPlot( |
| 234 | + object = seurat, |
| 235 | + features = 'pct_reads_in_peaks', |
| 236 | + pt.size = 0 |
| 237 | +) + NoLegend() + xlab("") + |
| 238 | + scale_fill_cb_friendly() + |
| 239 | + geom_hline(yintercept = params$min_FRiP) |
| 240 | +
|
| 241 | +cat('\n\n') |
| 242 | +``` |
| 243 | + |
| 244 | + |
| 245 | +### Transcriptional start site (TSS) enrichment score {.tabset} |
| 246 | + |
| 247 | +The ENCODE project has defined an ATAC-seq targeting score based on the ratio of fragments centered at the TSS to fragments in TSS-flanking regions (see https://www.encodeproject.org/data-standards/terms/). Poor ATAC-seq experiments typically will have a low TSS enrichment score. We can compute this metric for each cell with the TSSEnrichment() function, and the results are stored in metadata under the column name TSS.enrichment. |
| 248 | + |
| 249 | + |
| 250 | +```{r results='asis'} |
| 251 | +VlnPlot( |
| 252 | + object = seurat, |
| 253 | + features = 'TSS.enrichment', |
| 254 | + pt.size = 0 |
| 255 | +) + scale_fill_cb_friendly() + NoLegend() + xlab("") |
| 256 | +cat('\n\n') |
| 257 | +``` |
| 258 | +The following tabs show the TSS enrichment score distribution for each sample. Cells with high-quality ATAC-seq data should show a clear peak in reads at the TSS, with a decay the further we get from it. |
| 259 | + |
| 260 | +Each plot is split between cells with a high or low global TSS enrichment score (cuffoff at `r params$min_TSS`), to double-check whether cells with lowest enrichment scores still follow the expected pattern or rather need to be excluded. |
| 261 | + |
| 262 | +```{r results='asis'} |
| 263 | +seurat$TSS.group <- ifelse(seurat$TSS.enrichment > params$min_TSS, "High", "Low") |
| 264 | +Idents(seurat) <- 'sample' |
| 265 | +for (sample in levels(seurat$sample)){ |
| 266 | + cat('####', sample, '\n\n') |
| 267 | + p <- TSSPlot(subset(x = seurat, idents = sample), group.by = "TSS.group") + NoLegend() |
| 268 | + print(p) |
| 269 | + cat('\n\n') |
| 270 | +} |
| 271 | +``` |
| 272 | + |
| 273 | +### Nucleosome signal {.tabset} |
| 274 | + |
| 275 | +The histogram of DNA fragment sizes (determined from the paired-end sequencing reads) should exhibit a strong nucleosome banding pattern corresponding to the length of DNA wrapped around a single nucleosome, i.e peaks at approximately 100bp (nucleosome-free), and mono, di and tri nucleosome-bound peaks at 200, 400 and 600bp. We calculate this per single cell, and quantify the approximate ratio of mononucleosomal to nucleosome-free fragments (stored as nucleosome_signal). Cells with lower nucleosome signal have a higher ratio of nucleosome-free fragments. |
| 276 | + |
| 277 | +```{r warning = FALSE, results='asis'} |
| 278 | +seurat$nucleosome_group <- ifelse(seurat$nucleosome_signal > 1, "high NS", "low NS") |
| 279 | +for (sample in levels(seurat$sample)){ |
| 280 | + cat('####', sample, '\n\n') |
| 281 | + p <- FragmentHistogram(seurat, group.by = 'nucleosome_group', cells = colnames(seurat[, seurat$sample == sample])) |
| 282 | + print(p) |
| 283 | + cat('\n\n') |
| 284 | +} |
| 285 | +# FragmentHistogram(seurat, group.by = 'nucleosome_group', cells = colnames(seurat[, seurat$sample == 'Control'])) |
| 286 | +``` |
| 287 | + |
| 288 | +```{r fig.width=12} |
| 289 | +VlnPlot( |
| 290 | + object = seurat, |
| 291 | + features = 'nucleosome_signal', |
| 292 | + pt.size = 0 |
| 293 | +) + scale_fill_cb_friendly() + |
| 294 | +NoLegend() + xlab("") |
| 295 | +``` |
| 296 | + |
| 297 | + |
| 298 | + |
| 299 | + |
| 300 | +### Blacklist ratio |
| 301 | + |
| 302 | +tIt's he ratio of reads in genomic blacklist regions. The [ENCODE project](https://www.encodeproject.org/) has provided a list of [blacklist regions](https://github.com/Boyle-Lab/Blacklist), representing reads which are often associated with artefactual signal. Cells with a high proportion of reads mapping to these areas (compared to reads mapping to peaks) often represent technical artifacts and should be removed. ENCODE blacklist regions for human (hg19 and GRCh38), mouse (mm10), Drosophila (dm3), and C. elegans (ce10) are included in the Signac package. **Peaks overlapping with the balcklist regions were removed in the analysis, so we don't show blacklist fraction here**. |
| 303 | + |
| 304 | +Line is drawn at `r params$max_blacklistratio`. |
| 305 | + |
| 306 | +```{r fig.width=12} |
| 307 | +VlnPlot( |
| 308 | + object = seurat, |
| 309 | + features = 'blacklist_fraction', |
| 310 | + pt.size = 0 |
| 311 | +) + scale_fill_cb_friendly() + |
| 312 | + NoLegend() + |
| 313 | + geom_hline(yintercept = params$max_blacklistratio) |
| 314 | + |
| 315 | +``` |
| 316 | + |
| 317 | + |
| 318 | +## Normalization, linear dimensional reduction, and clustering |
| 319 | + |
| 320 | +* Normalization: Signac performs term frequency-inverse document frequency (TF-IDF) normalization. This is a two-step normalization procedure, that both normalizes across cells to correct for differences in cellular sequencing depth, and across peaks to give higher values to more rare peaks. |
| 321 | + |
| 322 | +* Feature selection: The low dynamic range of scATAC-seq data makes it challenging to perform variable feature selection, as we do for scRNA-seq. Instead, we can choose to use only the top n% of features (peaks) for dimensional reduction, or remove features present in less than n cells with the FindTopFeatures() function. Here, we will use all features, though we note that we see very similar results when using only a subset of features (try setting min.cutoff to ‘q75’ to use the top 25% all peaks), with faster runtimes. Features used for dimensional reduction are automatically set as VariableFeatures() for the Seurat object by this function. |
| 323 | + |
| 324 | +* Dimension reduction: We next run singular value decomposition (SVD) on the TD-IDF matrix, using the features (peaks) selected above. This returns a reduced dimension representation of the object (for users who are more familiar with scRNA-seq, you can think of this as analogous to the output of PCA). |
| 325 | + |
| 326 | +The combined steps of TF-IDF followed by SVD are known as latent semantic indexing (LSI), and were first introduced for the analysis of scATAC-seq data by [Cusanovich et al. 2015.](https://www.science.org/doi/10.1126/science.aax6234) |
| 327 | + |
| 328 | +The first LSI component often captures sequencing depth (techni ccal variation) rather than biological variation. If this is the case, the component should be removed from downstream analysis. We can assess the correlation between each LSI component and sequencing depth using the DepthCor() function (see below). |
| 329 | +Here we see there is a very strong correlation between the first LSI component and the total number of counts for the cell, so we will perform downstream steps without this component. |
| 330 | + |
| 331 | +```{r results='asis'} |
| 332 | +DepthCor(seurat, assay = 'MACS2peaks') |
| 333 | +cat('\n\n') |
| 334 | +``` |
| 335 | + |
| 336 | +## UMAP plots |
| 337 | + |
| 338 | +```{r results='asis'} |
| 339 | +DimPlot(object = seurat, group.by = 'sample', reduction = "umapATAC") + |
| 340 | + scale_color_cb_friendly() |
| 341 | +
|
| 342 | +cat('\n\n') |
| 343 | +``` |
| 344 | + |
| 345 | +<!-- # {.unlisted .unnumbered} --> |
| 346 | + |
| 347 | + |
| 348 | + |
| 349 | +# R session |
| 350 | + |
| 351 | +List and version of tools used for the QC report generation. |
| 352 | + |
| 353 | +```{r} |
| 354 | +sessionInfo() |
| 355 | +``` |
0 commit comments