# How to Build a Single-Cell RNA-seq Report in R Markdown

By the end of this walkthrough you will have a working, parameterized single-cell RNA-seq R Markdown template that loads a [10x Genomics](/blog/guides/10x-genomics) count matrix, runs quality control, normalization, clustering, and marker-based annotation in Seurat, and knits the entire analysis into a single HTML file. You will be able to render one report per sample with a short loop, verify that each step did what you expected, and record package versions so the same report can be rebuilt a year from now.

Here is what you need installed or on hand:

- R 4.2 or newer plus RStudio Desktop, which bundles the pandoc converter that knitr needs
- The packages Seurat, rmarkdown, knitr, tidyverse, and patchwork
- Optional Bioconductor packages: SingleCellExperiment, scater, and scDblFinder
- One or more 10x output folders named filtered_feature_bc_matrix, each containing matrix.mtx.gz, barcodes.tsv.gz, and features.tsv.gz
- About 8 GB of RAM for a 10,000-cell sample, more for larger studies

The worked example uses the free pbmc_3k_v3 dataset from the 10x Genomics website, roughly 2,700 peripheral blood mononuclear cells (PBMCs). Every command below runs as written against that data. The walkthrough doubles as a single-cell RNA-seq R Markdown template example, so you can swap the PBMC paths for your own sample folders and keep the rest of the structure intact.

## Why a Report Template Beats Loose Scripts

A single-cell analysis is a long, ordered chain. Best-practice tutorials describe pre-processing (quality control, normalization, data correction, feature selection, and dimensionality reduction) followed by cell- and gene-level downstream analysis [1]. Reviews of the field add cell-type annotation, dataset integration, and cell-cell communication inference to that list [2]. A bulk RNA-seq report can move from counts to differential expression in a few steps, while a single-cell report must document each stage separately, because each stage transforms the object the next stage consumes [3].

A template helps in three concrete ways. First, it enforces the order, so normalization never runs before filtering. Second, it records every threshold you chose in a params block where a reviewer can see it. Third, knitting regenerates every figure from the raw matrix, which means the report is the analysis rather than a description of an analysis that happened somewhere else.

The format also suits focused reanalysis projects. One study mined public scRNA-seq datasets to map ACE2 expression across human organs and flag the cell types most at risk during the COVID-19 pandemic, and in report form that project is a template run against public data with a single question attached [4]. Discovery studies work the same way. Single-cell profiling of 4,645 cells from 19 melanoma patients separated malignant cells from immune, stromal, and endothelial cells and exposed a drug-resistance expression state [5], and early glioblastoma profiling of 430 cells from five tumors revealed extensive transcriptional variation that bulk methods averaged away [6]. Reports like these succeed when a reader can trace any figure back to code, which is exactly what R Markdown provides.

## Install the Toolchain

R for bioinformatics remains the default environment because CRAN and Bioconductor host the analysis packages and R Markdown turns their output into documents. Install the core packages from CRAN and the single-cell utilities from Bioconductor:

```r
install.packages(c("Seurat", "rmarkdown", "knitr", "tidyverse", "patchwork"))

if (!requireNamespace("BiocManager", quietly = TRUE)) {
  install.packages("BiocManager")
}
BiocManager::install(c("SingleCellExperiment", "scater", "scDblFinder"))
```

Check what you have:

```r
R.version.string
packageVersion("Seurat")
packageVersion("rmarkdown")
```

Bioconductor distributes its tutorials as package vignettes, and the scater vignette is the closest thing to an official Bioconductor tutorial for single-cell quality control. It covers the same QC steps used below, expressed for SingleCellExperiment objects, and it is worth reading even if you stay in Seurat for the analysis.

## Inside the Single-Cell RNA-seq R Markdown Template Format

The template format has ten parts, in this order:

1. A YAML header that sets the output format and the report parameters
2. A setup chunk with global chunk options and a random seed
3. Data loading into a Seurat object
4. Quality-control metrics, plots, and cell filtering
5. Normalization and variable feature selection
6. Dimensionality reduction with PCA and UMAP
7. Clustering
8. Marker genes and cell-type annotation
9. Multi-sample notes, including integration
10. Session information

Each part is one or two code chunks with a short interpretation paragraph under the figures. The interpretation paragraphs are where you write the science, and the code is the record of how each figure was made. The template in this article is a free single-cell RNA-seq R Markdown template, so copy it, rename it, and adapt the thresholds to your project.

## The Walkthrough: Ten Steps From Raw Matrix to Finished HTML

### Step 1. Write the YAML Header and Report Parameters

```yaml
---
title: "Single-Cell RNA-seq QC and Clustering Report"
subtitle: "10x Genomics 3' v3 chemistry, one sample per report"
author: "Your Name"
date: "`r Sys.Date()`"
params:
  sample_id: "PBMC-01"
  data_dir: "data/PBMC-01/filtered_feature_bc_matrix"
  min_features: 200
  max_features: 2500
  max_mt_pct: 5
  n_pcs: 30
  resolution: 0.5
output:
  html_document:
    toc: true
    toc_depth: 2
    toc_float: true
    number_sections: true
    code_folding: hide
    df_print: paged
    theme: readable
---
```

The params block is what turns this file into a template rather than a one-off script. Every value there is available in your code as params$sample_id, params$max_mt_pct, and so on. Change the values at render time and the same file produces a different report without any editing. Numeric fields stay numeric, so params$max_mt_pct is the number 5, not the text "5". Parameters can be character, numeric, or logical.

The output block controls the document. toc_float pins the table of contents to the side of the page so reviewers can jump between sections. code_folding: hide collapses every code chunk behind a Show button, which keeps the report readable for collaborators who care about the figures and lets reviewers expand the code when they need it. df_print: paged turns long data frames into scrollable tables instead of walls of text. If a journal wants vector figures, the same header works with pdf_document, at the cost of a larger file and slower knitting.

### Step 2. Set Global Chunk Options and a Seed

Put this chunk immediately after the YAML header:

```{r setup, include=FALSE}
knitr::opts_chunk$set(
  echo = TRUE,
  message = FALSE,
  warning = FALSE,
  fig.width = 7,
  fig.height = 5,
  dpi = 150,
  cache = FALSE
)
set.seed(1234)
```

include = FALSE runs the chunk but prints nothing, which is what you want for setup. set.seed(1234) fixes the random seed so PCA initializations, UMAP layouts, and clustering runs come out the same on every knit. The options set here apply to every later chunk unless a chunk overrides them.

| Option | What it controls | Recommended value in an scRNA-seq report |
|--|--|--|
| echo | Whether R code appears in the output | TRUE for analysis sections, FALSE for presentation-only figures |
| include | Run the chunk but hide its output | FALSE for setup and library() chunks |
| message, warning | Show package messages and warnings | FALSE to keep Seurat startup text out of the rendered file |
| cache | Save chunk results and reuse them on the next knit | TRUE for slow steps such as FindAllMarkers() |
| eval | Run the chunk at all | FALSE for optional branches such as doublet detection |
| fig.width, fig.height | Figure size in inches | 7 by 5 for UMAP and violin plots |
| dpi | Figure resolution in dots per inch | 150 for HTML, 300 if you also knit to PDF |
| error | Keep knitting after an error | FALSE in the final report, TRUE only while debugging |

Two options deserve extra comment. cache = TRUE saves a chunk's result to disk and reuses it on the next knit, which turns a 20-minute marker run into a few seconds on re-knit. The cost is stale caches when upstream data changes, so cache slow chunks only and delete the *_cache folder whenever you change filtering. error = FALSE, the default, stops the knit at the first failure, which is correct for a final report because a document with silent failures is worse than no document.

### Step 3. Load the Count Matrix

```{r load-data, cache=TRUE}
library(Seurat)
library(ggplot2)
library(dplyr)

counts <- Read10X(data.dir = params$data_dir)
obj <- CreateSeuratObject(
  counts = counts,
  project = params$sample_id,
  min.cells = 3,
  min.features = params$min_features
)
obj
```

Expected output for the PBMC demo:

```text
An object of class seurat
32738 features across 2700 samples within 1 assay
Active assay: RNA (32738 features, 0 variable features)
```

The three input files each play a role. matrix.mtx.gz holds the counts in sparse triplet form, barcodes.tsv.gz lists one cell barcode per row, and features.tsv.gz lists the genes. A barcode is the short DNA sequence that labels everything captured in one droplet, and a UMI (unique molecular identifier) is the tag that lets the pipeline collapse PCR duplicates so counts reflect original molecules.

The two arguments filter at load time. min.cells = 3 keeps a gene only if it is detected in at least 3 cells, which trims the long tail of near-empty genes. min.features = 200 keeps a cell only if at least 200 genes were detected in it, an early screen for empty droplets and damaged cells. The counts stay sparse, meaning only nonzero values occupy memory, and that property is what makes single-cell data tractable in R at all.

Memory planning matters as studies grow. A multiplexed study of more than 1.2 million PBMCs from 261 donors shows the scale these reports can reach [7]. Past roughly 100,000 cells, prefer the h5 format, load with Read10X_h5(), and process samples one report at a time before any integration step. If your pipeline hands you a SingleCellExperiment instead, the Bioconductor route is DropletUtils::read10xCounts() on the same folder, and as.Seurat() converts it back when needed.

### Step 4. Add QC Metrics and Filter Low-Quality Cells

```{r qc-metrics}
obj[["percent.mt"]] <- PercentageFeatureSet(obj, pattern = "^MT-")
VlnPlot(obj, features = c("nFeature_RNA", "nCount_RNA", "percent.mt"), ncol = 3, pt.size = 0)
```

Three metrics drive the violin plots. nFeature_RNA counts distinct genes detected per cell, nCount_RNA counts total UMIs per cell, and percent.mt is the share of a cell's UMIs that come from mitochondrial genes. The worked calculation: a cell with 900 total UMIs of which 45 are mitochondrial has percent.mt = 45 / 900 * 100 = 5. High mitochondrial fractions usually indicate broken or dying cells, because damaged membranes lose cytoplasmic RNA faster than mitochondrial RNA.

Filter using the parameters you declared in the YAML header:

```{r filter-cells}
obj <- subset(
  obj,
  subset = nFeature_RNA > 200 &
           nFeature_RNA < params$max_features &
           percent.mt < params$max_mt_pct
)
ncol(obj)   # cells that survived
```

For the PBMC demo these filters keep about 2,638 of the 2,700 cells. Quality control sits at the front of every published workflow for a reason, since clustering and differential expression inherit whatever errors QC leaves behind [1] [8]. Treat the numbers as starting points and justify your final values in the report text, because the right cutoffs depend on the tissue and the dissociation protocol.

Doublets are the second QC concern. A doublet is one barcode that captured two cells, and doublets violate the single-cell premise and can fabricate plausible-looking intermediate populations. Deep-learning methods such as Solo train a classifier to separate simulated doublets from observed profiles and report higher accuracy than earlier tools [9]. In R, the Bioconductor package scDblFinder does the same job in a few lines:

```{r doublets, eval=FALSE}
library(scDblFinder)
sce <- as.SingleCellExperiment(obj)
sce <- scDblFinder(sce)
table(sce$scDblFinder.class)
obj$doublet_call <- sce$scDblFinder.class
obj <- subset(obj, subset = doublet_call == "singlet")
```

The chunk is set to eval = FALSE so the report shows the method without running it until you decide to. Check the result by re-plotting the violin plots after filtering and confirming the percent.mt distribution now ends below your threshold.

### Step 5. Normalize the Counts and Pick Variable Features

```{r normalize}
obj <- NormalizeData(obj, normalization.method = "LogNormalize", scale.factor = 10000)
obj <- FindVariableFeatures(obj, selection.method = "vst", nfeatures = 2000)
obj <- ScaleData(obj, vars.to.regress = "percent.mt")
obj <- RunPCA(obj, features = VariableFeatures(obj))
```

LogNormalize scales each cell to 10,000 UMIs and then applies log1p, so a gene with 50 raw counts in a cell of 5,000 total UMIs becomes log1p(50 / 5000 * 10000) = log1p(100) = 4.62. The scale.factor of 10000 is the convention that makes values comparable across cells with different sequencing depth. FindVariableFeatures with the vst method ranks genes by how much their variance exceeds what mean expression alone would predict, and nfeatures = 2000 is the standard panel size for downstream PCA. ScaleData with vars.to.regress = "percent.mt" fits out residual mitochondrial burden so it does not dominate the top principal components. State the regression choices in the report, because regressing out a covariate removes whatever biology travels with it.

The alternative is SCTransform, which models counts with regularized negative binomial regression using cellular sequencing depth as a covariate and works with Pearson residuals, removing the need for pseudocount addition and log-transformation [10]. The regularization pools information across genes of similar abundance, which keeps the negative binomial model from overfitting the data [10]. In code it replaces the first three lines above:

```{r sctransform, eval=FALSE}
obj <- SCTransform(obj, vars.to.regress = "percent.mt", verbose = FALSE)
```

SCTransform costs more memory and runtime. For a routine per-sample report, either method is defensible as long as the report states which one was used.

### Step 6. Reduce Dimensions and Choose How Many PCs

```{r pca, fig.height = 4}
ElbowPlot(obj, ndims = 50)
```

PCA (principal component analysis) compresses the 2,000 variable genes into ordered principal components, each capturing less variance than the last. The elbow plot scores each PC and shows where additional components stop adding structure. For the PBMC demo the curve flattens somewhere between 10 and 20, and 30 is a safe choice that rarely hurts. Larger datasets with more cell types usually need more PCs. Record your choice as params$n_pcs so the report shows it, and treat a suspiciously large cluster count as one sign you kept too few.

### Step 7. Cluster the Cells and Draw the UMAP

```{r cluster}
obj <- RunUMAP(obj, dims = 1:params$n_pcs, seed.use = 1234)
obj <- FindNeighbors(obj, dims = 1:params$n_pcs)
obj <- FindClusters(obj, resolution = params$resolution)
DimPlot(obj, reduction = "umap", label = TRUE, repel = TRUE) + NoLegend()
table(Idents(obj))   # cells per cluster
```

FindNeighbors builds a k-nearest-neighbor graph over the PCs, connecting each cell to the cells with the most similar expression profiles. FindClusters then runs Louvain community detection on that graph, grouping cells into communities that are densely connected internally. The resolution parameter tunes granularity: higher values split the data into more, smaller clusters, and lower values merge them. At resolution 0.5 the PBMC demo resolves into 9 clusters, and the console prints the community count when the chunk runs:

```text
Modularity Optimizer version 1.3.0 (Louvain algorithm)
...
Number of communities: 9
Finished
```

Clustering reviews treat this graph-based route as the standard approach while cautioning that no single resolution is objectively correct, so report the value you used and show the UMAP next to a neighboring resolution if the audience will scrutinize the split [8]. UMAP (uniform manifold approximation and projection) arranges the cells in two dimensions for the figure, and the layout depends on the seed, which is why the chunk sets seed.use = 1234. Unbiased clustering at sufficient depth keeps finding biology that marker-based panels missed, as in the deep-sequencing study that resolved six dendritic cell and four monocyte populations from about 2,400 enriched blood cells [11].

### Step 8. Find Markers and Annotate Cell Types

```{r markers, cache=TRUE}
markers <- FindAllMarkers(
  obj,
  only.pos = TRUE,
  min.pct = 0.25,
  logfc.threshold = 0.25
)
markers %>%
  group_by(cluster) %>%
  slice_max(n = 2, order_by = avg_log2FC) %>%
  kable()
```

FindAllMarkers tests every gene against every cluster with a Wilcoxon rank-sum test by default. min.pct = 0.25 requires a gene to be detected in at least 25 percent of cells in one of the two groups being compared, and logfc.threshold = 0.25 skips genes whose log fold change falls below that value, both of which cut runtime without losing the obvious markers. The avg_log2FC column reports the difference on the log2 scale, so 0.25 corresponds to a ratio of 2^0.25, about 1.19, meaning roughly 19 percent higher expression. The p_val_adj column carries the Bonferroni-adjusted p-value. The cache = TRUE option matters most here, because this is usually the slowest chunk in the report.

For the PBMC demo the top markers land where the textbooks say they should: IL7R for the CD4 T cell clusters, CD14 and LYZ for classical monocytes, MS4A1 for B cells, GNLY and NKG7 for natural killer cells, and PPBP for platelet contamination. Confirm each cluster's identity with a dot plot of canonical genes before renaming anything:

```{r annotate, fig.width = 8, fig.height = 3}
DotPlot(obj, features = c("CD3D", "IL7R", "CD8A", "GNLY", "MS4A1",
                          "CD14", "FCGR3A", "PPBP")) +
  RotatedAxis()

obj <- RenameIdents(obj, `0` = "CD4 T cells", `1` = "CD14+ monocytes",
                    `2` = "B cells", `3` = "CD8 T cells", `4` = "NK cells",
                    `5` = "FCGR3A+ monocytes", `6` = "Platelets")
obj[["cell_type"]] <- Idents(obj)
DimPlot(obj, reduction = "umap", group.by = "cell_type", label = TRUE, repel = TRUE)
```

The names on the left of each pair in RenameIdents must match the cluster numbers in your data, so check table(Idents(obj)) first, and any cluster you leave unmapped keeps its number. Storing the labels in a cell_type column keeps the numeric identities intact for later steps. Annotation is where computational and biological judgment meet, and reviews of annotation methods emphasize that automated reference mapping and manual marker curation each carry failure modes, so the report should state which approach produced the final labels [2].

### Step 9. Handle Multi-Sample Studies and Batch Effects

A batch effect is a technical difference between samples, caused by different isolation protocols, library preparations, or sequencing platforms, that can mimic biological differences [12]. Because such effects are effectively unavoidable when datasets accumulate in public repositories, integration methods exist to align datasets while preserving real cell-type differences [12]. The anchor-based Seurat workflow looks like this:

```{r integrate, eval=FALSE}
# assumes obj holds multiple samples, with a "sample" metadata column
samples <- SplitObject(obj, split.by = "sample")
samples <- lapply(samples, function(x) {
  x <- NormalizeData(x, verbose = FALSE)
  x <- FindVariableFeatures(x, verbose = FALSE)
  x <- ScaleData(x, verbose = FALSE)
  x <- RunPCA(x, verbose = FALSE)
})
anchors <- FindIntegrationAnchors(object.list = samples, dims = 1:30)
integrated <- IntegrateData(anchorset = anchors, dims = 1:30)
integrated <- RunUMAP(integrated, dims = 1:30)
DimPlot(integrated, group.by = "sample")
```

A practical reporting strategy is to render one per-sample report first, inspect QC and cluster shapes side by side, and only then run integration in a separate report. Integration methods rest on assumptions about what should and should not be shared between datasets, and overcorrection can erase genuine condition-specific biology, so the integration report should show cells colored by sample before and after correction [2]. For large studies, embedding-based alternatives such as Harmony operate on the PCA output and are faster than anchor integration.

### Step 10. Render the Report and Automate One File Per Sample

![Flowchart from the sample vector through the walk loop to rmarkdown render with a params list, ending in one HTML file per sample.](/article-images/illustrations/single-cell-rna-seq-rmarkdown-template-render-loop.webp)

Knit from the RStudio Knit button, or from the console:

```r
rmarkdown::render(
  input = "report.Rmd",
  params = list(
    sample_id = "PBMC-02",
    data_dir = "data/PBMC-02/filtered_feature_bc_matrix"
  ),
  output_file = "scRNA_report_PBMC-02.html"
)
```

To produce a report for every sample in the study:

```r
library(purrr)
samples <- c("PBMC-01", "PBMC-02", "PBMC-03")
walk(samples, function(s) {
  rmarkdown::render(
    "report.Rmd",
    params = list(
      sample_id = s,
      data_dir = file.path("data", s, "filtered_feature_bc_matrix")
    ),
    output_file = paste0("scRNA_report_", s, ".html"),
    quiet = TRUE
  )
})
```

Each render runs the file in a fresh R session, so the params block is the only thing that differs between reports. Close the template with a session information chunk:

```{r session-info}
sessionInfo()
```

sessionInfo() prints the R version, the platform, and the exact version of every attached package, which is the minimum record a reviewer needs to rerun the analysis. For stronger guarantees, initialize renv in the project with install.packages("renv"), then renv::init(), then renv::snapshot(), so package versions are written to a lockfile that can be restored later. Check the rendered HTML before sending it: confirm every figure is populated (a blank figure means a chunk failed while error = TRUE was set), confirm the table of contents links work, and confirm the code folding buttons appear.

## Where to Take the Report Next

Once the per-sample report is solid, three extensions cover most projects. Cell-cell communication inference predicts ligand-receptor pairs between annotated cell types, and comparative work shows that both the interaction database and the method strongly influence the predictions, so run a consensus framework such as LIANA and report which resources you used [13]. Immune receptor sequences can be reconstructed directly from standard scRNA-seq data without V(D)J enrichment using tools such as TRUST4, which adds repertoire analysis to the same report [14]. Multiomic assays that capture RNA together with chromatin accessibility and perturbation identity extend the template toward gene regulatory network questions [15]. Spatial transcriptomics increasingly pairs with scRNA-seq to put cell types back into tissue context, and reviews treat the two as complementary layers of the same analysis [2].

## Common Errors and Their Exact Fixes

1. `there is no package called 'Seurat'`. The R session doing the knitting does not have the package. Install it into the library RStudio actually uses, confirmed with .libPaths(), and re-knit.

2. `Directory does not contain an h5 file or 10X format files`. data_dir points to the wrong folder. Point it at the folder that directly contains matrix.mtx.gz, barcodes.tsv.gz, and features.tsv.gz, not its parent directory. For h5 files, call Read10X_h5() instead.

3. `Error: cannot allocate vector of size 2.4 Gb`. R ran out of RAM. Keep counts sparse, never coerce them to a data frame, load from h5, filter early, or render on a machine with more memory.

4. `Cannot find 'pca' object in the provided object`. RunUMAP() was called before RunPCA(). Run the PCA chunk first, or check available reductions with Reductions(obj) and pass the correct name.

5. `Error: pandoc version 1.12.3 or higher is required and was not found`. knitr cannot find pandoc. Install RStudio Desktop, which bundles a compatible pandoc, or install pandoc system-wide and restart R.

6. `bad indentation of a mapping entry at line 12`. The YAML header uses tabs or uneven spacing. Indent two spaces per level, never tabs, and keep the closing, - on its own line.

7. `Quitting from lines 88-96 [load-data] (report.Rmd)` followed by another error. knitr stopped at the first failing chunk. Read the error after the Quitting line, fix that chunk, and re-knit. During debugging you can set knitr::opts_chunk$set(error = TRUE) to see every failure at once, then remove it before the final render.

8. `The idents have not been set, or the number of idents is less than 2`. FindAllMarkers() ran before clustering, or the identities were overwritten. Run FindClusters() first and confirm table(Idents(obj)) shows at least two groups.

9. In Seurat v5, merging samples leaves counts split across separate layers and PCA complains about the data layout. Fix it with obj <- JoinLayers(obj) before normalization.

## Common Mistakes and Limitations

- Copying thresholds without justification. The 200 to 2500 gene window and a 5 percent mitochondrial cap are PBMC conventions, not laws. Cell types differ in how much stress sample handling causes, and comparative work on sensitive populations such as neutrophils shows that collection and processing choices change what QC needs to remove [16]. State your values and the reason for them.
- Skipping doublet detection. Doublets create plausible intermediate clusters and inflate apparent complexity. Report whether you ran a method such as scDblFinder or Solo and how many barcodes were removed [9].
- Integrating before looking. Batch correction applied blindly can merge cell types that genuinely differ or split ones that do not. Inspect per-sample reports first, then integrate, then show before-and-after plots.
- Treating cluster labels as ground truth. Clusters are computational partitions, and annotation methods differ in reliability. Anchor the labels in marker expression shown in the report, and keep the numeric cluster identities alongside the names [2] [11].
- Reporting a single resolution as if it were discovered rather than chosen. Resolution is a dial you set, so show the value, and show a neighboring resolution if the biology depends on the split.
- Producing reports that cannot be rerun. No seed, absolute paths to someone's desktop, and no sessionInfo() are the three most common reproducibility failures. The template fixes all three by construction.
- One further limitation: a report documents the analysis you ran, not the biology you wish you had measured. Annotation confidence, batch structure, and sequencing depth all bound what the figures can support, and the interpretation paragraphs should say so plainly.

## Frequently Asked Questions

### What is a single-cell RNA-seq R Markdown template?

It is a pre-structured .Rmd file that runs a complete single-cell analysis in order, from quality control through clustering and annotation, and knits the code, figures, and interpretation into one HTML report. You edit the parameters and the interpretation text, and the structure stays fixed.

### Is this template free to use and modify?

Yes. Copy the YAML header, chunk options, and code chunks from this article into your own project and adapt the thresholds, sample names, and sections with no restrictions.

### Can I use Bioconductor packages instead of Seurat in the same template?

Yes. Convert with as.SingleCellExperiment(obj), run QC and normalization with scater and scran, and convert back with as.Seurat(sce) if needed. The report structure and the rendering workflow do not change.

### How do I render one report per sample without editing the file each time?

Put the sample-specific values in the params block and call rmarkdown::render() with a params list, either once per sample or inside a purrr::walk() loop that reads sample names from a vector.

### Which chunk options matter most for large single-cell datasets?

cache = TRUE on slow chunks such as FindAllMarkers(), message = FALSE and warning = FALSE on everything, and eval = FALSE on optional branches so they print without running. Keep cache = FALSE on the setup chunk itself.

### What QC thresholds should I start with?

A common starting point is 200 to 2500 detected genes per cell and less than 5 percent mitochondrial UMIs for PBMCs, with the mitochondrial cap raised for tissues where dissociation stresses cells. Report the values you settled on and why.

### Should I use LogNormalize or SCTransform in the report?

LogNormalize is the simpler default and is fine for routine per-sample reports. SCTransform uses regularized negative binomial regression and often improves variable feature selection and downstream tests, at the cost of more memory and runtime. Either is defensible if the report states which one was used.

### How do I keep the report reproducible months later?

Set a seed in the setup chunk, end the document with sessionInfo(), and track package versions with renv so a lockfile records exactly what was installed. Keep the data directory relative to the project root.

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [
    {
      "@type": "Question",
      "name": "What is a single-cell RNA-seq R Markdown template?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "It is a pre-structured .Rmd file that runs a complete single-cell analysis in order, from quality control through clustering and annotation, and knits the code, figures, and interpretation into one HTML report."
      }
    },
    {
      "@type": "Question",
      "name": "Is this template free to use and modify?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Yes. Copy the YAML header, chunk options, and code chunks from the article into your own project and adapt the thresholds, sample names, and sections with no restrictions."
      }
    },
    {
      "@type": "Question",
      "name": "Can I use Bioconductor packages instead of Seurat in the same template?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Yes. Convert with as.SingleCellExperiment(obj), run QC and normalization with scater and scran, and convert back with as.Seurat(sce) if needed. The report structure and the rendering workflow do not change."
      }
    },
    {
      "@type": "Question",
      "name": "How do I render one report per sample without editing the file each time?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Put the sample-specific values in the params block and call rmarkdown::render() with a params list, either once per sample or inside a purrr::walk() loop that reads sample names from a vector."
      }
    },
    {
      "@type": "Question",
      "name": "Which chunk options matter most for large single-cell datasets?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "cache = TRUE on slow chunks such as FindAllMarkers(), message = FALSE and warning = FALSE on everything, and eval = FALSE on optional branches so they print without running."
      }
    },
    {
      "@type": "Question",
      "name": "What QC thresholds should I start with?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "A common starting point is 200 to 2500 detected genes per cell and less than 5 percent mitochondrial UMIs for PBMCs, with the mitochondrial cap raised for tissues where dissociation stresses cells. Report the values you settled on and why."
      }
    },
    {
      "@type": "Question",
      "name": "Should I use LogNormalize or SCTransform in the report?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "LogNormalize is the simpler default and is fine for routine per-sample reports. SCTransform uses regularized negative binomial regression and often improves variable feature selection and downstream tests, at the cost of more memory and runtime."
      }
    },
    {
      "@type": "Question",
      "name": "How do I keep the report reproducible months later?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Set a seed in the setup chunk, end the document with sessionInfo(), and track package versions with renv so a lockfile records exactly what was installed. Keep the data directory relative to the project root."
      }
    }
  ]
}
</script>

## Sources

1. [Current best practices in single-cell RNA-seq analysis: a tutorial](https://pubmed.ncbi.nlm.nih.gov/31217225/)
2. [A Review of Single-Cell RNA-Seq Annotation, Integration, and Cell-Cell Communication](https://pubmed.ncbi.nlm.nih.gov/37566049/)
3. [Bioinformatics perspectives on transcriptomics: A comprehensive review of bulk and single-cell RNA sequencing analyses](https://pubmed.ncbi.nlm.nih.gov/41675508/)
4. [Single-cell RNA-seq data analysis on the receptor ACE2 expression reveals the potential risk of different human organs vulnerable to 2019-nCoV infection](https://pubmed.ncbi.nlm.nih.gov/32170560/)
5. [Dissecting the multicellular ecosystem of metastatic melanoma by single-cell RNA-seq](https://pubmed.ncbi.nlm.nih.gov/27124452/)
6. [Single-cell RNA-seq highlights intratumoral heterogeneity in primary glioblastoma](https://pubmed.ncbi.nlm.nih.gov/24925914/)
7. [Single-cell RNA-seq reveals cell type-specific molecular and genetic associations to lupus](https://pubmed.ncbi.nlm.nih.gov/35389781/)
8. [Review of single-cell RNA-seq data clustering for cell-type identification and characterization](https://pubmed.ncbi.nlm.nih.gov/36737104/)
9. [Solo: Doublet Identification in Single-Cell RNA-Seq via Semi-Supervised Deep Learning.](https://pubmed.ncbi.nlm.nih.gov/32592658/)
10. [Normalization and variance stabilization of single-cell RNA-seq data using regularized negative binomial regression](https://pubmed.ncbi.nlm.nih.gov/31870423/)
11. [Single-cell RNA-seq reveals new types of human blood dendritic cells, monocytes and progenitors](https://pubmed.ncbi.nlm.nih.gov/28428369/)
12. [Integration of Single-Cell RNA-Seq Datasets: A Review of Computational Methods](https://pubmed.ncbi.nlm.nih.gov/36859475/)
13. [Comparison of methods and resources for cell-cell communication inference from single-cell RNA-Seq data](https://pubmed.ncbi.nlm.nih.gov/35680885/)
14. [TRUST4: immune repertoire reconstruction from bulk and single-cell RNA-seq data](https://pubmed.ncbi.nlm.nih.gov/33986545/)
15. [Simultaneous capture of single cell RNA-seq, ATAC-seq, and CRISPR perturbation enables multiomic screens to identify gene regulatory relationships](https://pubmed.ncbi.nlm.nih.gov/41218606/)
16. [Comparison of single-cell RNA-seq methods to enable transcriptome profiling of neutrophils in clinical samples](https://pubmed.ncbi.nlm.nih.gov/40957400/)