Skip to contents

Overview

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

Output figures

Three PDFs are saved to output_dir:

  • CNV_proportion_heatmap.pdf - ComplexHeatmap of per-chromosome CNV proportions, split by cell type.
  • CNV_dimplot.pdf - UMAP colored by CNV_Prediction (left) and cell type (right).
  • CNV_score_featureplot.pdf - Continuous CNV_score overlaid on the UMAP.

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 run

Limiting 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


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