Imports Bismark coverage or cytosine-report files into the methylKit object model, then runs the import-to-results spine - filterByCoverage, normalizeCoverage, unite/destrand, calculateDiffMeth, getMethylDiff - for both per-CpG (DMC) and fixed-tile (DMR) differential methylation, plus tileMethylCounts, PCA/correlation/clustering QC, and assocComp/removeComp batch handling. Covers the silent default traps that shape the false-positive rate: overdispersion='none' does no correction while 'MN' f...
Scanned 9/5/2026
Install to Claude Code
npx -y skills add FridrichMethod/awesome-skills --skill methylkit-analysis --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Methylkit Analysis?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/fridrichmethod-methylkit-analysis)More formats (shields.io, HTML) on the badges page.
---
name: bio-methylation-methylkit
description: "Imports Bismark coverage or cytosine-report files into the methylKit object model, then runs the import-to-results spine - filterByCoverage, normalizeCoverage, unite/destrand, calculateDiffMeth, getMethylDiff - for both per-CpG (DMC) and fixed-tile (DMR) differential methylation, plus tileMethylCounts, PCA/correlation/clustering QC, and assocComp/removeComp batch handling. Covers the silent default traps that shape the false-positive rate: overdispersion='none' does no correction while 'MN' forces the F-test (ignoring test='Chisq'), adjust defaults to SLIM not BH, getMethylDiff defaults difference=25/qvalue=0.01, cov.bases=0 admits single-CpG tiles, and pool destroys biological replication. Use when importing bisulfite count tables, filtering/normalizing/uniting methylation samples, running methylKit differential testing, or QC-ing methylomes. For per-site test-choice (count vs continuous) see differential-cpg-testing; for selection-aware region FDR (dmrseq/DSS) see dmr-detection."
tool_type: r
primary_tool: methylKit
---
## Version Compatibility
Reference examples tested with: methylKit 1.28+, GenomicRanges 1.54+.
Before using code patterns, verify installed versions match. If versions differ:
- R: `packageVersion('<pkg>')` then `?function_name` to verify parameters
If code throws ImportError, AttributeError, or TypeError, introspect the installed
package and adapt the example to match the actual API rather than retrying.
The `assembly=` string (e.g. `hg38`) is metadata only - methylKit never checks it. The genome build is real elsewhere: coordinates must match the alignment genome, and annotation packages (`TxDb.Hsapiens.UCSC.hg38.knownGene`, annotatr `build_annotations(genome='hg38')`) are genome-build-specific. methylKit's `overdispersion`, `test`, and `adjust` defaults have shifted across Bioconductor releases - run `?calculateDiffMeth` on the installed build before trusting any default.
# methylKit Analysis
**"Analyze methylation across my samples"** -> Import per-cytosine counts into a methylRawList, then filter, normalize, unite, and test - because each of those steps is a modeling decision that sets which CpGs survive and how many false positives the test emits, not boilerplate.
- R: `methRead(pipeline='bismarkCoverage')` -> `filterByCoverage()` -> `normalizeCoverage()` -> `unite(destrand=)` -> `calculateDiffMeth(overdispersion='MN')` -> `getMethylDiff()`
Scope: the methylKit OBJECT MODEL and the short-read bisulfite import-to-results workflow, for BOTH per-CpG (DMC) and fixed-tile (DMR) results. Which per-site test to use (count vs continuous, beta vs M) -> differential-cpg-testing. Selection-aware region callers (dmrseq/DSS/metilene) and region FDR -> dmr-detection. Long-read MM/ML modBAM input -> long-read-sequencing/nanopore-methylation (its counts pipe back into this object model).
## The Single Most Important Modern Insight -- The Object Model Is the Analysis
Coverage filtering, normalization, destranding, and the overdispersion model are not setup before the "real" test - they ARE the test. Each silently changes which CpGs exist and what the p-value means, and methylKit's defaults are tuned for nothing in particular. Three corollaries every misuse violates:
1. **The defaults offer no protection.** `calculateDiffMeth` defaults to `overdispersion='none'` - a plain logistic LRT that assumes binomial-only variance and over-calls under biological replication. Two healthy replicates differ at a CpG far more than coin-flip sampling predicts; only `overdispersion='MN'` adds the between-replicate (beta-binomial) layer. The paper discusses overdispersion; the function does not apply it unless told.
2. **The knobs interact and several are silent.** `overdispersion='MN'` automatically switches to the F-test, so a passed `test='Chisq'` is ignored with no warning. `adjust` defaults to SLIM (methylKit's own q-method), not BH, so counts are not comparable to a DSS/limma BH analysis. `tileMethylCounts(cov.bases=0)` lets a one-CpG window become a "region." None of these throw an error; the result just quietly changes.
3. **Coverage is the substrate, not the answer.** A single CpG's methylation percentage is a count ratio; below ~10x it is a coin flip. Filtering the low tail (noise) and the high tail (PCR/repeat artifacts) before testing decides the result more than the test does.
Organize the workflow around defending these, not around calling functions in order.
## The Import-to-Results Spine
Run these in order. Skipping or reordering them changes the result silently.
### 1. Import: methRead with the pipeline matching the input
**Goal:** Load per-cytosine counts into a methylRawList, choosing the parser that matches the Bismark output format.
**Approach:** `pipeline='bismarkCoverage'` reads `.cov`/`.cov.gz` (chr/start/end/%meth/numC/numT - NO strand, so destranding is limited); `pipeline='bismarkCytosineReport'` reads the CX/CpG report (carries strand + context, enables proper destranding). `treatment` is an integer vector (0/1, or 0/1/2 for multi-group). `context='CpG'` only - never destrand CHG/CHH downstream.
```r
library(methylKit)
file_list <- list('ctrl1.cov.gz', 'ctrl2.cov.gz', 'treat1.cov.gz', 'treat2.cov.gz')
sample_ids <- list('ctrl_1', 'ctrl_2', 'treat_1', 'treat_2')
meth_obj <- methRead(file_list, sample.id=sample_ids, treatment=c(0,0,1,1),
assembly='hg38', context='CpG', pipeline='bismarkCoverage')
# dbtype='tabix', save.db=TRUE gives disk-backed methylRawDB objects for large WGBS
```
### 2. Filter and normalize BEFORE uniting (and before tiling)
**Goal:** Drop unreliable and artifactual CpGs per sample, then remove library-size-driven coverage differences so a deeper sample does not look more "confident."
**Approach:** `filterByCoverage(lo.count, hi.perc)` removes the noisy low tail and the artifactual high tail; `normalizeCoverage` scales coverage between samples. Both are per-sample and must precede `unite` and `tileMethylCounts`.
```r
meth_filt <- filterByCoverage(meth_obj, lo.count=10, lo.perc=NULL, hi.count=NULL, hi.perc=99.9)
meth_norm <- normalizeCoverage(meth_filt, method='median')
```
### 3. Unite: destrand only for CpG, only with strand info
**Goal:** Build the per-base table of CpGs covered across samples for testing.
**Approach:** `unite` keeps CpGs covered in ALL samples; `min.per.group=2L` relaxes that to >=2 per group (keeps more sites, allows missingness). `destrand=TRUE` merges the + and - strand counts of a CpG dyad - valid ONLY for symmetric CpG context AND only meaningful when strand is present (cytosine report). On `.cov` (bismarkCoverage, no strand) destranding is limited; on CHG/CHH it is wrong.
```r
meth_united <- unite(meth_norm, destrand=TRUE) # destrand only if strand info present
meth_united <- unite(meth_norm, min.per.group=2L) # allow missingness across replicates
```
### 4. QC the united object before testing
**Goal:** Confirm samples cluster by biology, not by batch, before believing any DMC.
**Approach:** Run correlation, PCA, and clustering on the united (% methylation) object. A control clustering with the treated group, or PC1 tracking sequencing batch, means the contrast is confounded.
```r
getCorrelation(meth_united, plot=TRUE)
PCASamples(meth_united)
clusterSamples(meth_united, dist='correlation', method='ward.D', plot=TRUE)
```
### 5. Test: calculateDiffMeth with overdispersion correction
**Goal:** Test each CpG for a group difference using a model that accounts for between-replicate overdispersion.
**Approach:** With replicates, set `overdispersion='MN'`, which automatically uses the F-test (the `test=` argument is then ignored - passing `test='Chisq'` alongside MN does not produce a chi-square test). Set `adjust='BH'` if the q-values must be comparable to other tools; the default SLIM is methylKit-specific. `getMethylDiff` filters by effect size AND q.
```r
diff_meth <- calculateDiffMeth(meth_united, overdispersion='MN', adjust='BH', mc.cores=4)
dmcs <- getMethylDiff(diff_meth, difference=25, qvalue=0.01) # all DMCs
dmcs_hyper <- getMethylDiff(diff_meth, difference=25, qvalue=0.01, type='hyper')
# positive meth.diff = hyper in the higher-treatment group
```
## Tile-Based Regions (a fast screen, not selection-corrected inference)
**Goal:** Aggregate CpGs into fixed windows for a quick region-level scan.
**Approach:** Tile AFTER filter/normalize, raise `cov.bases` so a window needs real CpG support, then flow through the same unite -> calculateDiffMeth -> getMethylDiff path. The same `getMethylDiff` returns DMCs on a per-base object and (window) DMRs on a tiled object.
```r
tiles <- tileMethylCounts(meth_norm, win.size=1000, step.size=1000, cov.bases=3) # cov.bases>=3
tiles_united <- unite(tiles, destrand=FALSE) # tiles are not strand objects
diff_tiles <- calculateDiffMeth(tiles_united, overdispersion='MN', adjust='BH', mc.cores=4)
dmrs <- getMethylDiff(diff_tiles, difference=25, qvalue=0.01)
```
The per-tile q is a per-test SLIM/BH value: it does NOT model correlation between tiles and is NOT corrected for the region-selection step. Fixed windows also split or merge true DMRs at arbitrary boundaries. Treat methylKit tiles as a defensible screen; for rigorous region FDR (a permutation null that survives region selection) go to dmr-detection (dmrseq).
## Batch, Multi-Group, and the Single-Factor Limit
methylKit's `calculateDiffMeth` is a single-factor 2-group test. For known batch, remove the associated principal components before testing; for >2 groups, subset to pairwise contrasts. Complex designs (covariates, multi-factor) exceed what methylKit models - move to dmr-detection (DSS multiFactor / dmrseq covariates) or a continuous limma-on-M path (differential-cpg-testing).
```r
sample_anno <- data.frame(batch=c('a','a','b','b'))
as_comp <- assocComp(meth_united, sample_anno) # which PCs track the covariate
meth_corrected <- removeComp(meth_united, comp=1) # drop the batch PC, then test
meth_AB <- reorganize(meth_united, sample.ids=c('ctrl_1','ctrl_2','treat_1','treat_2'),
treatment=c(0,0,1,1)) # subset/relabel for a pairwise contrast
```
`pool(meth_united, sample.ids=...)` sums replicate counts into one pseudo-sample per group. This DESTROYS biological replication - the test then has no within-group variance estimate and its p-values are meaningless for inference. Use it only for no-replicate exploratory visualization, never for the reported test.
## Per-Method Failure Modes
### Overdispersion left at the default
**Trigger:** `calculateDiffMeth` with replicates and no `overdispersion=` argument. **Mechanism:** the default `'none'` is a binomial-only logistic LRT that ignores between-replicate variance. **Symptom:** implausibly many significant CpGs; q-values far smaller than a beta-binomial tool gives on the same data. **Fix:** `overdispersion='MN'` (which uses the F-test) whenever replicates exist.
### MN plus test='Chisq'
**Trigger:** passing both `overdispersion='MN'` and `test='Chisq'`. **Mechanism:** MN forces the F-test; `test=` is silently ignored. **Symptom:** the reported "chi-square test" was never run. **Fix:** drop `test=` when using MN; `test=` is honored only with `overdispersion='none'`.
### SLIM read as BH
**Trigger:** comparing methylKit q-value counts to a DSS/limma BH analysis. **Mechanism:** `adjust` defaults to SLIM, methylKit's own sliding-linear-model q-method. **Symptom:** DMC counts disagree with another tool's BH results at the "same" q. **Fix:** set `adjust='BH'` for cross-tool comparability and state the method used.
### cov.bases=0 tiles
**Trigger:** `tileMethylCounts` at the default `cov.bases=0`. **Mechanism:** a window with one covered CpG becomes a "region." **Symptom:** thousands of single-CpG "DMRs," many noisy. **Fix:** raise `cov.bases` to >=3.
### Tiling or testing the raw object
**Trigger:** `tileMethylCounts(meth_obj, ...)` or uniting before filtering/normalizing. **Mechanism:** low-coverage and library-size artifacts propagate into the tiles and the test. **Symptom:** artifactual regions; deeper samples look hyper-confident. **Fix:** `filterByCoverage` then `normalizeCoverage` BEFORE tiling/uniting.
### Destranding the wrong context or input
**Trigger:** `unite(destrand=TRUE)` on `.cov` input or on CHG/CHH context. **Mechanism:** `.cov` carries no strand; non-CpG dyads are not symmetric. **Symptom:** double-counting or wrong merges. **Fix:** destrand CpG only, ideally from the cytosine report.
### pool() then test
**Trigger:** `pool()` followed by `calculateDiffMeth`. **Mechanism:** pooling removes within-group variance. **Symptom:** tiny p-values with no biological meaning. **Fix:** never pool for the reported test; keep replicates separate.
## Quantitative Thresholds
| Threshold | Source | Rationale |
|-----------|--------|-----------|
| `lo.count` = 10 | convention (methylKit tutorial) | below ~10x a single-CpG percentage is a coin flip; not a derived value |
| `hi.perc` = 99.9 | convention | drops the top 0.1% coverage (PCR/repeat artifacts) |
| `overdispersion` = 'MN' with replicates | Akalin 2012 *Genome Biol* 13:R87 | adds the beta-binomial between-replicate layer; default 'none' over-calls |
| `adjust` = 'BH' (default SLIM) | Akalin 2012 *Genome Biol* 13:R87 | BH for comparability; SLIM is methylKit-specific |
| `getMethylDiff` difference=25, qvalue=0.01 | methylKit defaults | 25% is tutorial convention, NOT derived; justify per feature/coverage/purity and report it |
| `tileMethylCounts` cov.bases >= 3 | nuance | default 0 admits single-CpG tiles; require real CpG support |
| `win.size`=`step.size`=1000 (default) | methylKit defaults | `step < win` gives overlapping (sliding) tiles |
## Common Errors
| Error / symptom | Cause | Solution |
|-----------------|-------|----------|
| Implausibly many DMCs | `overdispersion='none'` under replication | set `overdispersion='MN'` |
| Reported chi-square never ran | `test='Chisq'` with MN | MN forces F; drop `test=` |
| Counts disagree with another tool at same q | default `adjust='SLIM'` | set `adjust='BH'`, state the method |
| Thousands of single-CpG "regions" | `cov.bases=0` | raise `cov.bases` to >=3 |
| Destrand error / double counts | destrand on `.cov` or non-CpG | destrand CpG only, from cytosine report |
| Meaningless tiny p-values | `pool()` before testing | keep replicates; never pool for inference |
| Deeper sample looks more confident | no `normalizeCoverage` | normalize before unite/test |
## References
- Akalin A, Kormaksson M, Li S, Garrett-Bakelman FE, Figueroa ME, Melnick A, Mason CE. 2012. methylKit: a comprehensive R package for the analysis of genome-wide DNA methylation profiles. *Genome Biol* 13:R87.
- Krueger F, Andrews SR. 2011. Bismark: a flexible aligner and methylation caller for Bisulfite-Seq applications. *Bioinformatics* 27:1571-1572.
- Robinson MD, Kahraman A, Law CW, Lindsay H, Nowicka M, Weber LM, Zhou X. 2014. Statistical methods for detecting differentially methylated loci and regions. *Front Genet* 5:324.
## Related Skills
- methylation-calling - Produces the coverage/cytosine reports read here
- differential-cpg-testing - Per-site statistical model choice (count vs continuous)
- dmr-detection - Selection-aware region callers (dmrseq/DSS) beyond methylKit tiles
- pathway-analysis/go-enrichment - Functional annotation of differentially methylated genes
- long-read-sequencing/nanopore-methylation - Long-read MM/ML calling; pipe counts into this object model
- workflows/methylation-pipeline - End-to-end bisulfite pipeline
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!