CNVkit Copy Number Analysis
Overview
CNVkit detects somatic copy number variants (CNVs) from whole-exome sequencing (WES), whole-genome sequencing (WGS), or targeted panel BAM files. It calculates read depth in both on-target (capture) bins and off-target (antitarget) bins, corrects for GC bias and library depth, segments the log2 copy ratio profile with circular binary segmentation (CBS) or a hidden Markov model (HMM), and calls amplifications and deletions. CNVkit provides both a CLI (cnvkit.py) and a Python API (cnvlib) for integration into analysis pipelines, and produces scatter plots, chromosome diagrams, heatmaps, and export files in VCF, BED, and SEG formats.
When to Use
- Calling somatic copy number variants from tumor-normal paired exome (WES) or targeted panel sequencing
- Detecting copy number alterations in tumor-only samples using a pooled normal reference
- Running CNV analysis on whole-genome sequencing (WGS) data with the
--method wgsmode - Estimating tumor purity and ploidy for samples where purity is unknown, to interpret copy ratio calls
- Generating SEG format copy number files for GISTIC2, cBioPortal, or IGV visualization
- Identifying focal amplifications (e.g., ERBB2, MYC) or homozygous deletions (e.g., CDKN2A, RB1)
- Use GATK CNV (
gatk DenoiseReadCounts/gatk ModelSegments) instead for deep WGS cohorts with large matched panel-of-normals (PoN); CNVkit is better suited for targeted/exome data - Use Control-FREEC instead when you need allele-frequency-based B-allele fraction modeling alongside CNV calling
Prerequisites
- Software: CNVkit v0.9.x, Python 3.8+, R (for CBS segmentation), samtools
- Python packages:
cnvlib(installed as part of CNVkit),matplotlib,pandas - Input files: sorted, indexed BAM files (tumor ± matched normal); BED file of capture targets; reference genome FASTA; access to R with DNAcopy package for CBS
- Data requirements: minimum ~50× mean target coverage for WES; WGS works at 20-30×
Check before installing: The tool may already be available in the current environment (e.g., inside a
pixi/condaenv). Runcommand -v cnvkit.pyfirst and skip the install commands below if it returns a path. When running inside a pixi project, invoke the tool viapixi run cnvkit.pyrather than barecnvkit.py.
# Install CNVkit via conda (recommended — handles R/DNAcopy dependency)
conda install -c bioconda cnvkit
# Or via pip (requires R + DNAcopy already installed)
pip install cnvkit
# Verify
cnvkit.py version
# cnvkit 0.9.10
# Install R DNAcopy (for CBS segmentation)
Rscript -e 'if (!requireNamespace("BiocManager")) install.packages("BiocManager"); BiocManager::install("DNAcopy")'
# Index BAM files if not already indexed
samtools index tumor.bam
samtools index normal.bam
Quick Start
# One-command paired tumor/normal CNV analysis (WES)
cnvkit.py batch tumor.bam \
--normal normal.bam \
--targets targets.bed \
--fasta GRCh38.fa \
--output-dir cnvkit_results/ \
--diagram --scatter \
--method hybrid
# Output files in cnvkit_results/:
# tumor.targetcoverage.cnn — target bin coverage
# tumor.antitargetcoverage.cnn — antitarget coverage
# tumor.cnr — copy number ratios
# tumor.cns — segmented copy numbers
# tumor-scatter.png — genome-wide scatter plot
# tumor-diagram.pdf — chromosome diagram
echo "CNV analysis complete"
Workflow
Step 1: Create Copy Number Reference
Build a reference from one or more normal BAM files. This corrects for systematic biases (GC content, mappability) and sets the neutral baseline.
# Option A: Paired normal reference (single matched normal)
cnvkit.py reference normal.targetcoverage.cnn normal.antitargetcoverage.cnn \
--fasta GRCh38.fa \
-o reference_normal.cnn
# Option B: Flat reference (no normal; uses GC/mappability correction only)
# Use when no matched normal is available
cnvkit.py reference \
--targets targets.bed \
--fasta GRCh38.fa \
--output flat_reference.cnn
# Option C: Pooled normal reference from multiple normals (most robust)
cnvkit.py batch \
normal1.bam normal2.bam normal3.bam \
--normal \
--targets targets.bed \
--fasta GRCh38.fa \
--output-reference pooled_reference.cnn \
--output-dir normals_cov/
echo "Reference created: pooled_reference.cnn"
Step 2: Calculate Coverage in Target and Antitarget Bins
Bin the target BED file and compute per-bin read depth for tumor and normal samples.
# First, create accessible bins from the target BED
cnvkit.py target targets.bed \
--annotate refFlat.txt \
--split \
-o targets.split.bed
cnvkit.py antitarget targets.bed \
--access data/access-5k-mappable.hg38.bed \
-o antitargets.bed
# Calculate coverage for tumor sample
cnvkit.py coverage tumor.bam targets.split.bed \
-o tumor.targetcoverage.cnn
cnvkit.py coverage tumor.bam antitargets.bed \
-o tumor.antitargetcoverage.cnn
echo "Coverage files:"
echo " tumor.targetcoverage.cnn"
echo " tumor.antitargetcoverage.cnn"
# Python API equivalent: compute coverage with cnvlib
import cnvlib
# Load and inspect coverage files
target_cov = cnvlib.read("tumor.targetcoverage.cnn")
antitarget_cov = cnvlib.read("tumor.antitargetcoverage.cnn")
print(f"Target bins: {len(target_cov)}")
print(f"Antitarget bins: {len(antitarget_cov)}")
print(f"Mean target depth: {target_cov['depth'].mean():.1f}×")
print(f"Median target depth: {target_cov['depth'].median():.1f}×")
# Check coverage distribution
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(8, 4))
target_cov["depth"].clip(upper=500).hist(bins=50, ax=ax, color="#2c6fad", alpha=0.7)
ax.set_xlabel("Read depth")
ax.set_ylabel("Bin count")
ax.set_title("Target bin coverage distribution")
plt.tight_layout()
plt.savefig("coverage_distribution.png", dpi=150)
print("Saved: coverage_distribution.png")
Step 3: Normalize and Correct Copy Ratios
Normalize tumor coverage against the reference (correcting for GC bias, library depth, and target efficiency).
# Fix sample-level biases relative to the reference
cnvkit.py fix \
tumor.targetcoverage.cnn \
tumor.antitargetcoverage.cnn \
pooled_reference.cnn \
-o tumor.cnr
echo "Copy number ratios: tumor.cnr"
# tumor.cnr columns: chromosome, start, end, gene, log2, depth, weight
# Inspect copy ratio file with cnvlib Python API
import cnvlib
import pandas as pd
cnr = cnvlib.read("tumor.cnr")
print(f"Total bins: {len(cnr)}")
print(f"Chromosomes: {sorted(cnr.chromosome.unique())}")
# Convert to DataFrame and inspect
df = cnr.data
print(f"\nLog2 copy ratio summary:")
print(df["log2"].describe().round(3))
# Flag high-amplitude events
high_amp = df[df["log2"] >= 2.0]
hom_del = df[df["log2"] <= -3.0]
print(f"\nHigh amplitude bins (log2 >= 2.0): {len(high_amp)}")
print(f"Homozygous deletion bins (log2 <= -3.0): {len(hom_del)}")
if not high_amp.empty:
print(high_amp[["chromosome", "start", "end", "gene", "log2"]].head())
Step 4: Segment Copy Number Ratios
Identify contiguous regions of similar copy ratio using CBS (Circular Binary Segmentation) or HMM segmentation.
# CBS segmentation (default; requires R DNAcopy)
cnvkit.py segment tumor.cnr \
-o tumor.cns \
--method cbs
# HMM segmentation (no R required; faster)
cnvkit.py segment tumor.cnr \
-o tumor.hmm.cns \
--method hmm
echo "Segments: tumor.cns"
# tumor.cns columns: chromosome, start, end, gene, log2, cn, depth, p_ttest, weight, probes
# Python API: segment and inspect
import cnvlib
import subprocess
# Run segmentation via Python subprocess (mirrors CLI)
subprocess.run(
["cnvkit.py", "segment", "tumor.cnr", "-o", "tumor.cns", "--method", "cbs"],
check=True
)
# Load and analyze segments
cns = cnvlib.read("tumor.cns")
df_seg = cns.data
pr