Sanity check · ChIP-seq
How to Choose a Normalization Method in ChIP-seq
RPKM, CPM, and median-of-ratios all assume nothing changed genome-wide, an assumption that quietly kills real histone-mark shifts before you ever see them.
By Ming "Tommy" Tang, Director of Bioinformatics in Big Pharma · Reviewed September 2026 · 5 min read
You've called peaks, built input-subtracted bigWigs, and now you're staring at a --normalizeUsing flag in bamCoverage or a size factor column from DESeq2, picking whichever the tutorial used last time. For RNA-seq that habit is usually harmless. For ChIP-seq it can silently erase the exact effect you set out to measure.
Library-size and median-of-ratios normalization (CPM, RPKM, TMM, DESeq2's default) all lean on one assumption: most of the signal doesn't change between samples. RNA-seq mostly gets away with that. ChIP-seq doesn't always, because a drug that wipes out a histone mark genome-wide, or a factor that loses most of its binding, violates the assumption directly, and standard normalization responds by scaling the shrunken signal back up until it looks unchanged.
This page gives you the checks to catch that before you write up a null result that's actually a normalization artifact, and a decision framework for which method, library-size, input-subtracted, or spike-in, matches the question your experiment is actually asking.
What it looks like when it's happening
- bigWig tracks built with RPKM, CPM, and RPGC look essentially identical in IGV even at a locus where independent evidence says the mark should have changed
- DESeq2 finds few or no significant differential peaks after a treatment (e.g. EZH2 inhibitor) that a Western blot confirms wiped out most of the mark genome-wide
- MA plot for the comparison is tightly centered on zero across the whole dynamic range, not just at background bins
- Size factors from estimateSizeFactors() cluster near 1.0 across every sample regardless of condition
- Cross-sample correlation heatmap of normalized bigWigs clusters samples by library prep batch instead of by treatment
- Tissue ChIP-seq replicates from the same condition show poor correlation while different conditions look artificially similar after CPM normalization
- FRiP and duplicate rate differ noticeably between replicates, but every downstream plot looks scaled to match anyway
Why it happens
DESeq2's median-of-ratios and edgeR's TMM were built for RNA-seq, where the working assumption is that most genes don't change between conditions, so the bulk of the distribution can anchor the scaling factor. Apply the same logic to ChIP-seq counts over peaks or genomic bins and you're assuming most of the genome's binding or histone signal doesn't change either. That's reasonable when you're comparing binding-site redistribution of the same factor, or a focal mark. It fails outright for global changes: an EZH2 inhibitor that erases H3K27me3 genome-wide, or a chromatin regulator knockout that broadly redistributes an activating mark. Feed those counts into estimateSizeFactors() and the algorithm reads the flattened signal as "nothing changed" and scales the treated sample back up to match the control, deleting the effect before differential testing even runs.
There's a technical reinforcement on top of the statistical one. Standard library prep normalizes cluster density across multiplexed samples before sequencing, which equalizes total read output regardless of how much real ChIP signal each sample started with. By the time you count reads, the sequencer has already erased part of the pre-library difference in mark abundance. Library-size methods, RPKM, CPM, BPM (deepTools' TPM-equivalent), RPGC, only rescale by depth and, for RPKM/BPM, region length. None of them can recover a genome-wide difference that was flattened before the reads were even generated.
Spike-in normalization works around both problems by adding a fixed, known amount of exogenous chromatin, commonly Drosophila chromatin pulled down with a species-specific antibody, to every sample before IP. Because the spike-in is external to the biology you're perturbing, its recovery rate is insensitive to whatever global shift you're inducing in the endogenous signal. The ratio of endogenous to spike-in reads becomes the true scaling factor, immune to both the median-of-ratios assumption and the library-prep equalization problem.
The last failure mode is mixing normalization purposes rather than choosing the wrong one outright. RPKM or CPM bigWigs are fine for eyeballing tracks in IGV, deepTools users report the different methods look nearly identical there, because a single-sample visual scan usually isn't testing the assumption that's failing. But feeding visualization-normalized values, or a size factor tuned for peak calling against input, into a DESeq2 differential model is a different task with different requirements. Input-normalized peak calling controls for shearing and amplification bias per sample; DESeq2's size factors control for library composition across samples for statistical testing. Treating one as a substitute for the other is where "it looked fine in IGV" turns into a differential result nobody can reproduce.
The checks
Run them in order. Each one tells you what healthy looks like and what the problem looks like.
0/7 checked · saved in this browser
Before choosing any normalization method, compute the fraction of reads in peaks (FRiP) and the PCR duplicate rate for each ChIP and input sample from the raw BAM files. Any normalization method downstream only corrects for scale; it can't fix an IP that didn't work.
- Healthy
- FRiP and duplicate rates are in a similar range across replicates of the same condition (transcription factors typically show higher FRiP than broad histone marks, but replicates should track each other).
- Red flag
- One or two samples have markedly lower FRiP or much higher duplicate rate than their replicates. Any normalization applied afterward will scale a weak IP up to look comparable to a strong one.
Tabulate total mapped reads (post-filtering) per BAM and cross-tabulate against experimental condition and batch.
- Healthy
- Depth is roughly balanced across conditions, or at least not confounded with the biological variable of interest.
- Red flag
- Depth differs by 2x or more and tracks with condition (e.g. all treatment samples sequenced shallower than all controls). This alone can manufacture false differential peaks independent of which normalization method you pick.
Generate bigWigs with RPGC, CPM, and RPKM for the same BAM and load them in IGV at a locus where you know the biology (a strong positive-control peak, or a canonical target such as ER binding at TFF1/GREB1 for a transcription-factor ChIP).
bashbamCoverage -b sample.bam -o sample_RPGC.bw --normalizeUsing RPGC --effectiveGenomeSize 2864785049 bamCoverage -b sample.bam -o sample_CPM.bw --normalizeUsing CPM bamCoverage -b sample.bam -o sample_RPKM.bw --normalizeUsing RPKM- Healthy
- The three tracks look essentially the same at that locus. deepTools' own issue tracker reports no visible difference between RPGC, RPKM, CPM, and BPM in IGV when there's no genome-wide shift to hide.
- Red flag
- The tracks agree with each other but none shows the expected signal change between conditions. That agreement isn't reassurance; it means none of these library-size methods can see a global shift even if one is real.
After building the DESeqDataSet from ChIP-seq peak or bin counts, run estimateSizeFactors() and print the size factors next to the experimental condition for each sample.
rdds <- estimateSizeFactors(dds) sizeFactors(dds)- Healthy
- Size factors vary in a way that's plausible given known sequencing-depth differences and roughly track library size.
- Red flag
- Size factors cluster tightly near 1.0 across a condition where you have independent evidence (Western blot, mass spec, qPCR) that the mark or factor changed genome-wide. Median-of-ratios assumes most regions are unchanged, so a real global shift gets normalized away instead of detected.
Bin the genome (excluding blacklisted and peak regions) and compute the log relative risk (log ratio) between each pair of samples per bin, then plot the empirical density of these background-bin log ratios.
- Healthy
- The peak of the background density lines up with the log of the normalization constant you're using (size factor or scaling factor).
- Red flag
- The density peak is shifted away from the assumed normalization constant, meaning the constant fed into peak calling doesn't match the actual background distribution, biasing effect sizes downstream.
If you spiked in exogenous chromatin (e.g. Drosophila chromatin pulled down with a species-specific antibody), compute size factors from the spike-in features alone via controlGenes, center them, and compare against the default whole-genome size factors from the DESeq2 check above.
rdds <- estimateSizeFactors(dds, controlGenes = spike_in_features) sizeFactors(dds) <- sizeFactors(dds) / exp(mean(log(sizeFactors(dds))))- Healthy
- The two sets of factors are close when there's no global shift, and diverge in the predicted direction when there is one.
- Red flag
- Whole-genome and spike-in size factors disagree substantially. That gap is the global shift library-size normalization is erasing. Use all spike-in features together; a single spike-in is too high-variance to anchor an estimate on.
For tissue ChIP-seq where chromatin input amount varies naturally between biological specimens, plot the quantified pre-IP chromatin/input DNA amount for each sample against its computed size factor.
- Healthy
- Size factors track input chromatin amount in a sensible, monotonic way.
- Red flag
- Size factors show no relationship to actual input amount, meaning plain CPM or equal-read normalization is masking real input-driven variation. Input-adjusted spike-in normalization has been shown to give tighter sample correlation and fewer false positives here than CPM or equal-read scaling.
What to do about it
Use library-size or SES normalization for standard sharp-peak comparisons
When: You're comparing a transcription factor or a sharply defined mark like H3K4me3, with no independent evidence of a genome-wide gain or loss between conditions.
Generate bigWigs with bamCoverage --normalizeUsing RPGC --effectiveGenomeSize <value> (or CPM) for visualization, and use bamCompare --normalizeUsing SES against matched input for peak calling. deepTools recommends SES specifically when ChIP and input are clearly distinguishable, which holds for TFs and sharp marks.
Caveat: SES and library-size methods assume no global shift. If that assumption is wrong, this fix reproduces the exact masking problem described above.
Add spike-in chromatin when a genome-wide change is plausible
When: The experiment perturbs a chromatin regulator, writer, or eraser (EZH2 inhibitor, HDAC inhibitor, a broad chromatin-machinery knockout) where a genome-wide gain or loss of the mark is a real possible outcome, not just background noise.
Spike in a fixed amount of exogenous chromatin (e.g. Drosophila chromatin with a species-specific antibody) into every sample before IP, at the bench, before sequencing. Normalize by the ratio of endogenous to spike-in reads instead of library size.
Caveat: This has to be planned before the wet-lab step. There's no way to retroactively add a spike-in to already-sequenced libraries; for existing data you're limited to the diagnostic checks above, not this fix.
Use input-adjusted spike-in normalization for tissue ChIP-seq
When: Samples come from tissue rather than a controlled cell line, where the amount of starting chromatin naturally varies between biological specimens.
Combine spike-in normalization with per-sample input chromatin quantification, rather than plain CPM or equal-read scaling. This has shown tighter sample correlation and fewer false positives than CPM or equal-read normalization specifically for tissue ChIP-seq.
Caveat: Requires accurate chromatin/input quantification (qPCR or fluorometric measurement) at the bench for every sample, more wet-lab bookkeeping than a purely computational fix.
Anchor DESeq2 size factors to spike-in controls, not the whole genome
When: You have spike-in reads available and are running DESeq2 for differential peak or bin calling.
Call estimateSizeFactors(dds, controlGenes = spike_in_features) using all spike-in features together (not a single one), then center the factors with sizeFactors(dds) <- sizeFactors(dds) / exp(mean(log(sizeFactors(dds)))) before running the rest of the DESeq2 workflow.
Caveat: A single spike-in feature is a high-variance estimator. Bioconductor guidance is to use all spike-ins together, not just one.
Keep visualization normalization and statistical normalization separate
When: You're building bigWigs for browser tracks or heatmaps and also running a separate differential test (DESeq2, edgeR) on the same data.
Pick any of RPKM/CPM/BPM/RPGC for the bigWigs you'll look at in IGV or deepTools heatmaps, they're practically indistinguishable there. Run the statistical normalization (DESeq2 size factors, with or without spike-in controlGenes) independently on the count matrix feeding the differential model.
Caveat: Don't derive your DE fold-changes from the same normalized bigWig values you used for the picture; they answer different questions and were built on different assumptions.
When not to "fix" it
If you have no biological reason to expect a genome-wide gain or loss, you're asking where a transcription factor redistributes its binding, not whether its total occupancy changed, then median-of-ratios or library-size normalization behaving as if "most things are unchanged" is the correct assumption, not a bug to engineer around. Retrofitting spike-in-style corrections onto that kind of comparison adds noise and complexity without a real global-shift artifact to correct for. Similarly, if you're only ever going to eyeball single-sample tracks in IGV and never run a cross-sample statistical test, the extra machinery of spike-in normalization and controlGenes size factors is more setup than the question needs.
Five things experienced analysts do here
- Decide whether you expect a global shift before you sequence, not after: spike-in chromatin has to go in at the bench, and there's no computational fix that recovers it retroactively.
- Treat a size-factor table that clusters tightly near 1.0 across a condition with known biological perturbation as suspicious, not reassuring; check it against independent evidence (Western blot, mass spec) before trusting it.
- Never let a bigWig normalized for IGV viewing double as the input to a differential test; visualization and statistical normalization solve different problems even when the deepTools flags look interchangeable.
- If you're using spike-in controls in DESeq2, pass all spike-in features to controlGenes and re-center the size factors; a single spike-in is too noisy to anchor a whole experiment on.
- Before blaming (or trusting) a normalization method, rule out plain sequencing-depth imbalance confounded with condition; it manufactures the same false-positive and false-negative patterns independent of which normalization formula you picked.
Questions people ask
- Should I use CPM or RPKM for ChIP-seq bigWig tracks?
For visualization alone it barely matters, deepTools users report no visible difference between RPGC, RPKM, CPM, and BPM tracks in IGV. Neither one corrects for a genome-wide shift in signal, so don't reach for either when you're trying to detect or rule out a global change; that's a job for spike-in normalization.
- Can I just run DESeq2 on ChIP-seq counts the same way I run it on RNA-seq?
The negative binomial model and differential testing machinery carry over fine. The default median-of-ratios size factor calculation is the part that can fail, because it assumes most regions don't change, true for most RNA-seq experiments, not guaranteed for ChIP-seq with a global mark change. If you have spike-in controls, pass them via controlGenes in estimateSizeFactors() instead of relying on the default.
- What is RPGC normalization in deepTools and when should I use it?
RPGC (reads per genomic content) scales coverage to 1x genome depth using an effective genome size you supply (roughly 2.86 billion for mouse, 2.65 billion for human). It's deepTools' recommended option for ChIP-seq bigWigs when you're not trying to detect a genome-wide shift; it standardizes coverage across samples sequenced to different depths.
- Do I need spike-in controls for every ChIP-seq experiment?
No. Spike-in is worth the extra bench work when your experiment could plausibly cause a genome-wide gain or loss of the mark (a chromatin enzyme inhibitor or knockout), or when chromatin input amount varies naturally across samples, as in tissue ChIP-seq. For comparisons where total occupancy isn't expected to change, standard library-size or input normalization is fine.
- Why do my normalized ChIP-seq tracks look identical across conditions even though a Western blot shows the mark changed?
Two things compound this: library prep equalizes cluster density across multiplexed samples before sequencing, which already flattens part of the real difference, and then median-of-ratios or library-size normalization assumes most signal is unchanged and rescales what's left to match. The fix is spike-in normalization added at the chromatin prep step, not a different flag on the same library-size calculation.
Related pages
- Guide · How to Call Peaks You Can Trust in ChIP-seq
- Glossary · Count matrix
- Glossary · Heatmap
- Glossary · Negative binomial model
Related reading on the blog
Sources
- bamCoverage, deepTools 3.5.6 documentation — Five --normalizeUsing options (RPKM, CPM, BPM, RPGC, None) and RPGC's effective-genome-size requirement.
- Normalizations · deeptools/deepTools Wiki — bamCompare's SES normalization recommendation for transcription factors and sharp marks like H3K4me3.
- Choose proper normalization method making bigWig files · Issue #1040 · deeptools/deepTools — User reports of near-identical bigWig appearance in IGV across RPGC, RPKM, CPM, and BPM.
- An Alternative Approach to ChIP-Seq Normalization Enables Detection of Genome-Wide Changes in Histone H3 Lysine 27 Trimethylation upon EZH2 Inhibition — How library-size normalization masks genome-wide H3K27me3 loss after EZH2 inhibition, and why spike-in normalization recovers it.
- Appropriate normalization is critical to improve reproducibility of tissue ChIP-seq — Input-adjusted spike-in normalization outperforming CPM and equal-read normalization for tissue ChIP-seq with variable chromatin input.
- Is this the right normalization? A diagnostic tool for ChIP-seq normalization — Diagnostic density-plot method for checking whether a ChIP-seq normalization constant matches the background bin distribution.
- normalization of ChIP-seq data by using the spike-ins or by using total library sizes — Using controlGenes with spike-in features in DESeq2's estimateSizeFactors(), and centering size factors from multiple spike-ins.
- Normalization in DESeq2 — DESeq2's median-of-ratios normalization and its assumptions.
Part of the Normalization choice series.