
scSidekick - Copy Number Variation Inference
17_cnv.RmdOverview
Distinguishing malignant cells from normal stromal and immune infiltrates is a common challenge in tumor single-cell studies. scSidekick provides two complementary wrappers:
-
RunInferCNV()- full inferCNV workflow with automatic gene-order generation, downsampling, HMM-optional inference, and a Tirosh-style reference-anchored CNV score threshold. -
RunCopyKAT()- fast Bayesian CNV calling via CopyKAT; can run in unsupervised mode (no normal reference required).
Both functions add new metadata columns to the Seurat object and save
standard figures to output_dir.
When to use which
RunInferCNV() |
RunCopyKAT() |
|
|---|---|---|
| Accuracy | Higher (gene-order smoothing + denoising) | Moderate (fast Bayesian model) |
| Speed | Slow (minutes to hours) | Fast (minutes) |
| Reference required | Yes | Optional (unsupervised mode) |
| Best use | Publication-grade tumor/normal separation | Quick first pass; no normal cells available |
Run both and compare CNV_Prediction
vs. copykat_prediction for maximum confidence.
Installation
# inferCNV (Bioconductor)
if (!requireNamespace("BiocManager")) install.packages("BiocManager")
BiocManager::install("infercnv")
# CopyKAT (GitHub)
devtools::install_github("navinlabcode/copykat")1. RunInferCNV() - reference-guided CNV inference
Basic call
library(scSidekick)
library(Seurat)
# Typical tumor dataset: immune and stromal cells serve as the normal reference
obj <- RunInferCNV(obj,
cell_type_col = "Assignment", # metadata column with cell-type labels
ref_group_names = c("T cell", "B cell", "Macrophage", "Endothelial"),
output_dir = "./Figures/inferCNV",
species = "human")RunInferCNV() automatically runs
AnnotateFeatures() to generate the gene order table if the
object has not been annotated yet.
Output metadata columns
| Column | Description |
|---|---|
CNV_score |
Per-cell CNV score: mean((expr - 1)^2) across all genes
(Tirosh 2016) |
CNV_Prediction |
"Malignant" or "Normal" based on
cnv_score_threshold
|
infercnv_subcluster |
Cell subcluster assigned by inferCNV’s internal clustering |
proportion_cnv_chrN |
Fraction of chromosome N genes called as CNV-affected |
2. Tuning the malignancy threshold
The default cnv_score_threshold = "auto" anchors the
threshold to the reference cells:
threshold = mean(ref_scores) + cnv_k * sd(ref_scores)
cnv_k = 2 (default) is a good starting point. Increase
it to call fewer cells malignant (more conservative); decrease it to be
more sensitive.
# More conservative: require 3 SD above reference mean
obj <- RunInferCNV(obj,
cell_type_col = "Assignment",
ref_group_names = c("T cell", "B cell", "Macrophage"),
output_dir = "./Figures/inferCNV",
species = "human",
cnv_k = 3)Override with a fixed numeric value when you have a biological reason:
obj <- RunInferCNV(obj,
cell_type_col = "Assignment",
ref_group_names = c("T cell", "B cell"),
output_dir = "./Figures/inferCNV",
species = "human",
cnv_score_threshold = 0.02)Inspect the score distribution to choose a threshold:
hist(obj$CNV_score,
breaks = 50,
main = "CNV score distribution",
xlab = "CNV score",
col = "steelblue")
abline(v = 0.02, col = "red", lty = 2)
# Tabulate predictions
table(obj$CNV_Prediction, obj$Assignment)3. resume = TRUE - skip re-running
inferCNV can take hours on large datasets. Use
resume = TRUE after the first run to skip the inference
step and jump straight to post-processing (useful when tuning
cnv_k or cnv_score_threshold):
# First run - full inference (~2 hours)
obj <- RunInferCNV(obj,
cell_type_col = "Assignment",
ref_group_names = c("T cell", "B cell"),
output_dir = "./Figures/inferCNV",
species = "human")
# Tune threshold without re-running - post-processing only (~seconds)
obj <- RunInferCNV(obj,
cell_type_col = "Assignment",
ref_group_names = c("T cell", "B cell"),
output_dir = "./Figures/inferCNV",
species = "human",
cnv_score_threshold = 0.015,
resume = TRUE)4. HMM mode and advanced parameters
RunInferCNV(obj,
cell_type_col = "Assignment",
ref_group_names = c("T cell", "B cell"),
output_dir = "./Figures/inferCNV_HMM",
species = "human",
HMM = TRUE, # adds subclonal CNV state calls; much slower
denoise = TRUE,
downsample = 300L, # cells per group (NULL = use all)
cutoff = 0.1, # 0.1 for 10x; use 1 for Smart-seq2
num_threads = 8L,
caffeinate = TRUE) # keep laptop awake during long runLimiting chromosomes
By default all autosomes + X/Y are analyzed. Restrict to a subset when you have a prior reason (e.g., known focal amplification on chr8q in MYC-driven tumors):
RunInferCNV(obj,
cell_type_col = "Assignment",
ref_group_names = c("T cell", "B cell"),
output_dir = "./Figures/inferCNV",
species = "human",
chromosomes = as.character(c(1:8, 17)))5. RunCopyKAT() - fast Bayesian CNV calling
Reference-guided mode
obj <- RunCopyKAT(obj,
cell_type_col = "Assignment",
ref_cells = c("T cell", "B cell", "Macrophage"),
output_dir = "./Figures/CopyKAT",
n_cores = 8L)Unsupervised mode (no normal reference)
When the sample contains no confidently normal cells, omit
ref_cells:
obj <- RunCopyKAT(obj,
cell_type_col = "Assignment",
output_dir = "./Figures/CopyKAT",
n_cores = 8L)CopyKAT will infer the diploid baseline automatically.
Output metadata columns
| Column | Description |
|---|---|
copykat_prediction |
"aneuploid", "diploid", or
"not.defined"
|
copykat_score |
Sum of absolute CNA values across all genomic windows (continuous) |
"not.defined" cells are those CopyKAT could not
confidently classify - typically very sparse cells or cells at the
boundary of the two populations.
6. Combining both tools
Use both and require agreement for high-confidence malignancy calls:
# Run both (output_dir can be the same or different)
obj <- RunInferCNV(obj,
cell_type_col = "Assignment",
ref_group_names = c("T cell", "B cell"),
output_dir = "./Figures/inferCNV",
species = "human")
obj <- RunCopyKAT(obj,
cell_type_col = "Assignment",
ref_cells = c("T cell", "B cell"),
output_dir = "./Figures/CopyKAT")
# Cells called malignant/aneuploid by BOTH tools
obj$CNV_consensus <- with(obj@meta.data,
ifelse(CNV_Prediction == "Malignant" & copykat_prediction == "aneuploid",
"Malignant", "Normal/Uncertain"))
table(obj$CNV_consensus, obj$Assignment)Visualize agreement:
library(ggplot2)
ggplot(obj@meta.data, aes(x = CNV_score, y = copykat_score,
color = CNV_consensus)) +
geom_point(size = 0.5, alpha = 0.6) +
scale_color_manual(values = c(Malignant = "#ab3000",
"Normal/Uncertain" = "#2f4b7c")) +
labs(x = "inferCNV score", y = "CopyKAT score",
title = "CNV score concordance") +
theme_classic()See also
-
AnnotateFeatures()/GetGeneOrder()- gene-order annotation used internally byRunInferCNV()(vignette 01) -
PlotDimPlots()- visualizeCNV_Predictionon the UMAP after inference (vignette 03) -
RunWilcoxAUC()- find markers distinguishing malignant from normal clusters after labeling
Session info
## R version 4.3.1 (2023-06-16)
## Platform: aarch64-apple-darwin20 (64-bit)
## Running under: macOS 15.6
##
## Matrix products: default
## BLAS: /Library/Frameworks/R.framework/Versions/4.3-arm64/Resources/lib/libRblas.0.dylib
## LAPACK: /Library/Frameworks/R.framework/Versions/4.3-arm64/Resources/lib/libRlapack.dylib; LAPACK version 3.11.0
##
## locale:
## [1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
##
## time zone: America/Chicago
## tzcode source: internal
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## loaded via a namespace (and not attached):
## [1] digest_0.6.37 desc_1.4.3 R6_2.6.1 fastmap_1.2.0
## [5] xfun_0.52 cachem_1.1.0 knitr_1.45 htmltools_0.5.8.1
## [9] rmarkdown_2.29 lifecycle_1.0.4 cli_3.6.5 sass_0.4.10
## [13] pkgdown_2.1.3 textshaping_0.3.7 jquerylib_0.1.4 systemfonts_1.3.2
## [17] compiler_4.3.1 rstudioapi_0.16.0 tools_4.3.1 ragg_1.4.0
## [21] bslib_0.9.0 evaluate_0.23 yaml_2.3.10 jsonlite_2.0.0
## [25] rlang_1.1.6 fs_1.6.6 htmlwidgets_1.6.4