| name | bio-applied-differential-binding |
| description | Find ChIP-seq/ATAC-seq peaks that gain or lose signal between conditions using DiffBind (dba.count/dba.normalize/dba.analyze with DESeq2 or edgeR) on a consensus peak set, then annotate hits to genes/promoters with ChIPseeker annotatePeak. Use when comparing TF binding or histone marks across conditions, building volcano/MA plots from dba.report() output, or annotating differential peaks to nearest TSS/gene. |
| tool_type | r |
| primary_tool | DiffBind |
Differential Binding & Peak Annotation
When to Use
- Comparing ChIP-seq or ATAC-seq peak signal between two or more conditions (treatment vs. control, knockdown vs. WT) with replicates
- Building a consensus peak set from multiple MACS2 narrowPeak/broadPeak calls before quantitative comparison
- Running DESeq2- or edgeR-based differential testing on peak read counts (same statistical machinery as RNA-seq, applied to genomic intervals)
- Annotating differential (or all) peaks to genomic features — promoter, exon, intron, distal intergenic — and to nearest gene/TSS
- Generating volcano/MA plots and PCA of binding affinity to QC replicate consistency before biological interpretation
Version Compatibility
- R ≥ 4.3, Bioconductor ≥ 3.18
- DiffBind ≥ 3.12 (wraps DESeq2 ≥ 1.42 or edgeR ≥ 4.0)
- ChIPseeker ≥ 1.38, with a matching
TxDb.* package (e.g. TxDb.Hsapiens.UCSC.hg38.knownGene) and org.Hs.eg.db
- Python ≥ 3.10, pandas ≥ 2.0, matplotlib ≥ 3.8 (for plotting exported results only — DiffBind/ChIPseeker have no Python equivalent)
Prerequisites
- R packages:
BiocManager::install(c("DiffBind", "ChIPseeker", "clusterProfiler", "org.Hs.eg.db", "TxDb.Hsapiens.UCSC.hg38.knownGene"))
- Deduplicated, indexed BAMs per sample plus matching Input/IgG control BAMs
- Peak calls per sample (MACS2 narrowPeak/broadPeak) — see
bio-chip-seq-peak-calling
- Familiarity with DESeq2-style count-based differential testing (
bio-differential-expression-deseq2-basics)
Differential Binding with DiffBind
Goal: identify peaks whose read counts differ significantly between two conditions, correcting for library size and using replicate information.
Approach: build a sample sheet, count reads across a consensus peak set (peaks reproducible in ≥2 samples), normalize, then run DESeq2 (or edgeR) via dba.analyze().
Sample sheet (samplesheet.csv) — one row per sample:
SampleID,Condition,Replicate,bamReads,ControlID,bamControl,Peaks,PeakCaller
CTCF_Ctrl1,Control,1,dedup/ctcf_ctrl1.bam,Input1,dedup/input1.bam,peaks/ctcf_ctrl1_peaks.narrowPeak,narrow
CTCF_Ctrl2,Control,2,dedup/ctcf_ctrl2.bam,Input2,dedup/input2.bam,peaks/ctcf_ctrl2_peaks.narrowPeak,narrow
CTCF_Trt1,Treatment,1,dedup/ctcf_trt1.bam,Input3,dedup/input3.bam,peaks/ctcf_trt1_peaks.narrowPeak,narrow
CTCF_Trt2,Treatment,2,dedup/ctcf_trt2.bam,Input4,dedup/input4.bam,peaks/ctcf_trt2_peaks.narrowPeak,narrow
library(DiffBind)
dba_obj <- dba(sampleSheet = "samplesheet.csv")
print(dba_obj)
dba_obj <- dba.count(dba_obj, bUseSummarizeOverlaps = TRUE, minOverlap = 2)
dba_obj <- dba.normalize(dba_obj, normalize = DBA_NORM_RLE)
dba_obj <- dba.contrast(dba_obj, categories = DBA_CONDITION, minMembers = 2)
dba_obj <- dba.analyze(dba_obj, method = DBA_DESEQ2)
db_peaks <- dba.report(dba_obj, th = fold log2
printdb_peaks
dba.plotPCAdba_obj DBA_CONDITION label DBA_ID
dba.plotVolcanodba_obj
rtracklayerexportdb_peaks
write.csvas.data.framedb_peaks row.names
Peak Annotation with ChIPseeker
Goal: map each (differential) peak to the nearest gene/TSS and a genomic feature category, for downstream GO/KEGG enrichment.
Approach: annotatePeak() against a TxDb for the genome build; tssRegion sets the promoter window; annoDb adds gene symbols/Entrez IDs.
library(ChIPseeker)
library(TxDb.Hsapiens.UCSC.hg38.knownGene)
library(org.Hs.eg.db)
txdb <- TxDb.Hsapiens.UCSC.hg38.knownGene
peaks <- readPeakFile("diffbind_results.bed", as = "GRanges")
anno <- annotatePeak(
peaks,
tssRegion = c(-2000, 200),
TxDb = txdb,
annoDb = "org.Hs.eg.db"
)
plotAnnoPie(anno)
plotAnnoBar(anno)
plotDistToTSS(anno, title = "Distribution of peaks relative to TSS")
anno_df <- as.data.frame(anno)
head(anno_df[, c("seqnames",
promoter_peaks anno_dfanno_dfdistanceToTSS
cat nrowpromoter_peaks
write.csvanno_df row.names
Plotting DiffBind Results in Python
Goal: rebuild volcano and MA plots from an exported diffbind_results.csv without needing R installed on the plotting machine.
Approach: read the DESeq2-style columns (log2FoldChange/log2FC, padj/FDR) and classify by direction before plotting.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
def classify_binding(df, fdr_col="FDR", lfc_col="log2FC", fdr_thresh=0.05, lfc_thresh=1.5):
"""Label each peak as Gained/Lost/Unchanged from DiffBind dba.report() output.
df must have numeric fdr_col and lfc_col columns (e.g. loaded from
diffbind_results.csv). Returns df with 'Significant' and 'Direction' added.
"""
df = df.copy()
df["Significant"] = (df[fdr_col] < fdr_thresh) & (df[lfc_col].abs() > np.log2(lfc_thresh))
df["Direction"] = "Unchanged"
df.loc[df["Significant"] & (df[lfc_col] > 0), "Direction"] = "Gained"
df.loc[df["Significant"] & (df[lfc_col] < 0), "Direction"] = "Lost"
return df
def plot_volcano(df, lfc_col="log2FC", fdr_col="FDR", fdr_thresh=0.05, lfc_thresh=1.5, ax=None):
"""Volcano plot of differential binding results, colored by direction."""
ax = ax or plt.gca()
colors = {"Gained": "coral", "Lost": "steelblue", "Unchanged": "lightgray"}
neg_log10_fdr = -np.log10(df[fdr_col].clip(lower=1e-50))
for direction, grp df.groupby():
idx = grp.index
ax.scatter(grp[lfc_col], neg_log10_fdr.loc[idx], c=colors[direction],
s= direction != ,
alpha= direction != ,
label=)
ax.axhline(-np.log10(fdr_thresh), color=, ls=, lw=, label=)
ax.axvline(np.log2(lfc_thresh), color=, ls=, lw=)
ax.axvline(-np.log2(lfc_thresh), color=, ls=, lw=)
ax.set_xlabel()
ax.set_ylabel()
ax.legend(markerscale=)
ax
Pitfalls
- Coordinate systems: BED is 0-based half-open; VCF/GFF/GRanges are 1-based inclusive — mixing them causes off-by-one errors when exporting DiffBind results and re-importing to ChIPseeker
- Consensus peak definition:
minOverlap/minMembers defaults to peaks in ≥2 samples; too permissive inflates the peak set with singleton noise, too strict drops condition-specific real sites
- Normalization choice: for TF ChIP-seq use RLE/TMM (library-size driven); for broad marks with global signal shifts (e.g. total H3K27ac loss), consider spike-in or background-region normalization instead — default normalization can mask true global change
- Multiple testing: thousands of peaks are tested simultaneously — always use
th (FDR/BH), never raw p-values, to call significance
- Batch effects: check
dba.plotPCA() for replicate clustering by condition (not by batch/day) before trusting dba.report() output
- Promoter window choice: ChIPseeker's
tssRegion default (-3000, 3000) is looser than the common (-2000, 200); mismatched windows between analyses make "% promoter peaks" non-comparable across papers
See Also
bio-chip-seq-peak-calling — generating the narrowPeak/broadPeak inputs DiffBind requires
bio-chip-seq-peak-annotation — deeper ChIPseeker/annotation workflows
bio-differential-expression-deseq2-basics — the DESeq2 statistics DiffBind reuses under the hood
bio-pathway-analysis-go-enrichment — clusterProfiler enrichment on annotated peak gene lists