
Tooluniverse Epigenomics
- 374 installs
- 1.6k repo stars
- Updated August 4, 2026
- mims-harvard/tooluniverse
tooluniverse-epigenomics is a ToolUniverse skill that analyzes DNA methylation, ChIP-seq, and ATAC-seq data for developers who need genome-wide epigenomic statistics, chromatin integration, and multi-omics interpretation
About
tooluniverse-epigenomics is a genomics skill in mims-harvard/tooluniverse combining Python computation with database annotation for methylation, ChIP-seq, ATAC-seq, and multi-omics integration. It runs a 7-phase workflow from question parsing through methylation processing, peak analysis, ToolUniverse ENCODE and GTEx lookups, and genome-wide statistics, with a bundled methylation_density.py script for CpG density metrics. The skill enforces row-versus-unique-site counting rules that prevent silent wrong answers on long-format methylation CSVs. Developers reach for tooluniverse-epigenomics when analyzing CpG methylation, histone marks, chromatin accessibility, or integrating epigenomic data with expression.
- Regulatory mark and locus lookup
- Chromatin context enrichment
- Cross-cohort epigenomic comparison
- ToolUniverse genomics APIs
- Agent-driven regulatory interpretation
Tooluniverse Epigenomics by the numbers
- 374 all-time installs (skills.sh)
- +5 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #525 of 2,064 Data Science & ML skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/mims-harvard/tooluniverse --skill tooluniverse-epigenomicsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 374 |
|---|---|
| repo stars | ★ 1.6k |
| Last updated | August 4, 2026 |
| Repository | mims-harvard/tooluniverse ↗ |
How do you analyze DNA methylation and ChIP-seq data?
Investigate methylation, chromatin marks, and regulatory context with ToolUniverse epigenomic resources to interpret expression changes and disease associations.
Who is it for?
Computational biologists analyzing Illumina methylation arrays, ChIP-seq peaks, or ATAC-seq with Python and ToolUniverse APIs.
Skip if: RNA-seq differential expression, variant calling, or protein structure workflows outside epigenomics.
When should I use this skill?
A developer asks about CpG methylation density, ChIP-seq peak annotation, ATAC-seq NFR detection, or epigenomic multi-omics integration.
What you get
Epigenomic statistics, filtered methylation metrics, peak annotations, and database-backed regulatory context.
- Methylation density and filter statistics
- Peak annotation results
- Database-backed regulatory context
By the numbers
- 7-phase analysis workflow from question parsing through genome-wide statistics
- 4 bundled reference files plus methylation_density.py script
- Supports Illumina 450K and EPIC methylation array analysis
Files
Genomics and Epigenomics Data Processing
⚠️ TOP-OF-MIND RULE: long-format methylation CSV — count ROWS, not unique positions
When the input is a long-format methylation CSV (one row per (sample, CpG_position) e.g. columns Pos, Chromosome, MethylationPercentage), "how many sites are removed when filtering" almost always means rows removed, NOT unique-position removals. The two answers differ by a factor of ≈ n_samples.
| Question phrasing | What it means |
|---|---|
| "how many sites are removed when filtering …" | rows removed (= samples × positions failing the filter) |
| "how many unique CpG sites pass filter" | unique positions (dedupe by Pos then filter) |
❌ WRONG: df.drop_duplicates(["Pos"]).query("MethylationPercentage<10 or >90") then len(filtered) → counts unique positions (typically 100–1500)
✅ RIGHT: df.query("MethylationPercentage<10 or MethylationPercentage>90") then len(df) - len(filtered) → counts rows (typically 10k–30k)
If your answer is < 2000 when the data has 1000+ positions × 20+ samples, you deduplicated too early. Re-read the question's noun before reporting.
---
RULE ZERO — Check for pre-computed results FIRST
Before following any instruction below, scan the data folder for:
*_executed.ipynb→ read withtu run read_executed_notebook '{"data_folder":"<path>","search":"<keyword>"}'and cite its cell outputs as the authoritative answer- Pre-computed result files (CSV/TSV with names like
*results*,*deseq*,*enrich*,*stats*,*_simplified.csv) → read directly and report the requested value - Canonical analysis scripts (
analysis.R,run_*.py,find_*.R,*.Rmd) → execute as-is and read the output
Only follow this skill's re-analysis recipe below if none of the above exist. Re-running from raw data produces different numbers than the published answer and is much slower (often 5-10× turn count).
---
Production-ready skill combining Python computation (pandas, scipy, numpy, pysam, statsmodels) with ToolUniverse annotation tools for epigenomics analysis.
LOOK UP, DON'T GUESS
When uncertain about any scientific fact, SEARCH databases first.
When to Use
Methylation data, ChIP-seq peaks, ATAC-seq, multi-omics integration, genome-wide epigenomic statistics. Keywords: methylation, CpG, ChIP-seq, ATAC-seq, histone, chromatin, epigenetic.
NOT for: RNA-seq DEG, variant calling, gene enrichment, protein structure.
---
Key Principles
1. Data-first - Load/inspect before analysis 2. Question-driven - Extract specific numeric answer 3. Coordinate system awareness - Track genome build (hg19/hg38/mm10), chr prefix 4. Statistical rigor - FDR correction, effect size filtering 5. CpG identification - Parse Illumina probe IDs, genomic coordinates
PRIMARY SCRIPT — methylation_density.py (use FIRST for CpG-density questions)
For long-format methylation CSVs (Pos, Chromosome, MethylationPercentage) paired with chromosome-length CSVs, ALWAYS run the bundled script before hand-rolling pandas. It deterministically computes every common metric in one pass and avoids the rows-vs-sites pitfall that produces silently-wrong answers.
python skills/tooluniverse-epigenomics/scripts/methylation_density.py \
--cpg <CpG csv> --chr-lengths <chr lengths csv> \
--filter-meth-extremes 90 10The full JSON output contains every metric. Pick the one that matches the question's wording (NOT a similar-looking one):
| Question phrasing | Script field |
|---|---|
| "how many sites are removed when filtering …" | rows_removed |
| "how many unique CpG sites pass filter" | unique_pos_after_filter |
| "genome-wide AVERAGE chromosomal density" | density_avg_per_chr |
| "density on chromosome X" | density_chromosome (pass --chromosome X) |
| "total density across the genome" | density_total_over_genome |
The two density numbers (density_avg_per_chr vs density_total_over_genome) typically differ by ~2× because CpGs are not uniformly distributed across chromosomes; reporting one when the question asks for the other is the most common failure mode here.
For "sites removed" questions, the long-format CSV has multiple rows per CpG position (one per sample), so rows_removed is in the tens of thousands while unique_pos_removed is in the hundreds. Match the granularity to the question.
Distinguish "rows" vs "unique sites" — methylation CSVs are usually long-format
CpG methylation CSVs typically have ONE ROW PER (sample × CpG site) — so len(df) >> n_unique_sites. Before computing anything, decide which axis the question is asking about:
| Question phrasing | Axis | Operation |
|---|---|---|
| "how many sites are removed when filtering" | sample-rows | filter then count rows; do NOT dedupe by Pos. The CSV is in long format; "sites" here is row-shaped. Subtract len(df_filtered) from len(df). |
| "how many unique CpG sites pass filter" | unique positions | dedupe by position (or Pos column), then filter |
| "genome-wide average chromosomal density" | per-chromosome density | MEAN of per-chromosome densities: (n_unique_per_chr / chr_length).mean(). NOT total_unique / total_genome — that gives a different answer (typically ≈ ½ of the per-chr mean for unevenly distributed CpGs). |
| "density on chromosome X" | single chromosome | unique positions on X / length(X). Be careful which species — check the question text for "Zebra Finch" vs "Jackdaw". |
| "chi-square for uniform distribution across chromosomes" | unique positions per chromosome | filter rows first, then dedupe by (Chromosome, Pos), then count per-chromosome unique positions for chi-square against expected = chr_length / total_length × n_unique_filtered |
Sanity check: if your filtered count is two orders of magnitude smaller than the GT range, you likely deduped when the question wanted row-level counts (or vice versa). Re-run with the other axis and compare.
For the chi-square uniformity test: expected counts = chromosome_length / total_genome_length × n_unique_sites. The chi-square statistic depends on the count granularity (rows vs unique sites) — a row-level chi-square gives a much higher chi-square than a unique-position chi-square because n is larger.
Precedence: when an *_executed.ipynb exists, read its filtering code verbatim — df[(df.MethylationPercentage > 90) | (df.MethylationPercentage < 10)] (no dedup) and df.drop_duplicates('Pos') (with dedup) yield wildly different counts on the same dataset.
---
Workflow
Phase 0: Question Parsing
Identify data files, specific statistic, thresholds, genome build. Categorize by keywords. See ANALYSIS_PROCEDURES.md for decision tree.
Phase 1: Methylation Processing
- Load beta/M-value matrix (CSV/TSV/parquet/HDF5)
- Filter by variance, missing rate, probe type, chromosome, CpG island relation
- Differential methylation: T-test/Wilcoxon between groups + FDR
- Age-related CpG: Pearson/Spearman correlation + FDR
- Chromosome density: CpG count / chromosome length
Phase 2: ChIP-seq Peak Analysis
- Load BED/narrowPeak/broadPeak, normalize chromosomes
- Peak stats, annotation to genes, overlap analysis (Jaccard)
Phase 3: ATAC-seq
- NFR detection (<150bp peaks), region classification
Phase 4: Multi-Omics Integration
- Methylation-expression correlation per probe-gene (Pearson/Spearman + FDR)
- ChIP-seq + expression: promoter peaks vs expression levels
Phase 5: Clinical Data
- Missing data analysis across modalities, complete case identification
Phase 6: ToolUniverse Annotation
ENCODE tools:
ENCODE_search_rnaseq_experiments:assay_type("total RNA-seq" default; fall back to "polyA plus RNA-seq"),biosample,limitENCODE_search_histone_experiments:target(e.g., "H3K27ac"),cell_type/tissue/biosample,limit
GEO tools: GEO_search_rnaseq_datasets, GEO_search_atacseq_datasets -- both accept limit or max_results
GTEx tools:
GTEx_get_median_gene_expression:gene_symbol(NOT Ensembl ID)GTEx_query_eqtl:gene_symbol,tissue_id(case-sensitive exact, e.g.,"Whole_Blood")
Other: ensembl_lookup_gene (requires species='homo_sapiens'), ensembl_get_regulatory_features (NO "chr" prefix), SCREEN_get_regulatory_elements, ChIPAtlas_* (requires operation param), SRA_search_experiments (library_strategy: "ChIP-Seq"/"Bisulfite-Seq"/"ATAC-seq")
Phase 7: Genome-Wide Statistics
Global mean/median beta, probe variance, chromosome density, DMP counts.
See CODE_REFERENCE.md for full implementations.
---
Common Patterns
| Pattern | Key Steps |
|---|---|
| Differential methylation | Filter probes → groups → t-test → FDR → threshold |
| Age-related CpG density | Correlate with age → FDR → map to chr → density ratio |
| Multi-omics missing data | Extract IDs → intersect → check NaN → complete case count |
| ChIP-seq annotation | Load peaks → annotate genes → classify regions |
| Methylation-expression | Align samples → correlate → FDR → anti-correlations |
---
GTEx Tissue IDs
Whole_Blood, Liver, Lung, Breast_Mammary_Tissue, Brain_Cortex, Heart_Left_Ventricle, Kidney_Cortex, Thyroid, Adipose_Subcutaneous, Muscle_Skeletal
---
Evidence Grading
| Grade | Criteria |
|---|---|
| Strong | padj < 0.01 AND abs(delta-beta) >= 0.2, replicated |
| Moderate | padj < 0.05 AND abs(delta-beta) >= 0.1 |
| Weak | padj < 0.05 but delta-beta < 0.1 |
| Insufficient | padj >= 0.05 or no replication |
Delta-beta >= 0.2 = strong effect. ChIP-seq: q < 0.01, FE >= 2 for confidence. ATAC-seq NFR < 150bp = active regulatory. Always apply BH FDR. Verify genome build consistency.
---
Limitations
- No pybedtools/pyBigWig: pure Python intervals
- Illumina-centric (450K/EPIC); uses t-test/Wilcoxon (not limma)
- No peak calling (assumes pre-called)
- API rate limits: ~20 genes per batch
Reference Files
CODE_REFERENCE.md, TOOLS_REFERENCE.md, ANALYSIS_PROCEDURES.md, QUICK_START.md
# API Keys for ToolUniverse
# Copy this file to .env and fill in your actual API keys
BIOGRID_API_KEY=your_api_key_here
BOLTZ_MCP_SERVER_HOST=your_api_key_here
BRENDA_EMAIL=your_api_key_here
BRENDA_PASSWORD=your_api_key_here
DISGENET_API_KEY=your_api_key_here
EXPERT_FEEDBACK_MCP_SERVER_URL=your_api_key_here
NVIDIA_API_KEY=your_api_key_here
OMIM_API_KEY=your_api_key_here
TXAGENT_MCP_SERVER_HOST=your_api_key_here
USPTO_API_KEY=your_api_key_here
USPTO_MCP_SERVER_HOST=your_api_key_here
Analysis Procedures Reference
Detailed decision trees, step-by-step analysis patterns, edge cases, and fallback strategies for the epigenomics skill.
---
Question Parameter Extraction
Extract these from the user's question before starting analysis:
| Parameter | Default | Example Question Text |
|---|---|---|
| Significance threshold | 0.05 | "padj < 0.05", "FDR < 0.01" |
| Beta difference threshold | 0 | " |
| Variance filter | None | "variance > 0.01", "top 5000 most variable" |
| Chromosome filter | All | "chromosome 17", "autosomes only" |
| Genome build | hg38 | "hg19", "GRCh37", "mm10" |
| CpG type filter | All | "cg probes only", "exclude ch probes" |
| Region filter | None | "promoter", "gene body", "intergenic" |
| Missing data handling | Report | "complete cases", "no missing data" |
| Specific comparison | Infer | "tumor vs normal", "old vs young" |
| Specific statistic | Infer | "density", "ratio", "count", "average" |
---
Decision Tree
Q: What type of epigenomics data?
METHYLATION -> Phase 1 (Methylation Processing)
CHIP-SEQ -> Phase 2 (ChIP-seq Processing)
ATAC-SEQ -> Phase 3 (ATAC-seq Processing)
MULTI-OMICS -> Phase 4 (Integration)
CLINICAL -> Phase 5 (Clinical Integration)
ANNOTATION -> Phase 6 (ToolUniverse Annotation)
Q: Is this a genome-wide statistics question?
YES -> Focus on chromosome-level aggregation (Phase 7)
NO -> Focus on site/region-level analysis---
Common Analysis Patterns
Pattern 1: Methylation Array Analysis
Input: Beta-value matrix + manifest + clinical data
Question: "How many CpGs are differentially methylated?"
Flow:
1. Load beta matrix, manifest, clinical data
2. Filter CpG probes (cg only, remove sex chr, variance filter)
3. Define groups from clinical data
4. Run differential_methylation()
5. Apply thresholds (padj < 0.05, |delta_beta| > 0.2)
6. Report count and direction (hyper/hypo)Pattern 2: Age-Related CpG Density
Input: Beta-value matrix + manifest + ages
Question: "What is the density ratio of age-related CpGs between chr1 and chr2?"
Flow:
1. Load beta matrix and ages from clinical data
2. Run identify_age_related_cpgs()
3. Filter significant age-related CpGs
4. Map to chromosomes using manifest
5. Calculate chromosome_cpg_density()
6. Compute ratio between specified chromosomesPattern 3: Multi-Omics Missing Data
Input: Clinical + expression + methylation data files
Question: "How many patients have complete data for all modalities?"
Flow:
1. Load all data files
2. Extract sample IDs from each
3. Find intersection (common samples)
4. Check for NaN/missing within clinical variables
5. Report complete cases countPattern 4: ChIP-seq Peak Annotation
Input: BED/narrowPeak file
Question: "What fraction of peaks are in promoter regions?"
Flow:
1. Load BED file with load_bed_file()
2. Load or fetch gene annotation (Ensembl)
3. Run annotate_peaks_to_genes()
4. Classify regions with classify_peak_regions()
5. Calculate fraction in promotersPattern 5: Methylation-Expression Integration
Input: Beta matrix + expression matrix + probe-gene mapping
Question: "What is the correlation between methylation and expression?"
Flow:
1. Load both matrices
2. Build probe-gene map from manifest
3. Align samples across datasets
4. Run correlate_methylation_expression()
5. Report significant anti-correlations---
Edge Cases
Missing Probe Annotation
When no manifest/annotation file is available:
- Extract chromosome from probe ID naming patterns if possible
- Use ToolUniverse Ensembl tools to build minimal annotation
- Report limitation: "chromosome mapping unavailable for X probes"
Mixed Genome Builds
When data uses different builds:
- Detect build from context (data README, file names, known coordinates)
- Use appropriate chromosome lengths for density calculations
- Do NOT mix hg19 and hg38 coordinates
Very Large Datasets
For datasets with >500K CpG sites:
- Use chunked processing for differential methylation
- Pre-filter by variance before statistical testing
- Use vectorized operations (avoid row-by-row loops where possible)
Sample ID Mismatches
Clinical and molecular data may use different ID formats:
- TCGA: barcode (TCGA-XX-XXXX-01A) vs patient ID (TCGA-XX-XXXX)
- Try truncating or matching partial IDs
- Report number of matched/unmatched samples
---
Fallback Strategies
| Scenario | Primary | Fallback |
|---|---|---|
| No manifest file | Load from data dir | Build minimal from Ensembl lookup |
| No pybedtools | Pure Python overlap | pandas-based interval intersection |
| No pyBigWig | Skip BigWig analysis | Use pre-computed summary tables |
| Missing clinical data | Report missing | Use available samples only |
| Low sample count | Parametric test | Use non-parametric (Wilcoxon) |
| Large dataset (>500K probes) | Full analysis | Sample or chunk-based processing |
Code Reference: Epigenomics Data Processing
Full Python function implementations for all workflow phases. See SKILL.md for the workflow overview.
---
Phase 0: Data Discovery
import os
import glob
data_dir = "." # or specified path
all_files = glob.glob(os.path.join(data_dir, "**/*"), recursive=True)
# Categorize files
methylation_files = [f for f in all_files if any(x in f.lower() for x in
['methyl', 'beta', 'cpg', 'illumina', '450k', '850k', 'epic', 'mval'])]
chipseq_files = [f for f in all_files if any(x in f.lower() for x in
['chip', 'peak', 'narrowpeak', 'broadpeak', 'histone'])]
atacseq_files = [f for f in all_files if any(x in f.lower() for x in
['atac', 'accessibility', 'openChromatin', 'dnase'])]
bed_files = [f for f in all_files if f.endswith(('.bed', '.bed.gz', '.narrowPeak', '.broadPeak'))]
bigwig_files = [f for f in all_files if f.endswith(('.bw', '.bigwig', '.bigWig'))]
clinical_files = [f for f in all_files if any(x in f.lower() for x in
['clinical', 'patient', 'sample', 'metadata', 'phenotype', 'survival'])]
expression_files = [f for f in all_files if any(x in f.lower() for x in
['express', 'rnaseq', 'fpkm', 'tpm', 'counts', 'transcriptom'])]
manifest_files = [f for f in all_files if any(x in f.lower() for x in
['manifest', 'annotation', 'probe', 'platform'])]
for category, files in [
('Methylation', methylation_files),
('ChIP-seq', chipseq_files),
('ATAC-seq', atacseq_files),
('BED', bed_files),
('BigWig', bigwig_files),
('Clinical', clinical_files),
('Expression', expression_files),
('Manifest', manifest_files),
]:
if files:
print(f"{category}: {files}")---
Phase 1: Methylation Data Processing
1.1 Load Methylation Data
import pandas as pd
import numpy as np
def load_methylation_data(file_path, **kwargs):
"""Load methylation beta-value or M-value matrix.
Expected format:
- Rows: CpG probes (cg00000029, cg00000108, ...)
- Columns: Samples (TCGA-XX-XXXX, ...)
- Values: Beta values (0-1) or M-values (log2 ratio)
"""
ext = os.path.splitext(file_path)[1].lower()
if ext in ['.csv']:
df = pd.read_csv(file_path, index_col=0, **kwargs)
elif ext in ['.tsv', '.txt']:
df = pd.read_csv(file_path, sep='\t', index_col=0, **kwargs)
elif ext in ['.parquet']:
df = pd.read_parquet(file_path, **kwargs)
elif ext in ['.h5', '.hdf5']:
df = pd.read_hdf(file_path, **kwargs)
else:
try:
df = pd.read_csv(file_path, sep='\t', index_col=0, **kwargs)
except Exception:
df = pd.read_csv(file_path, index_col=0, **kwargs)
return df
def detect_methylation_type(df):
"""Detect if data is beta values (0-1) or M-values (unbounded)."""
sample_vals = df.iloc[:1000, :5].values.flatten()
sample_vals = sample_vals[~np.isnan(sample_vals)]
if sample_vals.min() >= 0 and sample_vals.max() <= 1:
return 'beta'
else:
return 'mvalue'
def beta_to_mvalue(beta):
"""Convert beta values to M-values: M = log2(beta / (1 - beta))."""
beta = np.clip(beta, 1e-6, 1 - 1e-6)
return np.log2(beta / (1 - beta))
def mvalue_to_beta(mvalue):
"""Convert M-values to beta values: beta = 2^M / (2^M + 1)."""
return 2**mvalue / (2**mvalue + 1)1.2 Load Probe Manifest
def load_probe_annotation(manifest_path):
"""Load Illumina methylation array manifest.
Common columns: IlmnID, Name, CHR, MAPINFO (position), Strand,
UCSC_RefGene_Name, UCSC_RefGene_Group, Relation_to_UCSC_CpG_Island
"""
for skiprows in [0, 7, 8]:
try:
manifest = pd.read_csv(manifest_path, skiprows=skiprows,
low_memory=False)
if 'CHR' in manifest.columns or 'chr' in manifest.columns:
break
if 'Name' in manifest.columns or 'IlmnID' in manifest.columns:
break
except Exception:
continue
col_map = {}
for col in manifest.columns:
lower = col.lower()
if lower in ['chr', 'chromosome']:
col_map[col] = 'chr'
elif lower in ['mapinfo', 'position', 'pos', 'start']:
col_map[col] = 'position'
elif lower in ['name', 'ilmnid', 'probe_id', 'cpg_id']:
col_map[col] = 'probe_id'
elif 'refgene_name' in lower or 'gene' in lower:
col_map[col] = 'gene_name'
elif 'refgene_group' in lower:
col_map[col] = 'gene_group'
elif 'cpg_island' in lower or 'relation' in lower:
col_map[col] = 'cpg_island_relation'
manifest = manifest.rename(columns=col_map)
return manifest
def normalize_chromosome(chrom):
"""Normalize chromosome name: '1' -> 'chr1', 'chrX' -> 'chrX', etc."""
if chrom is None or pd.isna(chrom):
return None
chrom = str(chrom).strip()
if not chrom.startswith('chr'):
chrom = 'chr' + chrom
return chrom
def get_chromosome_lengths(genome='hg38'):
"""Return chromosome lengths for common genome builds."""
hg38 = {
'chr1': 248956422, 'chr2': 242193529, 'chr3': 198295559,
'chr4': 190214555, 'chr5': 181538259, 'chr6': 170805979,
'chr7': 159345973, 'chr8': 145138636, 'chr9': 138394717,
'chr10': 133797422, 'chr11': 135086622, 'chr12': 133275309,
'chr13': 114364328, 'chr14': 107043718, 'chr15': 101991189,
'chr16': 90338345, 'chr17': 83257441, 'chr18': 80373285,
'chr19': 58617616, 'chr20': 64444167, 'chr21': 46709983,
'chr22': 50818468, 'chrX': 156040895, 'chrY': 57227415,
}
hg19 = {
'chr1': 249250621, 'chr2': 243199373, 'chr3': 198022430,
'chr4': 191154276, 'chr5': 180915260, 'chr6': 171115067,
'chr7': 159138663, 'chr8': 146364022, 'chr9': 141213431,
'chr10': 135534747, 'chr11': 135006516, 'chr12': 133851895,
'chr13': 115169878, 'chr14': 107349540, 'chr15': 102531392,
'chr16': 90354753, 'chr17': 81195210, 'chr18': 78077248,
'chr19': 59128983, 'chr20': 63025520, 'chr21': 48129895,
'chr22': 51304566, 'chrX': 155270560, 'chrY': 59373566,
}
mm10 = {
'chr1': 195471971, 'chr2': 182113224, 'chr3': 160039680,
'chr4': 156508116, 'chr5': 151834684, 'chr6': 149736546,
'chr7': 145441459, 'chr8': 129401213, 'chr9': 124595110,
'chr10': 130694993, 'chr11': 122082543, 'chr12': 120129022,
'chr13': 120421639, 'chr14': 124902244, 'chr15': 104043685,
'chr16': 98207768, 'chr17': 94987271, 'chr18': 90702639,
'chr19': 61431566, 'chrX': 171031299, 'chrY': 91744698,
}
genomes = {'hg38': hg38, 'hg19': hg19, 'mm10': mm10}
return genomes.get(genome, hg38)1.3 CpG Site Filtering
def filter_cpg_probes(df, manifest=None, filters=None):
"""Filter CpG probes based on various criteria.
Args:
df: Methylation matrix (probes x samples)
manifest: Probe annotation DataFrame
filters: dict with keys:
- 'variance_threshold': float, minimum variance across samples
- 'mean_beta_range': tuple (min, max)
- 'missing_threshold': float (0-1), max fraction of NaN per probe
- 'chromosomes': list, keep only these chromosomes
- 'exclude_sex_chr': bool, remove chrX and chrY
- 'probe_type': 'cg' or 'ch'
- 'cpg_island': str ('Island', 'Shore', 'Shelf', 'OpenSea')
- 'gene_group': str ('TSS200', 'TSS1500', 'Body', '1stExon', etc.)
- 'top_n_variable': int, keep top N most variable probes
"""
if filters is None:
filters = {}
probe_mask = pd.Series(True, index=df.index)
if 'probe_type' in filters:
ptype = filters['probe_type']
probe_mask &= df.index.str.startswith(ptype)
if 'missing_threshold' in filters:
threshold = filters['missing_threshold']
missing_frac = df.isna().mean(axis=1)
probe_mask &= missing_frac <= threshold
if 'variance_threshold' in filters:
var_threshold = filters['variance_threshold']
probe_var = df.var(axis=1, skipna=True)
probe_mask &= probe_var >= var_threshold
if 'mean_beta_range' in filters:
min_beta, max_beta = filters['mean_beta_range']
probe_mean = df.mean(axis=1, skipna=True)
probe_mask &= (probe_mean >= min_beta) & (probe_mean <= max_beta)
if 'top_n_variable' in filters:
n = filters['top_n_variable']
probe_var = df.var(axis=1, skipna=True)
top_probes = probe_var.nlargest(n).index
probe_mask &= df.index.isin(top_probes)
if manifest is not None and len(manifest) > 0:
probe_id_col = 'probe_id' if 'probe_id' in manifest.columns else manifest.columns[0]
manifest_indexed = manifest.set_index(probe_id_col) if probe_id_col in manifest.columns else manifest
if 'chromosomes' in filters and 'chr' in manifest_indexed.columns:
valid_chr = [normalize_chromosome(c) for c in filters['chromosomes']]
chr_probes = manifest_indexed[
manifest_indexed['chr'].apply(normalize_chromosome).isin(valid_chr)
].index
probe_mask &= df.index.isin(chr_probes)
if filters.get('exclude_sex_chr', False) and 'chr' in manifest_indexed.columns:
nonsex_probes = manifest_indexed[
~manifest_indexed['chr'].apply(normalize_chromosome).isin(['chrX', 'chrY'])
].index
probe_mask &= df.index.isin(nonsex_probes)
if 'cpg_island' in filters and 'cpg_island_relation' in manifest_indexed.columns:
relation = filters['cpg_island']
island_probes = manifest_indexed[
manifest_indexed['cpg_island_relation'].str.contains(relation, na=False, case=False)
].index
probe_mask &= df.index.isin(island_probes)
if 'gene_group' in filters and 'gene_group' in manifest_indexed.columns:
group = filters['gene_group']
group_probes = manifest_indexed[
manifest_indexed['gene_group'].str.contains(group, na=False, case=False)
].index
probe_mask &= df.index.isin(group_probes)
filtered_df = df[probe_mask]
return filtered_df1.4 Differential Methylation Analysis
from scipy import stats
import statsmodels.stats.multitest as mt
def differential_methylation(beta_df, group1_samples, group2_samples,
test='ttest', correction='fdr_bh', alpha=0.05):
"""Perform differential methylation analysis between two groups.
Returns:
DataFrame with columns: mean_g1, mean_g2, delta_beta, pvalue, padj
"""
g1 = beta_df[group1_samples]
g2 = beta_df[group2_samples]
results = []
for probe in beta_df.index:
vals1 = g1.loc[probe].dropna().values
vals2 = g2.loc[probe].dropna().values
if len(vals1) < 2 or len(vals2) < 2:
results.append({
'probe': probe, 'mean_g1': np.nan, 'mean_g2': np.nan,
'delta_beta': np.nan, 'pvalue': np.nan
})
continue
mean1 = np.nanmean(vals1)
mean2 = np.nanmean(vals2)
delta = mean2 - mean1
if test == 'ttest':
stat, pval = stats.ttest_ind(vals1, vals2, equal_var=False)
elif test == 'wilcoxon':
stat, pval = stats.mannwhitneyu(vals1, vals2, alternative='two-sided')
elif test == 'ks':
stat, pval = stats.ks_2samp(vals1, vals2)
else:
stat, pval = stats.ttest_ind(vals1, vals2, equal_var=False)
results.append({
'probe': probe, 'mean_g1': mean1, 'mean_g2': mean2,
'delta_beta': delta, 'pvalue': pval
})
result_df = pd.DataFrame(results).set_index('probe')
valid_pvals = result_df['pvalue'].dropna()
if len(valid_pvals) > 0:
reject, padj, _, _ = mt.multipletests(valid_pvals.values, alpha=alpha, method=correction)
result_df.loc[valid_pvals.index, 'padj'] = padj
else:
result_df['padj'] = np.nan
return result_df
def identify_dmps(dm_results, alpha=0.05, delta_beta_threshold=0.0):
"""Identify differentially methylated positions (DMPs)."""
dmps = dm_results[
(dm_results['padj'] < alpha) &
(dm_results['delta_beta'].abs() >= delta_beta_threshold)
].copy()
dmps['direction'] = np.where(dmps['delta_beta'] > 0, 'hyper', 'hypo')
return dmps.sort_values('padj')1.5 Age-Related CpG Analysis
def identify_age_related_cpgs(beta_df, ages, method='correlation',
correction='fdr_bh', alpha=0.05):
"""Identify CpG sites associated with age.
Returns:
DataFrame with correlation, p-value, adjusted p-value
"""
results = []
for probe in beta_df.index:
vals = beta_df.loc[probe].values
mask = ~np.isnan(vals) & ~np.isnan(ages.values if hasattr(ages, 'values') else ages)
if sum(mask) < 5:
results.append({'probe': probe, 'correlation': np.nan,
'pvalue': np.nan})
continue
if method == 'correlation':
corr, pval = stats.pearsonr(ages[mask] if hasattr(ages, '__getitem__') else
np.array(ages)[mask], vals[mask])
elif method == 'spearman':
corr, pval = stats.spearmanr(ages[mask] if hasattr(ages, '__getitem__') else
np.array(ages)[mask], vals[mask])
else:
corr, pval = stats.pearsonr(ages[mask] if hasattr(ages, '__getitem__') else
np.array(ages)[mask], vals[mask])
results.append({'probe': probe, 'correlation': corr, 'pvalue': pval})
result_df = pd.DataFrame(results).set_index('probe')
valid_pvals = result_df['pvalue'].dropna()
if len(valid_pvals) > 0:
reject, padj, _, _ = mt.multipletests(valid_pvals.values, alpha=alpha, method=correction)
result_df.loc[valid_pvals.index, 'padj'] = padj
else:
result_df['padj'] = np.nan
return result_df1.6 Chromosome-Level Methylation Statistics
def chromosome_cpg_density(cpg_probes, manifest, genome='hg38'):
"""Calculate CpG density per chromosome.
Returns:
DataFrame with chr, n_cpgs, chr_length, density (CpGs per bp)
"""
chr_lengths = get_chromosome_lengths(genome)
probe_id_col = 'probe_id' if 'probe_id' in manifest.columns else manifest.columns[0]
if probe_id_col in manifest.columns:
probe_chr = manifest.set_index(probe_id_col)
else:
probe_chr = manifest
if 'chr' in probe_chr.columns:
chr_col = 'chr'
elif 'CHR' in probe_chr.columns:
chr_col = 'CHR'
else:
raise ValueError("No chromosome column found in manifest")
probe_chrs = probe_chr.loc[probe_chr.index.isin(cpg_probes), chr_col]
probe_chrs = probe_chrs.apply(normalize_chromosome)
chr_counts = probe_chrs.value_counts()
results = []
for chrom, count in chr_counts.items():
if chrom in chr_lengths:
length = chr_lengths[chrom]
density = count / length
results.append({
'chr': chrom,
'n_cpgs': count,
'chr_length': length,
'density_per_bp': density,
'density_per_mb': density * 1e6,
})
return pd.DataFrame(results).sort_values('chr',
key=lambda x: x.str.replace('chr', '').replace({'X': '23', 'Y': '24'}).astype(int))
def genome_wide_average_density(density_df):
"""Calculate genome-wide average CpG density across all chromosomes."""
total_cpgs = density_df['n_cpgs'].sum()
total_length = density_df['chr_length'].sum()
return total_cpgs / total_length
def chromosome_density_ratio(density_df, chr1, chr2):
"""Calculate density ratio between two chromosomes."""
chr1 = normalize_chromosome(chr1)
chr2 = normalize_chromosome(chr2)
d1 = density_df[density_df['chr'] == chr1]['density_per_bp'].values[0]
d2 = density_df[density_df['chr'] == chr2]['density_per_bp'].values[0]
return d1 / d2---
Phase 2: ChIP-seq Peak Analysis
2.1 Load BED/Peak Files
def load_bed_file(file_path, format='bed'):
"""Load BED format file (standard BED, narrowPeak, broadPeak)."""
if format == 'narrowPeak' or file_path.endswith('.narrowPeak'):
names = ['chrom', 'start', 'end', 'name', 'score', 'strand',
'signalValue', 'pValue', 'qValue', 'peak']
elif format == 'broadPeak' or file_path.endswith('.broadPeak'):
names = ['chrom', 'start', 'end', 'name', 'score', 'strand',
'signalValue', 'pValue', 'qValue']
else:
with open(file_path, 'r') as f:
first_line = f.readline().strip()
while first_line.startswith('#') or first_line.startswith('track') or first_line.startswith('browser'):
first_line = f.readline().strip()
n_cols = len(first_line.split('\t'))
bed_col_names = ['chrom', 'start', 'end', 'name', 'score', 'strand',
'thickStart', 'thickEnd', 'itemRgb', 'blockCount',
'blockSizes', 'blockStarts']
names = bed_col_names[:n_cols]
df = pd.read_csv(file_path, sep='\t', header=None, names=names,
comment='#', low_memory=False)
df = df[~df['chrom'].astype(str).str.startswith(('track', 'browser'))]
df['chrom'] = df['chrom'].apply(normalize_chromosome)
df['start'] = pd.to_numeric(df['start'], errors='coerce')
df['end'] = pd.to_numeric(df['end'], errors='coerce')
return df
def peak_statistics(peaks_df):
"""Calculate basic peak statistics."""
peaks_df = peaks_df.copy()
peaks_df['length'] = peaks_df['end'] - peaks_df['start']
stats_dict = {
'total_peaks': len(peaks_df),
'mean_peak_length': peaks_df['length'].mean(),
'median_peak_length': peaks_df['length'].median(),
'total_coverage_bp': peaks_df['length'].sum(),
'peaks_per_chromosome': peaks_df['chrom'].value_counts().to_dict(),
}
if 'signalValue' in peaks_df.columns:
stats_dict['mean_signal'] = peaks_df['signalValue'].mean()
stats_dict['median_signal'] = peaks_df['signalValue'].median()
if 'qValue' in peaks_df.columns:
stats_dict['mean_qvalue'] = peaks_df['qValue'].mean()
return stats_dict2.2 Peak Annotation
def annotate_peaks_to_genes(peaks_df, gene_annotation=None,
tss_upstream=2000, tss_downstream=500):
"""Annotate peaks to nearest gene / genomic feature.
Classifies each peak as: promoter, gene_body, proximal, distal, or intergenic.
"""
if gene_annotation is None:
return peaks_df
annotated = peaks_df.copy()
annotations = []
for _, peak in peaks_df.iterrows():
peak_chr = peak['chrom']
peak_mid = (peak['start'] + peak['end']) // 2
chr_genes = gene_annotation[gene_annotation['chr'] == peak_chr]
if len(chr_genes) == 0:
annotations.append({
'nearest_gene': 'intergenic',
'distance_to_tss': np.nan,
'feature': 'intergenic'
})
continue
tss_positions = chr_genes.apply(
lambda g: g['start'] if g.get('strand', '+') == '+' else g['end'],
axis=1
)
distances = (peak_mid - tss_positions).abs()
nearest_idx = distances.idxmin()
nearest_gene = chr_genes.loc[nearest_idx]
distance = distances.loc[nearest_idx]
tss = tss_positions.loc[nearest_idx]
if abs(peak_mid - tss) <= tss_upstream:
feature = 'promoter'
elif peak['start'] >= nearest_gene['start'] and peak['end'] <= nearest_gene['end']:
feature = 'gene_body'
elif abs(peak_mid - tss) <= 10000:
feature = 'proximal'
else:
feature = 'distal'
annotations.append({
'nearest_gene': nearest_gene.get('gene_name', nearest_gene.name),
'distance_to_tss': int(distance),
'feature': feature
})
ann_df = pd.DataFrame(annotations, index=peaks_df.index)
return pd.concat([peaks_df, ann_df], axis=1)
def classify_peak_regions(annotated_peaks):
"""Classify peaks into genomic regions. Returns dict with counts per region type."""
if 'feature' not in annotated_peaks.columns:
return {'unknown': len(annotated_peaks)}
return annotated_peaks['feature'].value_counts().to_dict()2.3 Peak Overlap Analysis
def find_overlaps(peaks_a, peaks_b, min_overlap=1):
"""Find overlapping peaks between two BED DataFrames (pure Python)."""
overlaps = []
for chrom in peaks_a['chrom'].unique():
a_chr = peaks_a[peaks_a['chrom'] == chrom].sort_values('start')
b_chr = peaks_b[peaks_b['chrom'] == chrom].sort_values('start')
if len(b_chr) == 0:
continue
for _, a_peak in a_chr.iterrows():
for _, b_peak in b_chr.iterrows():
if b_peak['start'] >= a_peak['end']:
break
if b_peak['end'] <= a_peak['start']:
continue
overlap_start = max(a_peak['start'], b_peak['start'])
overlap_end = min(a_peak['end'], b_peak['end'])
overlap_bp = overlap_end - overlap_start
if overlap_bp >= min_overlap:
overlaps.append({
'a_chrom': chrom,
'a_start': a_peak['start'],
'a_end': a_peak['end'],
'b_start': b_peak['start'],
'b_end': b_peak['end'],
'overlap_bp': overlap_bp,
})
return pd.DataFrame(overlaps) if overlaps else pd.DataFrame()
def jaccard_similarity(peaks_a, peaks_b, genome='hg38'):
"""Calculate Jaccard similarity between two peak sets."""
coverage_a = (peaks_a['end'] - peaks_a['start']).sum()
coverage_b = (peaks_b['end'] - peaks_b['start']).sum()
overlaps = find_overlaps(peaks_a, peaks_b)
if len(overlaps) == 0:
return 0.0
intersection = overlaps['overlap_bp'].sum()
union = coverage_a + coverage_b - intersection
return intersection / union if union > 0 else 0.0---
Phase 3: ATAC-seq Analysis
def load_atac_peaks(file_path):
"""Load ATAC-seq peak file (typically narrowPeak format)."""
return load_bed_file(file_path, format='narrowPeak')
def atac_peak_statistics(peaks_df):
"""ATAC-seq specific statistics with NFR detection."""
basic_stats = peak_statistics(peaks_df)
peaks_df = peaks_df.copy()
peaks_df['length'] = peaks_df['end'] - peaks_df['start']
nfr_peaks = peaks_df[peaks_df['length'] < 150]
nucleosome_peaks = peaks_df[peaks_df['length'] >= 150]
basic_stats['nfr_peaks'] = len(nfr_peaks)
basic_stats['nucleosome_peaks'] = len(nucleosome_peaks)
basic_stats['nfr_fraction'] = len(nfr_peaks) / len(peaks_df) if len(peaks_df) > 0 else 0
return basic_stats
def chromatin_accessibility_by_region(peaks_df, gene_annotation=None):
"""Calculate chromatin accessibility distribution across genomic regions."""
annotated = annotate_peaks_to_genes(peaks_df, gene_annotation)
regions = classify_peak_regions(annotated)
total = sum(regions.values())
region_fractions = {k: v / total for k, v in regions.items()}
return {
'counts': regions,
'fractions': region_fractions,
'total_peaks': total,
}---
Phase 4: Multi-Omics Integration
4.1 Expression-Methylation Correlation
def correlate_methylation_expression(beta_df, expression_df, probe_gene_map,
method='pearson', correction='fdr_bh'):
"""Correlate methylation levels with gene expression.
Returns:
DataFrame with correlation, p-value per probe-gene pair
"""
common_samples = list(set(beta_df.columns) & set(expression_df.columns))
if len(common_samples) < 5:
raise ValueError(f"Not enough common samples: {len(common_samples)}")
beta_aligned = beta_df[common_samples]
expr_aligned = expression_df[common_samples]
results = []
for probe, gene in probe_gene_map.items():
if probe not in beta_aligned.index or gene not in expr_aligned.index:
continue
meth_vals = beta_aligned.loc[probe].values
expr_vals = expr_aligned.loc[gene].values
mask = ~np.isnan(meth_vals) & ~np.isnan(expr_vals)
if sum(mask) < 5:
continue
if method == 'pearson':
corr, pval = stats.pearsonr(meth_vals[mask], expr_vals[mask])
else:
corr, pval = stats.spearmanr(meth_vals[mask], expr_vals[mask])
results.append({
'probe': probe,
'gene': gene,
'correlation': corr,
'pvalue': pval,
'n_samples': sum(mask),
})
result_df = pd.DataFrame(results)
if len(result_df) > 0:
valid_pvals = result_df['pvalue'].dropna()
if len(valid_pvals) > 0:
reject, padj, _, _ = mt.multipletests(valid_pvals.values, method=correction)
result_df.loc[valid_pvals.index, 'padj'] = padj
return result_df4.2 ChIP-seq + Expression Integration
def integrate_chipseq_expression(peaks_df, expression_df, gene_annotation,
tss_window=5000):
"""Integrate ChIP-seq peaks with gene expression.
Returns:
DataFrame with genes having promoter peaks and their expression
"""
annotated = annotate_peaks_to_genes(peaks_df, gene_annotation,
tss_upstream=tss_window)
promoter_peaks = annotated[annotated['feature'] == 'promoter']
peak_genes = promoter_peaks['nearest_gene'].unique()
common_genes = [g for g in peak_genes if g in expression_df.index]
result = pd.DataFrame({
'gene': common_genes,
'has_promoter_peak': True,
'mean_expression': [expression_df.loc[g].mean() for g in common_genes],
})
return result---
Phase 5: Clinical Data Integration
def missing_data_analysis(clinical_df=None, expression_df=None,
methylation_df=None, sample_id_col=None):
"""Analyze missing data across multiple omics modalities.
Returns:
dict with completeness statistics
"""
results = {}
clinical_samples = set()
if clinical_df is not None:
if sample_id_col and sample_id_col in clinical_df.columns:
clinical_samples = set(clinical_df[sample_id_col].dropna())
else:
clinical_samples = set(clinical_df.index)
results['clinical_samples'] = len(clinical_samples)
expression_samples = set()
if expression_df is not None:
expression_samples = set(expression_df.columns)
results['expression_samples'] = len(expression_samples)
methylation_samples = set()
if methylation_df is not None:
methylation_samples = set(methylation_df.columns)
results['methylation_samples'] = len(methylation_samples)
all_sets = []
if clinical_samples:
all_sets.append(clinical_samples)
if expression_samples:
all_sets.append(expression_samples)
if methylation_samples:
all_sets.append(methylation_samples)
if len(all_sets) > 0:
complete_samples = set.intersection(*all_sets)
results['complete_samples'] = len(complete_samples)
results['complete_sample_ids'] = sorted(complete_samples)
else:
results['complete_samples'] = 0
if clinical_df is not None:
for col in clinical_df.columns:
n_missing = clinical_df[col].isna().sum()
n_total = len(clinical_df)
results[f'clinical_{col}_missing'] = n_missing
results[f'clinical_{col}_complete'] = n_total - n_missing
return results
def find_complete_cases(data_frames, variables=None):
"""Find samples that are complete across specified data frames and variables."""
sample_sets = []
for name, df in data_frames.items():
if df is not None:
if variables and name in variables:
for var in variables[name]:
if var in df.columns:
complete = set(df[df[var].notna()].index)
sample_sets.append(complete)
elif var in df.index:
complete = set(df.columns[df.loc[var].notna()])
sample_sets.append(complete)
else:
sample_sets.append(set(df.columns))
if not sample_sets:
return set()
return set.intersection(*sample_sets)---
Phase 6: ToolUniverse Annotation Integration
from tooluniverse import ToolUniverse
tu = ToolUniverse()
tu.load_tools()
def annotate_genes_with_tooluniverse(gene_list, tu):
"""Annotate a list of genes using Ensembl + SCREEN."""
annotations = {}
for gene in gene_list[:20]: # Limit for API rate
annotation = {'gene': gene}
try:
ens = tu.tools.ensembl_lookup_gene(id=gene, species='homo_sapiens')
if isinstance(ens, dict):
data = ens.get('data', ens)
annotation['ensembl_id'] = data.get('id', 'N/A')
annotation['chr'] = data.get('seq_region_name', 'N/A')
annotation['start'] = data.get('start', 'N/A')
annotation['end'] = data.get('end', 'N/A')
annotation['biotype'] = data.get('biotype', 'N/A')
except Exception:
pass
try:
screen = tu.tools.SCREEN_get_regulatory_elements(
gene_name=gene, element_type="enhancer", limit=5
)
if screen is not None:
annotation['screen_enhancers'] = 'available'
except Exception:
pass
annotations[gene] = annotation
return pd.DataFrame.from_dict(annotations, orient='index')
def query_chipatlas_experiments(antigen, genome='hg38', cell_type=None, tu=None):
"""Query ChIPAtlas for available ChIP-seq experiments."""
if tu is None:
from tooluniverse import ToolUniverse
tu = ToolUniverse()
tu.load_tools()
params = {
'operation': 'get_experiment_list',
'genome': genome,
'antigen': antigen,
'limit': 50,
}
if cell_type:
params['cell_type'] = cell_type
return tu.tools.ChIPAtlas_get_experiments(**params)
def annotate_regions_with_ensembl(regions, species='human', tu=None):
"""Annotate genomic regions with Ensembl regulatory features."""
if tu is None:
from tooluniverse import ToolUniverse
tu = ToolUniverse()
tu.load_tools()
annotations = {}
for chrom, start, end in regions[:10]: # Limit for API rate
ens_chrom = chrom.replace('chr', '') if chrom.startswith('chr') else chrom
region_str = f"{ens_chrom}:{start}-{end}"
try:
result = tu.tools.ensembl_get_regulatory_features(
region=region_str, feature="regulatory", species=species
)
annotations[(chrom, start, end)] = result
except Exception as e:
annotations[(chrom, start, end)] = {'error': str(e)}
return annotations---
Phase 7: Genome-Wide Statistics
def genome_wide_methylation_stats(beta_df, manifest=None, genome='hg38'):
"""Calculate comprehensive genome-wide methylation statistics."""
stats_result = {
'total_probes': len(beta_df),
'total_samples': beta_df.shape[1],
'global_mean_beta': float(beta_df.mean().mean()),
'global_median_beta': float(beta_df.median().median()),
'global_std_beta': float(beta_df.values[~np.isnan(beta_df.values)].std()),
'missing_fraction': float(beta_df.isna().mean().mean()),
}
stats_result['sample_means'] = beta_df.mean(axis=0).describe().to_dict()
probe_var = beta_df.var(axis=1, skipna=True)
stats_result['probe_variance'] = {
'mean': float(probe_var.mean()),
'median': float(probe_var.median()),
'max': float(probe_var.max()),
}
stats_result['high_variance_probes'] = int((probe_var > 0.01).sum())
if manifest is not None:
density_df = chromosome_cpg_density(beta_df.index.tolist(), manifest, genome)
stats_result['chromosome_density'] = density_df.to_dict('records')
stats_result['genome_wide_density'] = genome_wide_average_density(density_df)
return stats_result
def summarize_differential_methylation(dm_results, alpha=0.05):
"""Summarize differential methylation results."""
sig = dm_results[dm_results['padj'] < alpha]
hyper = sig[sig['delta_beta'] > 0]
hypo = sig[sig['delta_beta'] < 0]
return {
'total_tested': len(dm_results),
'total_significant': len(sig),
'hypermethylated': len(hyper),
'hypomethylated': len(hypo),
'fraction_significant': len(sig) / len(dm_results) if len(dm_results) > 0 else 0,
'mean_delta_beta_sig': float(sig['delta_beta'].mean()) if len(sig) > 0 else 0,
'max_abs_delta_beta': float(sig['delta_beta'].abs().max()) if len(sig) > 0 else 0,
}Genomics & Epigenomics Data Processing - Quick Start
Overview
This skill processes epigenomics data files (methylation arrays, ChIP-seq peaks, ATAC-seq data) and answers quantitative questions using pure Python (pandas, scipy, statsmodels) plus ToolUniverse annotation tools. Designed for questions about CpG sites, differential methylation, chromatin accessibility, and multi-omics integration.
---
Quick Start Examples
Example 1: Differential Methylation Analysis
Question: "How many CpG sites show significant differential methylation between tumor and normal?"
import pandas as pd
import numpy as np
from scipy import stats
import statsmodels.stats.multitest as mt
# Load data
beta = pd.read_csv('methylation_beta_values.csv', index_col=0)
clinical = pd.read_csv('clinical_data.csv', index_col=0)
# Define groups
tumor_samples = clinical[clinical['sample_type'] == 'Tumor'].index.tolist()
normal_samples = clinical[clinical['sample_type'] == 'Normal'].index.tolist()
# Filter to common samples
tumor_samples = [s for s in tumor_samples if s in beta.columns]
normal_samples = [s for s in normal_samples if s in beta.columns]
# Differential methylation (vectorized for speed)
g1 = beta[tumor_samples]
g2 = beta[normal_samples]
results = pd.DataFrame({
'mean_tumor': g1.mean(axis=1),
'mean_normal': g2.mean(axis=1),
'delta_beta': g1.mean(axis=1) - g2.mean(axis=1),
})
# T-test per probe
pvalues = []
for probe in beta.index:
vals1 = g1.loc[probe].dropna().values
vals2 = g2.loc[probe].dropna().values
if len(vals1) >= 2 and len(vals2) >= 2:
_, pval = stats.ttest_ind(vals1, vals2, equal_var=False)
pvalues.append(pval)
else:
pvalues.append(np.nan)
results['pvalue'] = pvalues
valid = results['pvalue'].dropna()
reject, padj, _, _ = mt.multipletests(valid.values, method='fdr_bh')
results.loc[valid.index, 'padj'] = padj
# Count significant
n_sig = (results['padj'] < 0.05).sum()
print(f"Significant DMPs (padj < 0.05): {n_sig}")---
Example 2: Age-Related CpG Chromosome Density
Question: "What is the ratio of filtered age-related CpG density between chr19 and chr1?"
import pandas as pd
import numpy as np
from scipy import stats
import statsmodels.stats.multitest as mt
# Load data
beta = pd.read_csv('methylation_beta_values.csv', index_col=0)
manifest = pd.read_csv('probe_manifest.csv')
clinical = pd.read_csv('clinical_data.csv', index_col=0)
# Get ages
ages = clinical['age_at_diagnosis'].values
# Correlate each probe with age
correlations = []
for probe in beta.index:
vals = beta.loc[probe].values
mask = ~np.isnan(vals) & ~np.isnan(ages)
if sum(mask) >= 5:
corr, pval = stats.pearsonr(ages[mask], vals[mask])
correlations.append({'probe': probe, 'corr': corr, 'pvalue': pval})
corr_df = pd.DataFrame(correlations).set_index('probe')
reject, padj, _, _ = mt.multipletests(corr_df['pvalue'].values, method='fdr_bh')
corr_df['padj'] = padj
# Filter significant age-related CpGs
age_cpgs = corr_df[corr_df['padj'] < 0.05].index.tolist()
# Map to chromosomes
def normalize_chr(c):
c = str(c).strip()
return f'chr{c}' if not str(c).startswith('chr') else c
manifest_idx = manifest.set_index('Name') # or 'probe_id'
probe_chrs = manifest_idx.loc[manifest_idx.index.isin(age_cpgs), 'CHR']
probe_chrs = probe_chrs.apply(normalize_chr)
chr_counts = probe_chrs.value_counts()
# Chromosome lengths (hg38)
chr_lengths = {
'chr1': 248956422, 'chr19': 58617616,
# ... (full dict in SKILL.md)
}
# Calculate density
density_19 = chr_counts.get('chr19', 0) / chr_lengths['chr19']
density_1 = chr_counts.get('chr1', 0) / chr_lengths['chr1']
ratio = density_19 / density_1
print(f"chr19/chr1 density ratio: {ratio:.4f}")---
Example 3: Multi-Omics Missing Data Analysis
Question: "How many patients have no missing data for vital status, gene expression, and methylation data?"
import pandas as pd
# Load data
clinical = pd.read_csv('clinical_data.csv', index_col=0)
expression = pd.read_csv('expression_matrix.csv', index_col=0) # genes x samples
methylation = pd.read_csv('methylation_beta.csv', index_col=0) # probes x samples
# Get samples with vital_status
clinical_with_vital = set(clinical[clinical['vital_status'].notna()].index)
# Get samples in expression data
expression_samples = set(expression.columns)
# Get samples in methylation data
methylation_samples = set(methylation.columns)
# Intersection
complete = clinical_with_vital & expression_samples & methylation_samples
print(f"Patients with complete data: {len(complete)}")---
Example 4: ChIP-seq Peak Analysis
Question: "How many ChIP-seq peaks overlap with promoter regions?"
import pandas as pd
# Load peak file
peaks = pd.read_csv('H3K27ac_peaks.narrowPeak', sep='\t', header=None,
names=['chrom', 'start', 'end', 'name', 'score', 'strand',
'signalValue', 'pValue', 'qValue', 'peak'])
# Load gene annotation (or use Ensembl via ToolUniverse)
genes = pd.read_csv('gene_annotation.bed', sep='\t',
names=['chr', 'start', 'end', 'gene_name', 'score', 'strand'])
# Define promoters (TSS +/- 2000bp)
promoters = genes.copy()
promoters['prom_start'] = promoters.apply(
lambda g: g['start'] - 2000 if g['strand'] == '+' else g['end'] - 2000, axis=1)
promoters['prom_end'] = promoters.apply(
lambda g: g['start'] + 500 if g['strand'] == '+' else g['end'] + 500, axis=1)
# Count overlaps (pure Python)
n_promoter_peaks = 0
for _, peak in peaks.iterrows():
chr_proms = promoters[promoters['chr'] == peak['chrom']]
overlap = chr_proms[
(chr_proms['prom_start'] < peak['end']) &
(chr_proms['prom_end'] > peak['start'])
]
if len(overlap) > 0:
n_promoter_peaks += 1
print(f"Peaks in promoters: {n_promoter_peaks}/{len(peaks)} ({100*n_promoter_peaks/len(peaks):.1f}%)")---
Example 5: Genome-Wide CpG Density
Question: "What is the genome-wide average chromosomal density of unique age-related CpGs per base pair?"
# After identifying age-related CpGs and chromosome mapping (Example 2)
total_cpgs = chr_counts.sum()
total_genome_length = sum(chr_lengths[c] for c in chr_counts.index if c in chr_lengths)
genome_wide_density = total_cpgs / total_genome_length
print(f"Genome-wide density: {genome_wide_density:.2e} CpGs/bp")---
ToolUniverse Annotation
Use ToolUniverse for biological context after computational analysis:
from tooluniverse import ToolUniverse
tu = ToolUniverse()
tu.load_tools()
# Annotate genes from differential methylation
gene = "TP53"
ens = tu.tools.ensembl_lookup_gene(id=gene, species='homo_sapiens')
# Get regulatory elements near a gene
screen = tu.tools.SCREEN_get_regulatory_elements(
gene_name="TP53", element_type="enhancer", limit=10
)
# Find ChIP-seq experiments for histone mark
chipatlas = tu.tools.ChIPAtlas_get_experiments(
operation="get_experiment_list",
genome="hg38",
antigen="H3K27ac",
limit=20
)
# Get regulatory features for a region
ensembl_reg = tu.tools.ensembl_get_regulatory_features(
region="17:7661779-7687550", # No "chr" prefix
feature="regulatory",
species="human"
)---
Key Functions Reference
| Function | Purpose | Input | Output |
|---|---|---|---|
load_methylation_data() | Load beta/M-value matrix | file path | DataFrame |
detect_methylation_type() | Detect beta vs M-values | DataFrame | 'beta' or 'mvalue' |
filter_cpg_probes() | Filter probes by criteria | DataFrame + filters | filtered DataFrame |
differential_methylation() | DM analysis between groups | beta + samples | DataFrame with padj |
identify_age_related_cpgs() | Age-correlated CpGs | beta + ages | DataFrame with padj |
chromosome_cpg_density() | CpG density per chromosome | probes + manifest | density DataFrame |
genome_wide_average_density() | Overall genome density | density DataFrame | float |
chromosome_density_ratio() | Ratio between chromosomes | density + chr names | float |
load_bed_file() | Load BED/narrowPeak | file path | DataFrame |
peak_statistics() | Basic peak stats | BED DataFrame | dict |
annotate_peaks_to_genes() | Annotate peaks to genes | peaks + genes | annotated DataFrame |
find_overlaps() | Peak overlap analysis | two BED DataFrames | overlap DataFrame |
missing_data_analysis() | Cross-modality completeness | multiple DataFrames | dict |
correlate_methylation_expression() | Meth-expression correlation | beta + expression | correlation DataFrame |
---
Genome Builds Supported
| Build | Species | Autosomes | Sex Chromosomes |
|---|---|---|---|
| hg38 (GRCh38) | Human | chr1-chr22 | chrX, chrY |
| hg19 (GRCh37) | Human | chr1-chr22 | chrX, chrY |
| mm10 (GRCm38) | Mouse | chr1-chr19 | chrX, chrY |
#!/usr/bin/env python3
"""Compute methylation CpG-density statistics from a long-format methylation CSV.
Long-format methylation CSVs typically have one row per (sample × CpG site),
so `len(df)` >> `n_unique_sites`. Several published metrics aggregate across
chromosomes, sites, or samples in subtly different ways. This script makes
those aggregations explicit and deterministic.
Usage:
python methylation_density.py \\
--cpg <CpG csv with Pos, Chromosome, MethylationPercentage> \\
--chr-lengths <CSV with Chromosome, Length> \\
[--filter-meth-extremes 90 10] \\
[--chromosome Z] \\
[--report all]
What it computes (printed in JSON to stdout):
rows_total # all rows (sample-positions)
rows_after_filter # after extreme-methylation filter
rows_removed # rows filtered out (= rows_total - rows_after_filter)
unique_pos_total # unique CpG positions (deduped by Pos)
unique_pos_after_filter # unique positions where any row passed filter
unique_pos_removed # unique positions where NO row passed filter
total_genome_length # sum of all chromosome lengths in the chr-lengths file
density_total_over_genome # unique_pos_after_filter / total_genome_length
density_avg_per_chr # mean of (unique_pos_after_filter[c] / chr_length[c]) over chromosomes
per_chr # dict of chromosome → {n_cpgs, length, density}
density_chromosome # if --chromosome X given: unique_pos_after_filter[X] / chr_length[X]
chisquare_uniform # chi-square test that per-chromosome CpG counts
# match a length-proportional ("uniform density")
# expectation: {statistic, df, p_value, n_chr}
Question-to-metric mapping (general):
"How many sites are removed when filtering ..." → rows_removed (sample-row level)
"How many unique CpG sites pass filter" → unique_pos_after_filter
"Genome-wide AVERAGE chromosomal density" → density_avg_per_chr (mean of per-chr)
"Density on chromosome X" → density_chromosome (single-chr)
"Total density across genome" → density_total_over_genome
"Chi-square stat for uniform distribution" → chisquare_uniform.statistic
The chi-square uniformity test mirrors the canonical R idiom
`chisq.test(n_cpgs, p = expected/sum(expected))` where expected counts are
length-proportional (chromosomes with no length match are dropped first).
The "average chromosomal density" metric is the per-chromosome mean,
not total/total — these differ when CpGs are unevenly distributed.
"""
import argparse
import json
import sys
from pathlib import Path
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--cpg", required=True, type=Path,
help="Methylation CSV with Pos, Chromosome, MethylationPercentage columns")
ap.add_argument("--chr-lengths", required=True, type=Path,
help="CSV with Chromosome, Length columns")
ap.add_argument("--filter-meth-extremes", nargs=2, type=float,
metavar=("HIGH", "LOW"),
help="Keep rows with MethylationPercentage > HIGH or < LOW. "
"Typical: 90 10")
ap.add_argument("--chromosome",
help="Compute density on this single chromosome")
ap.add_argument("--report", default="all",
choices=["all", "rows_removed", "density_avg_per_chr",
"density_chromosome", "unique_pos_after_filter"],
help="Which metric to print on stdout (default: all as JSON)")
args = ap.parse_args()
try:
import pandas as pd
except ImportError:
sys.exit("ERROR: pandas required (pip install pandas)")
df = pd.read_csv(args.cpg)
chr_lens = pd.read_csv(args.chr_lengths)
chr_lens["Chromosome"] = chr_lens["Chromosome"].astype(str)
df["Chromosome"] = df["Chromosome"].astype(str)
rows_total = len(df)
unique_pos_total = df["Pos"].nunique() if "Pos" in df.columns else None
if args.filter_meth_extremes:
high, low = args.filter_meth_extremes
kept = df[(df["MethylationPercentage"] > high) | (df["MethylationPercentage"] < low)]
else:
kept = df
rows_after_filter = len(kept)
rows_removed = rows_total - rows_after_filter
unique_pos_after_filter = kept["Pos"].nunique() if "Pos" in kept.columns else None
unique_pos_removed = (unique_pos_total - unique_pos_after_filter
if unique_pos_total is not None else None)
# Per-chromosome unique positions (after filter) AND raw row counts
by_chr = (kept.groupby("Chromosome")["Pos"].nunique()
.reset_index().rename(columns={"Pos": "n_cpgs"}))
by_chr_rows = (kept.groupby("Chromosome").size()
.reset_index(name="n_rows"))
by_chr = by_chr.merge(by_chr_rows, on="Chromosome", how="left")
by_chr = by_chr.merge(chr_lens, on="Chromosome", how="left").dropna(subset=["Length"])
by_chr["density"] = by_chr["n_cpgs"] / by_chr["Length"]
total_genome_length = float(chr_lens["Length"].sum())
density_total_over_genome = (
unique_pos_after_filter / total_genome_length
if unique_pos_after_filter is not None else None
)
density_avg_per_chr = float(by_chr["density"].mean()) if len(by_chr) else None
density_chromosome = None
density_chromosome_over_genome_rows = None
density_chromosome_over_genome_unique = None
if args.chromosome:
target = str(args.chromosome).upper()
sub = by_chr[by_chr["Chromosome"].str.upper() == target]
if not sub.empty:
density_chromosome = float(sub["density"].iloc[0])
# Alternative interpretation: per-chromosome filtered count
# divided by TOTAL genome length (e.g. "density of chrZ CpGs in
# the Zebra Finch genome" — when 'in the X genome' implies a
# genome-wide denominator instead of per-chromosome).
density_chromosome_over_genome_rows = float(
sub["n_rows"].iloc[0] / total_genome_length
)
density_chromosome_over_genome_unique = float(
sub["n_cpgs"].iloc[0] / total_genome_length
)
per_chr = {row["Chromosome"]: {
"n_cpgs": int(row["n_cpgs"]),
"n_rows": int(row["n_rows"]),
"length": int(row["Length"]),
"density": float(row["density"]),
"density_over_genome_rows": float(row["n_rows"] / total_genome_length),
"density_over_genome_unique": float(row["n_cpgs"] / total_genome_length),
} for _, row in by_chr.iterrows()}
# Chi-square test: do per-chromosome CpG counts match a length-proportional
# ("uniform density") expectation? Mirrors the canonical R idiom
# chisq.test(n_cpgs, p = expected/sum(expected)).
chisquare_uniform = None
if len(by_chr) >= 2:
try:
from scipy.stats import chisquare as _chisquare
observed = by_chr["n_cpgs"].astype(float)
total_cpgs = float(observed.sum())
total_len = float(by_chr["Length"].sum())
expected = by_chr["Length"].astype(float) * (total_cpgs / total_len)
expected = expected / expected.sum() * total_cpgs
stat, pval = _chisquare(observed.values, expected.values)
chisquare_uniform = {
"statistic": float(stat),
"df": int(len(by_chr) - 1),
"p_value": float(pval),
"n_chr": int(len(by_chr)),
}
except ImportError:
chisquare_uniform = {"error": "scipy required for chi-square test"}
result = {
"rows_total": rows_total,
"rows_after_filter": rows_after_filter,
"rows_removed": rows_removed,
"unique_pos_total": unique_pos_total,
"unique_pos_after_filter": unique_pos_after_filter,
"unique_pos_removed": unique_pos_removed,
"total_genome_length": total_genome_length,
"density_total_over_genome": density_total_over_genome,
"density_avg_per_chr": density_avg_per_chr,
"density_chromosome": density_chromosome,
"density_chromosome_over_genome_rows": density_chromosome_over_genome_rows,
"density_chromosome_over_genome_unique": density_chromosome_over_genome_unique,
"chisquare_uniform": chisquare_uniform,
"per_chr": per_chr,
}
if args.report == "all":
print(json.dumps(result, indent=2))
else:
v = result.get(args.report)
print(v)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Comprehensive Test Suite for tooluniverse-epigenomics Skill
Tests all computational epigenomics capabilities:
- Methylation data processing (beta values, CpG filtering, DM analysis)
- Age-related CpG analysis
- Chromosome-level statistics and density calculations
- BED/peak file processing
- Peak annotation and overlap analysis
- Multi-omics integration (methylation-expression correlation)
- Clinical data missing data analysis
- Genome-wide statistics
- ToolUniverse annotation integration
"""
import sys
import os
import time
import traceback
import tempfile
import pandas as pd
import numpy as np
from scipy import stats
import statsmodels.stats.multitest as mt
# ============================================================
# Test Infrastructure
# ============================================================
RESULTS = []
def run_test(test_func):
"""Run a test function and record results."""
name = test_func.__name__
doc = test_func.__doc__ or name
start = time.time()
try:
test_func()
elapsed = time.time() - start
RESULTS.append({"name": name, "doc": doc.strip(), "status": "PASS", "time": elapsed, "error": None})
print(f" PASS {name} ({elapsed:.1f}s)")
except Exception as e:
elapsed = time.time() - start
error_msg = f"{type(e).__name__}: {e}"
RESULTS.append({"name": name, "doc": doc.strip(), "status": "FAIL", "time": elapsed, "error": error_msg})
print(f" FAIL {name} ({elapsed:.1f}s): {error_msg}")
traceback.print_exc()
# ============================================================
# Helper: Generate Synthetic Test Data
# ============================================================
def generate_methylation_data(n_probes=500, n_samples=20, seed=42):
"""Generate synthetic methylation beta-value matrix."""
np.random.seed(seed)
probes = [f"cg{str(i).zfill(8)}" for i in range(n_probes)]
samples = [f"SAMPLE_{str(i).zfill(3)}" for i in range(n_samples)]
betas = np.random.beta(2, 5, size=(n_probes, n_samples))
return pd.DataFrame(betas, index=probes, columns=samples)
def generate_manifest(probes, seed=42):
"""Generate synthetic probe manifest with chromosome and position."""
np.random.seed(seed)
n = len(probes)
chromosomes = np.random.choice(
['1', '2', '3', '4', '5', '6', '7', '8', '9', '10',
'11', '12', '13', '14', '15', '16', '17', '18', '19',
'20', '21', '22', 'X', 'Y'],
size=n
)
positions = np.random.randint(1000000, 200000000, size=n)
gene_names = np.random.choice(
['TP53', 'BRCA1', 'EGFR', 'MYC', 'PTEN', 'RB1', 'AKT1',
'CDK4', 'MDM2', 'KRAS', '', ''], size=n
)
gene_groups = np.random.choice(
['TSS200', 'TSS1500', 'Body', '1stExon', '5UTR', '3UTR', ''],
size=n
)
island_relations = np.random.choice(
['Island', 'N_Shore', 'S_Shore', 'N_Shelf', 'S_Shelf', 'OpenSea'],
size=n
)
return pd.DataFrame({
'probe_id': probes,
'chr': chromosomes,
'position': positions,
'gene_name': gene_names,
'gene_group': gene_groups,
'cpg_island_relation': island_relations,
})
def generate_bed_file(n_peaks=200, seed=42):
"""Generate synthetic BED DataFrame (narrowPeak format)."""
np.random.seed(seed)
chroms = np.random.choice(
[f'chr{i}' for i in range(1, 23)] + ['chrX'],
size=n_peaks
)
starts = np.random.randint(1000000, 200000000, size=n_peaks)
lengths = np.random.randint(100, 5000, size=n_peaks)
ends = starts + lengths
signals = np.random.exponential(10, size=n_peaks)
pvalues = np.random.uniform(0, 50, size=n_peaks) # -log10(p)
qvalues = np.random.uniform(0, 30, size=n_peaks) # -log10(q)
peak_offsets = lengths // 2
return pd.DataFrame({
'chrom': chroms,
'start': starts,
'end': ends,
'name': [f'peak_{i}' for i in range(n_peaks)],
'score': np.random.randint(0, 1000, size=n_peaks),
'strand': '.',
'signalValue': signals,
'pValue': pvalues,
'qValue': qvalues,
'peak': peak_offsets,
})
def generate_gene_annotation(n_genes=100, seed=42):
"""Generate synthetic gene annotation."""
np.random.seed(seed)
gene_names = [f'GENE_{i}' for i in range(n_genes)]
chroms = np.random.choice(
[f'chr{i}' for i in range(1, 23)] + ['chrX'],
size=n_genes
)
starts = np.random.randint(1000000, 200000000, size=n_genes)
ends = starts + np.random.randint(1000, 100000, size=n_genes)
strands = np.random.choice(['+', '-'], size=n_genes)
return pd.DataFrame({
'chr': chroms,
'start': starts,
'end': ends,
'gene_name': gene_names,
'score': 0,
'strand': strands,
})
# ============================================================
# Phase 1: Methylation Data Loading Tests
# ============================================================
def test_01_generate_and_load_beta_matrix():
"""Methylation: Generate and validate beta-value matrix"""
beta = generate_methylation_data(n_probes=500, n_samples=20)
assert beta.shape == (500, 20), f"Expected (500, 20), got {beta.shape}"
assert beta.values.min() >= 0, "Beta values should be >= 0"
assert beta.values.max() <= 1, "Beta values should be <= 1"
assert all(beta.index.str.startswith('cg')), "Probes should start with 'cg'"
print(f" Shape: {beta.shape}, range: [{beta.values.min():.3f}, {beta.values.max():.3f}]")
def test_02_detect_methylation_type():
"""Methylation: Detect beta vs M-value data type"""
beta = generate_methylation_data()
# Detect beta values
sample = beta.iloc[:1000, :5].values.flatten()
sample = sample[~np.isnan(sample)]
is_beta = sample.min() >= 0 and sample.max() <= 1
assert is_beta, "Should detect as beta values"
# Convert to M-values
beta_clipped = np.clip(beta.values, 1e-6, 1 - 1e-6)
mvalues = pd.DataFrame(
np.log2(beta_clipped / (1 - beta_clipped)),
index=beta.index, columns=beta.columns
)
sample_m = mvalues.iloc[:1000, :5].values.flatten()
sample_m = sample_m[~np.isnan(sample_m)]
is_mvalue = not (sample_m.min() >= 0 and sample_m.max() <= 1)
assert is_mvalue, "M-values should not be bounded 0-1"
print(f" Beta range: [{sample.min():.3f}, {sample.max():.3f}]")
print(f" M-value range: [{sample_m.min():.3f}, {sample_m.max():.3f}]")
def test_03_beta_mvalue_conversion():
"""Methylation: Convert between beta and M-values"""
beta = generate_methylation_data(n_probes=100, n_samples=5)
# Beta -> M-value
beta_clipped = np.clip(beta.values, 1e-6, 1 - 1e-6)
mvalues = np.log2(beta_clipped / (1 - beta_clipped))
# M-value -> Beta (round-trip)
beta_recovered = 2**mvalues / (2**mvalues + 1)
# Should be close to original (within floating point)
diff = np.abs(beta.values - beta_recovered).max()
assert diff < 1e-5, f"Round-trip conversion error: {diff}"
print(f" Max round-trip error: {diff:.2e}")
def test_04_csv_methylation_io():
"""Methylation: Write and read CSV methylation data"""
beta = generate_methylation_data(n_probes=100, n_samples=10)
with tempfile.NamedTemporaryFile(suffix='.csv', delete=False, mode='w') as f:
beta.to_csv(f.name)
loaded = pd.read_csv(f.name, index_col=0)
os.unlink(f.name)
assert loaded.shape == beta.shape, f"Shape mismatch: {loaded.shape} vs {beta.shape}"
assert np.allclose(loaded.values, beta.values, atol=1e-10), "Values mismatch after CSV round-trip"
print(f" CSV round-trip successful: {loaded.shape}")
def test_05_tsv_methylation_io():
"""Methylation: Write and read TSV methylation data"""
beta = generate_methylation_data(n_probes=100, n_samples=10)
with tempfile.NamedTemporaryFile(suffix='.tsv', delete=False, mode='w') as f:
beta.to_csv(f.name, sep='\t')
loaded = pd.read_csv(f.name, sep='\t', index_col=0)
os.unlink(f.name)
assert loaded.shape == beta.shape
print(f" TSV round-trip successful: {loaded.shape}")
# ============================================================
# Phase 2: CpG Filtering Tests
# ============================================================
def test_06_filter_by_probe_type():
"""CpG Filter: Filter by probe type (cg vs ch)"""
np.random.seed(42)
# Create mixed probe list
probes = [f"cg{str(i).zfill(8)}" for i in range(300)] + \
[f"ch{str(i).zfill(8)}" for i in range(200)]
beta = pd.DataFrame(
np.random.beta(2, 5, size=(500, 10)),
index=probes,
columns=[f"S{i}" for i in range(10)]
)
# Filter cg only
cg_mask = beta.index.str.startswith('cg')
filtered = beta[cg_mask]
assert len(filtered) == 300, f"Expected 300 cg probes, got {len(filtered)}"
assert all(filtered.index.str.startswith('cg'))
# Filter ch only
ch_mask = beta.index.str.startswith('ch')
filtered_ch = beta[ch_mask]
assert len(filtered_ch) == 200
print(f" cg probes: {len(filtered)}, ch probes: {len(filtered_ch)}")
def test_07_filter_by_variance():
"""CpG Filter: Filter probes by variance threshold"""
beta = generate_methylation_data(n_probes=500, n_samples=20)
probe_var = beta.var(axis=1, skipna=True)
threshold = 0.01
high_var = beta[probe_var >= threshold]
low_var = beta[probe_var < threshold]
assert len(high_var) + len(low_var) == 500
assert len(high_var) > 0, "Should have some high-variance probes"
print(f" Variance >= {threshold}: {len(high_var)} probes")
print(f" Variance < {threshold}: {len(low_var)} probes")
def test_08_filter_by_missing_data():
"""CpG Filter: Filter probes with too much missing data"""
beta = generate_methylation_data(n_probes=500, n_samples=20)
# Introduce missing data
np.random.seed(99)
mask = np.random.random(beta.shape) < 0.1 # 10% missing
beta_with_na = beta.copy()
beta_with_na.values[mask] = np.nan
# Make some probes entirely missing
beta_with_na.iloc[:10, :] = np.nan
missing_frac = beta_with_na.isna().mean(axis=1)
filtered = beta_with_na[missing_frac <= 0.2] # max 20% missing
assert len(filtered) < 500, "Should remove some probes"
assert len(filtered) > 400, "Should keep most probes"
print(f" After missing filter (<=20%): {len(filtered)} / 500 probes")
def test_09_filter_by_mean_beta_range():
"""CpG Filter: Filter probes by mean beta range"""
beta = generate_methylation_data(n_probes=500, n_samples=20)
probe_mean = beta.mean(axis=1)
# Keep probes with mean between 0.1 and 0.9
filtered = beta[(probe_mean >= 0.1) & (probe_mean <= 0.9)]
assert len(filtered) > 0, "Should have probes in range"
assert len(filtered) <= 500
remaining_means = filtered.mean(axis=1)
assert remaining_means.min() >= 0.1
assert remaining_means.max() <= 0.9
print(f" Mean beta [0.1, 0.9]: {len(filtered)} / 500 probes")
def test_10_filter_top_n_variable():
"""CpG Filter: Select top N most variable probes"""
beta = generate_methylation_data(n_probes=500, n_samples=20)
probe_var = beta.var(axis=1, skipna=True)
n = 100
top_probes = probe_var.nlargest(n).index
filtered = beta.loc[top_probes]
assert len(filtered) == n, f"Expected {n} probes, got {len(filtered)}"
# Verify these are truly the most variable
min_var_selected = probe_var.loc[top_probes].min()
max_var_not_selected = probe_var.loc[~probe_var.index.isin(top_probes)].max()
assert min_var_selected >= max_var_not_selected, "Top N selection incorrect"
print(f" Top {n} most variable probes selected")
def test_11_filter_by_chromosome():
"""CpG Filter: Filter probes by chromosome using manifest"""
beta = generate_methylation_data(n_probes=500, n_samples=10)
manifest = generate_manifest(beta.index.tolist())
# Keep only autosomes (exclude X, Y)
def normalize_chromosome(c):
c = str(c).strip()
return f'chr{c}' if not str(c).startswith('chr') else c
manifest_idx = manifest.set_index('probe_id')
nonsex = manifest_idx[
~manifest_idx['chr'].apply(normalize_chromosome).isin(['chrX', 'chrY'])
]
autosome_probes = nonsex.index
filtered = beta[beta.index.isin(autosome_probes)]
n_sex = len(beta) - len(filtered)
assert len(filtered) < 500, "Should remove some probes"
assert len(filtered) > 0
print(f" Autosome probes: {len(filtered)}, removed sex chr: {n_sex}")
# ============================================================
# Phase 3: Differential Methylation Tests
# ============================================================
def test_12_differential_methylation_ttest():
"""DM Analysis: T-test differential methylation between two groups"""
np.random.seed(42)
n_probes = 200
n_samples_per_group = 10
# Group 1: beta ~ Beta(2, 5)
g1_data = np.random.beta(2, 5, size=(n_probes, n_samples_per_group))
# Group 2: shift some probes (first 20 are differentially methylated)
g2_data = np.random.beta(2, 5, size=(n_probes, n_samples_per_group))
g2_data[:20, :] += 0.3 # Add offset to first 20 probes
g2_data = np.clip(g2_data, 0, 1)
probes = [f"cg{str(i).zfill(8)}" for i in range(n_probes)]
g1_samples = [f"G1_{i}" for i in range(n_samples_per_group)]
g2_samples = [f"G2_{i}" for i in range(n_samples_per_group)]
beta = pd.DataFrame(
np.hstack([g1_data, g2_data]),
index=probes,
columns=g1_samples + g2_samples
)
# Run DM analysis
results = []
for probe in beta.index:
vals1 = beta.loc[probe, g1_samples].values
vals2 = beta.loc[probe, g2_samples].values
mean1 = np.mean(vals1)
mean2 = np.mean(vals2)
_, pval = stats.ttest_ind(vals1, vals2, equal_var=False)
results.append({
'probe': probe, 'mean_g1': mean1, 'mean_g2': mean2,
'delta_beta': mean2 - mean1, 'pvalue': pval
})
dm = pd.DataFrame(results).set_index('probe')
reject, padj, _, _ = mt.multipletests(dm['pvalue'].values, method='fdr_bh')
dm['padj'] = padj
sig = dm[dm['padj'] < 0.05]
assert len(sig) > 5, f"Expected >5 significant DMPs, got {len(sig)}"
# Most significant should be in the first 20
top_sig = sig.head(10).index
n_correct = sum(int(p.replace('cg', '')) < 20 for p in top_sig)
print(f" Significant DMPs: {len(sig)} / {n_probes}")
print(f" Top 10 from true DMPs: {n_correct}/10")
def test_13_differential_methylation_wilcoxon():
"""DM Analysis: Wilcoxon rank-sum test (non-parametric)"""
np.random.seed(42)
n_probes = 100
g1 = np.random.beta(2, 5, size=(n_probes, 8))
g2 = np.random.beta(2, 5, size=(n_probes, 8))
g2[:10, :] += 0.25
g2 = np.clip(g2, 0, 1)
pvals = []
for i in range(n_probes):
_, pval = stats.mannwhitneyu(g1[i], g2[i], alternative='two-sided')
pvals.append(pval)
reject, padj, _, _ = mt.multipletests(pvals, method='fdr_bh')
n_sig = sum(reject)
print(f" Wilcoxon significant DMPs: {n_sig} / {n_probes}")
assert n_sig >= 0, "Test should complete without error"
def test_14_identify_dmps_with_threshold():
"""DM Analysis: Filter DMPs by padj and delta_beta thresholds"""
np.random.seed(42)
n_probes = 200
g1 = np.random.beta(2, 5, size=(n_probes, 10))
g2 = np.random.beta(2, 5, size=(n_probes, 10))
g2[:30, :] += 0.3 # Large effect
g2[30:50, :] += 0.05 # Small effect
g2 = np.clip(g2, 0, 1)
means_g1 = g1.mean(axis=1)
means_g2 = g2.mean(axis=1)
delta_beta = means_g2 - means_g1
pvals = []
for i in range(n_probes):
_, pval = stats.ttest_ind(g1[i], g2[i], equal_var=False)
pvals.append(pval)
reject, padj, _, _ = mt.multipletests(pvals, method='fdr_bh')
# Apply both thresholds
sig_mask = (padj < 0.05) & (np.abs(delta_beta) >= 0.2)
n_sig = sum(sig_mask)
print(f" padj < 0.05 only: {sum(padj < 0.05)}")
print(f" padj < 0.05 AND |delta_beta| >= 0.2: {n_sig}")
assert n_sig <= sum(padj < 0.05), "Adding threshold should not increase count"
def test_15_hyper_hypo_classification():
"""DM Analysis: Classify DMPs as hyper- or hypo-methylated"""
np.random.seed(42)
delta_betas = np.array([0.3, -0.25, 0.15, -0.4, 0.0, 0.1, -0.05])
padj_vals = np.array([0.001, 0.01, 0.03, 0.001, 0.5, 0.04, 0.02])
sig = padj_vals < 0.05
hyper = sig & (delta_betas > 0)
hypo = sig & (delta_betas < 0)
assert sum(hyper) == 3, f"Expected 3 hyper, got {sum(hyper)}"
assert sum(hypo) == 3, f"Expected 3 hypo, got {sum(hypo)}"
print(f" Hyper: {sum(hyper)}, Hypo: {sum(hypo)}, NS: {sum(~sig)}")
# ============================================================
# Phase 4: Age-Related CpG Tests
# ============================================================
def test_16_age_correlation():
"""Age CpG: Pearson correlation with age"""
np.random.seed(42)
n_probes = 100
n_samples = 50
ages = np.random.uniform(20, 80, size=n_samples)
# Create probes: first 10 correlated with age
betas = np.random.beta(2, 5, size=(n_probes, n_samples))
for i in range(10):
betas[i, :] = 0.2 + 0.005 * ages + np.random.normal(0, 0.05, n_samples)
betas[i, :] = np.clip(betas[i, :], 0, 1)
probes = [f"cg{str(i).zfill(8)}" for i in range(n_probes)]
correlations = []
for i in range(n_probes):
corr, pval = stats.pearsonr(ages, betas[i, :])
correlations.append({'probe': probes[i], 'correlation': corr, 'pvalue': pval})
corr_df = pd.DataFrame(correlations).set_index('probe')
reject, padj, _, _ = mt.multipletests(corr_df['pvalue'].values, method='fdr_bh')
corr_df['padj'] = padj
sig = corr_df[corr_df['padj'] < 0.05]
print(f" Age-related CpGs: {len(sig)} / {n_probes}")
# First 10 probes should be significant
top_sig = sig.index.tolist()
n_correct = sum(int(p.replace('cg', '')) < 10 for p in top_sig)
print(f" Correctly identified: {n_correct}/10 true age-related probes")
assert n_correct >= 5, "Should detect most true age-related probes"
def test_17_spearman_correlation():
"""Age CpG: Spearman rank correlation with age"""
np.random.seed(42)
n = 50
ages = np.random.uniform(20, 80, size=n)
betas = 0.2 + 0.005 * ages + np.random.normal(0, 0.05, n)
betas = np.clip(betas, 0, 1)
corr_pearson, pval_pearson = stats.pearsonr(ages, betas)
corr_spearman, pval_spearman = stats.spearmanr(ages, betas)
assert abs(corr_pearson) > 0.3, "Pearson correlation should be moderate"
assert abs(corr_spearman) > 0.3, "Spearman correlation should be moderate"
print(f" Pearson r={corr_pearson:.3f}, p={pval_pearson:.2e}")
print(f" Spearman rho={corr_spearman:.3f}, p={pval_spearman:.2e}")
# ============================================================
# Phase 5: Chromosome Statistics Tests
# ============================================================
def test_18_chromosome_normalization():
"""Chromosome: Normalize chromosome names"""
def normalize_chromosome(chrom):
if chrom is None or pd.isna(chrom):
return None
chrom = str(chrom).strip()
if not chrom.startswith('chr'):
chrom = 'chr' + chrom
return chrom
assert normalize_chromosome('1') == 'chr1'
assert normalize_chromosome('chr1') == 'chr1'
assert normalize_chromosome('X') == 'chrX'
assert normalize_chromosome('chrX') == 'chrX'
assert normalize_chromosome('22') == 'chr22'
assert normalize_chromosome(None) is None
print(f" All chromosome normalizations correct")
def test_19_chromosome_lengths():
"""Chromosome: Verify chromosome lengths for hg38, hg19, mm10"""
hg38 = {
'chr1': 248956422, 'chr2': 242193529, 'chr17': 83257441,
'chr19': 58617616, 'chrX': 156040895, 'chrY': 57227415,
}
hg19 = {
'chr1': 249250621, 'chr17': 81195210, 'chr19': 59128983,
}
mm10 = {
'chr1': 195471971, 'chr19': 61431566,
}
# Verify key lengths
assert hg38['chr1'] == 248956422
assert hg38['chr19'] == 58617616
assert hg19['chr1'] == 249250621
assert mm10['chr1'] == 195471971
print(f" hg38 chr1: {hg38['chr1']:,} bp")
print(f" hg38 chr19: {hg38['chr19']:,} bp")
print(f" hg19 chr1: {hg19['chr1']:,} bp")
def test_20_cpg_density_per_chromosome():
"""Chromosome: Calculate CpG density per chromosome"""
beta = generate_methylation_data(n_probes=500, n_samples=10)
manifest = generate_manifest(beta.index.tolist())
def normalize_chromosome(c):
c = str(c).strip()
return f'chr{c}' if not str(c).startswith('chr') else c
chr_lengths = {
'chr1': 248956422, 'chr2': 242193529, 'chr3': 198295559,
'chr17': 83257441, 'chr19': 58617616, 'chrX': 156040895,
'chrY': 57227415,
}
# Map probes to chromosomes
manifest_idx = manifest.set_index('probe_id')
probe_chrs = manifest_idx['chr'].apply(normalize_chromosome)
chr_counts = probe_chrs.value_counts()
# Calculate density
densities = {}
for chrom, count in chr_counts.items():
if chrom in chr_lengths:
densities[chrom] = count / chr_lengths[chrom]
assert len(densities) > 0, "Should have density for at least one chromosome"
for chrom, density in densities.items():
assert density > 0, f"Density for {chrom} should be > 0"
assert density < 1, f"Density for {chrom} should be < 1"
print(f" Densities calculated for {len(densities)} chromosomes")
for chrom in sorted(list(densities.keys()))[:3]:
print(f" {chrom}: {densities[chrom]:.2e} CpGs/bp")
def test_21_genome_wide_average_density():
"""Chromosome: Genome-wide average CpG density"""
# Simulated density data
density_data = pd.DataFrame({
'chr': ['chr1', 'chr2', 'chr19'],
'n_cpgs': [100, 80, 60],
'chr_length': [248956422, 242193529, 58617616],
})
density_data['density_per_bp'] = density_data['n_cpgs'] / density_data['chr_length']
total_cpgs = density_data['n_cpgs'].sum()
total_length = density_data['chr_length'].sum()
avg_density = total_cpgs / total_length
assert avg_density > 0
assert avg_density < 1
print(f" Total CpGs: {total_cpgs}")
print(f" Total genome length: {total_length:,}")
print(f" Average density: {avg_density:.2e}")
def test_22_chromosome_density_ratio():
"""Chromosome: Density ratio between two chromosomes"""
density_data = pd.DataFrame({
'chr': ['chr1', 'chr19'],
'n_cpgs': [100, 60],
'chr_length': [248956422, 58617616],
})
density_data['density_per_bp'] = density_data['n_cpgs'] / density_data['chr_length']
d1 = density_data[density_data['chr'] == 'chr1']['density_per_bp'].values[0]
d19 = density_data[density_data['chr'] == 'chr19']['density_per_bp'].values[0]
ratio_19_to_1 = d19 / d1
assert ratio_19_to_1 > 1, "chr19 should be denser (it is smaller)"
print(f" chr1 density: {d1:.2e}")
print(f" chr19 density: {d19:.2e}")
print(f" chr19/chr1 ratio: {ratio_19_to_1:.2f}")
# ============================================================
# Phase 6: BED/Peak File Tests
# ============================================================
def test_23_load_bed_dataframe():
"""BED: Load and validate BED-like DataFrame"""
peaks = generate_bed_file(n_peaks=200)
assert len(peaks) == 200
assert 'chrom' in peaks.columns
assert 'start' in peaks.columns
assert 'end' in peaks.columns
assert all(peaks['start'] < peaks['end']), "Start should be < end"
print(f" Loaded {len(peaks)} peaks across {peaks['chrom'].nunique()} chromosomes")
def test_24_peak_statistics():
"""BED: Calculate peak statistics"""
peaks = generate_bed_file(n_peaks=200)
peaks['length'] = peaks['end'] - peaks['start']
stats_dict = {
'total_peaks': len(peaks),
'mean_peak_length': peaks['length'].mean(),
'median_peak_length': peaks['length'].median(),
'total_coverage_bp': peaks['length'].sum(),
}
assert stats_dict['total_peaks'] == 200
assert stats_dict['mean_peak_length'] > 0
assert stats_dict['total_coverage_bp'] > 0
print(f" Total peaks: {stats_dict['total_peaks']}")
print(f" Mean length: {stats_dict['mean_peak_length']:.0f} bp")
print(f" Total coverage: {stats_dict['total_coverage_bp']:,} bp")
def test_25_peaks_per_chromosome():
"""BED: Count peaks per chromosome"""
peaks = generate_bed_file(n_peaks=500)
chr_counts = peaks['chrom'].value_counts()
assert len(chr_counts) > 5, "Should have peaks on multiple chromosomes"
assert chr_counts.sum() == 500
print(f" Peaks across {len(chr_counts)} chromosomes")
print(f" Top 3: {dict(chr_counts.head(3))}")
def test_26_write_and_read_bed():
"""BED: Write and read BED file"""
peaks = generate_bed_file(n_peaks=50)
with tempfile.NamedTemporaryFile(suffix='.bed', delete=False, mode='w') as f:
peaks.to_csv(f.name, sep='\t', header=False, index=False)
# Read back
loaded = pd.read_csv(
f.name, sep='\t', header=None,
names=['chrom', 'start', 'end', 'name', 'score', 'strand',
'signalValue', 'pValue', 'qValue', 'peak']
)
os.unlink(f.name)
assert len(loaded) == 50
assert loaded['chrom'].iloc[0] == peaks['chrom'].iloc[0]
print(f" BED round-trip successful: {len(loaded)} peaks")
# ============================================================
# Phase 7: Peak Annotation Tests
# ============================================================
def test_27_peak_to_gene_annotation():
"""Peak Annotation: Annotate peaks to nearest genes"""
peaks = generate_bed_file(n_peaks=50)
genes = generate_gene_annotation(n_genes=100)
# Simple annotation: find nearest gene on same chromosome
annotated = []
for _, peak in peaks.iterrows():
chr_genes = genes[genes['chr'] == peak['chrom']]
if len(chr_genes) == 0:
annotated.append({'nearest_gene': 'intergenic', 'feature': 'intergenic'})
continue
peak_mid = (peak['start'] + peak['end']) // 2
tss = chr_genes.apply(
lambda g: g['start'] if g['strand'] == '+' else g['end'], axis=1)
distances = (peak_mid - tss).abs()
nearest = chr_genes.loc[distances.idxmin()]
dist = distances.min()
if dist <= 2000:
feature = 'promoter'
elif peak['start'] >= nearest['start'] and peak['end'] <= nearest['end']:
feature = 'gene_body'
else:
feature = 'distal'
annotated.append({
'nearest_gene': nearest['gene_name'],
'feature': feature,
'distance_to_tss': int(dist),
})
ann_df = pd.DataFrame(annotated)
features = ann_df['feature'].value_counts()
print(f" Annotation distribution: {dict(features)}")
assert len(ann_df) == 50
def test_28_classify_peak_regions():
"""Peak Annotation: Classify peaks into genomic regions"""
features = pd.Series(['promoter', 'promoter', 'gene_body', 'gene_body', 'gene_body',
'distal', 'distal', 'distal', 'distal', 'intergenic'])
counts = features.value_counts().to_dict()
assert counts['promoter'] == 2
assert counts['gene_body'] == 3
assert counts['distal'] == 4
assert counts['intergenic'] == 1
fractions = {k: v / len(features) for k, v in counts.items()}
assert abs(fractions['promoter'] - 0.2) < 0.01
print(f" Region distribution: {fractions}")
# ============================================================
# Phase 8: Peak Overlap Tests
# ============================================================
def test_29_simple_overlap():
"""Overlap: Detect overlapping intervals"""
peaks_a = pd.DataFrame({
'chrom': ['chr1', 'chr1', 'chr2'],
'start': [100, 500, 200],
'end': [300, 700, 400],
})
peaks_b = pd.DataFrame({
'chrom': ['chr1', 'chr1', 'chr2'],
'start': [200, 800, 300],
'end': [400, 900, 500],
})
overlaps = []
for chrom in peaks_a['chrom'].unique():
a_chr = peaks_a[peaks_a['chrom'] == chrom]
b_chr = peaks_b[peaks_b['chrom'] == chrom]
for _, a in a_chr.iterrows():
for _, b in b_chr.iterrows():
if b['start'] < a['end'] and b['end'] > a['start']:
overlap_bp = min(a['end'], b['end']) - max(a['start'], b['start'])
overlaps.append({
'chrom': chrom,
'a_start': a['start'],
'b_start': b['start'],
'overlap_bp': overlap_bp,
})
assert len(overlaps) == 2, f"Expected 2 overlaps, got {len(overlaps)}"
# chr1: 100-300 overlaps 200-400 (overlap: 200-300 = 100bp)
# chr2: 200-400 overlaps 300-500 (overlap: 300-400 = 100bp)
print(f" Found {len(overlaps)} overlaps")
for o in overlaps:
print(f" {o['chrom']}: {o['overlap_bp']}bp overlap")
def test_30_no_overlap():
"""Overlap: Detect no overlap between non-overlapping intervals"""
peaks_a = pd.DataFrame({
'chrom': ['chr1'],
'start': [100],
'end': [200],
})
peaks_b = pd.DataFrame({
'chrom': ['chr1'],
'start': [300],
'end': [400],
})
has_overlap = False
for _, a in peaks_a.iterrows():
for _, b in peaks_b.iterrows():
if a['chrom'] == b['chrom'] and b['start'] < a['end'] and b['end'] > a['start']:
has_overlap = True
assert not has_overlap, "Should be no overlap"
print(f" Correctly detected no overlap")
# ============================================================
# Phase 9: ATAC-seq Tests
# ============================================================
def test_31_atac_peak_stats():
"""ATAC-seq: Nucleosome-free region statistics"""
peaks = generate_bed_file(n_peaks=500, seed=42)
peaks['length'] = peaks['end'] - peaks['start']
nfr = peaks[peaks['length'] < 150]
nucleosome = peaks[peaks['length'] >= 150]
nfr_fraction = len(nfr) / len(peaks)
print(f" NFR peaks (<150bp): {len(nfr)}")
print(f" Nucleosome peaks (>=150bp): {len(nucleosome)}")
print(f" NFR fraction: {nfr_fraction:.2%}")
assert len(nfr) + len(nucleosome) == 500
# ============================================================
# Phase 10: Multi-Omics Integration Tests
# ============================================================
def test_32_methylation_expression_correlation():
"""Multi-Omics: Methylation-expression correlation"""
np.random.seed(42)
n_samples = 30
# Generate correlated methylation and expression
noise = np.random.normal(0, 0.1, n_samples)
meth_vals = np.random.beta(2, 5, n_samples)
expr_vals = 10 - 8 * meth_vals + noise # Negative correlation
corr, pval = stats.pearsonr(meth_vals, expr_vals)
assert corr < -0.3, f"Expected negative correlation, got {corr}"
assert pval < 0.05, f"Expected significant, got p={pval}"
print(f" Pearson r = {corr:.3f}, p = {pval:.2e}")
def test_33_multi_probe_gene_correlation():
"""Multi-Omics: Multiple probe-gene pair correlations"""
np.random.seed(42)
n_samples = 30
n_pairs = 50
pvals = []
for i in range(n_pairs):
meth = np.random.beta(2, 5, n_samples)
if i < 10: # First 10 are truly correlated
expr = 10 - 8 * meth + np.random.normal(0, 0.5, n_samples)
else:
expr = np.random.normal(5, 2, n_samples)
_, pval = stats.pearsonr(meth, expr)
pvals.append(pval)
reject, padj, _, _ = mt.multipletests(pvals, method='fdr_bh')
n_sig = sum(reject)
print(f" Significant correlations: {n_sig} / {n_pairs}")
assert n_sig >= 5, "Should detect correlated pairs"
# ============================================================
# Phase 11: Clinical Integration Tests
# ============================================================
def test_34_missing_data_analysis():
"""Clinical: Missing data analysis across modalities"""
np.random.seed(42)
# Clinical data: 100 patients, some missing vital_status
clinical = pd.DataFrame({
'vital_status': np.random.choice(['Alive', 'Dead', np.nan], size=100,
p=[0.4, 0.4, 0.2]),
'age': np.random.uniform(30, 80, 100),
}, index=[f'P{i:03d}' for i in range(100)])
# Expression: 80 patients have data
expr_samples = [f'P{i:03d}' for i in range(80)]
expression = pd.DataFrame(
np.random.normal(5, 2, (100, 80)),
index=[f'GENE_{i}' for i in range(100)],
columns=expr_samples
)
# Methylation: 70 patients have data
meth_samples = [f'P{i:03d}' for i in range(30)] + [f'P{i:03d}' for i in range(50, 90)]
methylation = pd.DataFrame(
np.random.beta(2, 5, (200, 70)),
index=[f'cg{i:08d}' for i in range(200)],
columns=meth_samples
)
# Find complete cases
has_vital = set(clinical[clinical['vital_status'].notna()].index)
has_expr = set(expression.columns)
has_meth = set(methylation.columns)
complete = has_vital & has_expr & has_meth
print(f" Clinical with vital_status: {len(has_vital)}")
print(f" Expression samples: {len(has_expr)}")
print(f" Methylation samples: {len(has_meth)}")
print(f" Complete cases: {len(complete)}")
assert len(complete) > 0, "Should have some complete cases"
assert len(complete) < 100, "Should not have all patients complete"
def test_35_sample_id_matching():
"""Clinical: Match sample IDs across modalities"""
# Different ID formats
clinical_ids = {f'TCGA-AB-{i:04d}' for i in range(100)}
expr_ids = {f'TCGA-AB-{i:04d}-01A' for i in range(80)}
meth_ids = {f'TCGA-AB-{i:04d}-01A' for i in range(70)}
# Truncation matching
def truncate_id(full_id, length=12):
return full_id[:length]
clinical_short = {truncate_id(i) for i in clinical_ids}
expr_short = {truncate_id(i) for i in expr_ids}
meth_short = {truncate_id(i) for i in meth_ids}
overlap = clinical_short & expr_short & meth_short
print(f" Clinical: {len(clinical_ids)} patients")
print(f" Expression: {len(expr_ids)} samples")
print(f" Methylation: {len(meth_ids)} samples")
print(f" Matched after ID truncation: {len(overlap)}")
assert len(overlap) > 0
# ============================================================
# Phase 12: Genome-Wide Statistics Tests
# ============================================================
def test_36_global_methylation_stats():
"""Genome-Wide: Global methylation statistics"""
beta = generate_methylation_data(n_probes=500, n_samples=20)
stats_result = {
'total_probes': len(beta),
'total_samples': beta.shape[1],
'global_mean': float(beta.mean().mean()),
'global_median': float(beta.median().median()),
'global_std': float(beta.values[~np.isnan(beta.values)].std()),
}
assert stats_result['total_probes'] == 500
assert stats_result['total_samples'] == 20
assert 0 < stats_result['global_mean'] < 1
assert 0 < stats_result['global_std'] < 0.5
print(f" Mean: {stats_result['global_mean']:.3f}")
print(f" Median: {stats_result['global_median']:.3f}")
print(f" Std: {stats_result['global_std']:.3f}")
def test_37_probe_variance_distribution():
"""Genome-Wide: Probe variance distribution"""
beta = generate_methylation_data(n_probes=500, n_samples=20)
probe_var = beta.var(axis=1, skipna=True)
n_high = (probe_var > 0.01).sum()
n_very_high = (probe_var > 0.05).sum()
print(f" Variance > 0.01: {n_high} probes")
print(f" Variance > 0.05: {n_very_high} probes")
print(f" Mean variance: {probe_var.mean():.4f}")
print(f" Max variance: {probe_var.max():.4f}")
assert probe_var.mean() > 0
def test_38_per_sample_statistics():
"""Genome-Wide: Per-sample methylation statistics"""
beta = generate_methylation_data(n_probes=500, n_samples=20)
sample_means = beta.mean(axis=0)
sample_stds = beta.std(axis=0)
assert len(sample_means) == 20
assert all(sample_means > 0)
assert all(sample_means < 1)
print(f" Sample mean range: [{sample_means.min():.3f}, {sample_means.max():.3f}]")
print(f" Sample std range: [{sample_stds.min():.3f}, {sample_stds.max():.3f}]")
# ============================================================
# Phase 13: Multiple Testing Correction Tests
# ============================================================
def test_39_bh_correction():
"""Multiple Testing: Benjamini-Hochberg FDR"""
np.random.seed(42)
pvals = np.concatenate([
np.random.uniform(0, 0.001, 20), # 20 true positives
np.random.uniform(0, 1, 980), # 980 nulls
])
reject, padj, _, _ = mt.multipletests(pvals, method='fdr_bh')
n_sig = sum(reject)
print(f" BH significant: {n_sig} / 1000")
assert n_sig >= 10, "Should detect most true positives"
assert n_sig <= 100, "Should not have too many false positives"
def test_40_bonferroni_correction():
"""Multiple Testing: Bonferroni correction"""
np.random.seed(42)
pvals = np.concatenate([
np.random.uniform(0, 0.0001, 10),
np.random.uniform(0, 1, 990),
])
reject_bh, _, _, _ = mt.multipletests(pvals, method='fdr_bh')
reject_bonf, _, _, _ = mt.multipletests(pvals, method='bonferroni')
n_bh = sum(reject_bh)
n_bonf = sum(reject_bonf)
print(f" BH: {n_bh}, Bonferroni: {n_bonf}")
assert n_bonf <= n_bh, "Bonferroni should be more conservative"
# ============================================================
# Phase 14: Manifest Processing Tests
# ============================================================
def test_41_manifest_generation():
"""Manifest: Generate and validate probe manifest"""
probes = [f"cg{str(i).zfill(8)}" for i in range(100)]
manifest = generate_manifest(probes)
assert len(manifest) == 100
assert 'probe_id' in manifest.columns
assert 'chr' in manifest.columns
assert 'position' in manifest.columns
assert 'gene_name' in manifest.columns
print(f" Manifest: {len(manifest)} probes, columns: {list(manifest.columns)}")
def test_42_manifest_chromosome_mapping():
"""Manifest: Map probes to chromosomes"""
probes = [f"cg{str(i).zfill(8)}" for i in range(200)]
manifest = generate_manifest(probes)
def normalize_chromosome(c):
c = str(c).strip()
return f'chr{c}' if not str(c).startswith('chr') else c
manifest['chr_normalized'] = manifest['chr'].apply(normalize_chromosome)
chr_counts = manifest['chr_normalized'].value_counts()
assert len(chr_counts) > 5, "Should map to multiple chromosomes"
print(f" Mapped to {len(chr_counts)} chromosomes")
print(f" Top 3: {dict(chr_counts.head(3))}")
def test_43_cpg_island_relation():
"""Manifest: Filter by CpG island relation"""
probes = [f"cg{str(i).zfill(8)}" for i in range(200)]
manifest = generate_manifest(probes)
island_probes = manifest[manifest['cpg_island_relation'] == 'Island']
shore_probes = manifest[manifest['cpg_island_relation'].str.contains('Shore', na=False)]
open_sea = manifest[manifest['cpg_island_relation'] == 'OpenSea']
print(f" Island: {len(island_probes)}")
print(f" Shore: {len(shore_probes)}")
print(f" OpenSea: {len(open_sea)}")
assert len(island_probes) + len(shore_probes) + len(open_sea) <= 200
# ============================================================
# Phase 15: ToolUniverse Integration Tests
# ============================================================
def test_44_tooluniverse_loading():
"""ToolUniverse: Load tools and verify epigenomics-related tools exist"""
from tooluniverse import ToolUniverse
tu = ToolUniverse()
tu.load_tools()
required = [
'ensembl_lookup_gene',
'ensembl_get_regulatory_features',
'SCREEN_get_regulatory_elements',
'ENCODE_search_experiments',
'ChIPAtlas_get_experiments',
'jaspar_search_matrices',
'ReMap_get_transcription_factor_binding',
'RegulomeDB_query_variant',
]
all_tools = set(tu.all_tool_dict.keys())
missing = [t for t in required if t not in all_tools]
assert len(missing) == 0, f"Missing tools: {missing}"
print(f" All {len(required)} required tools present in {len(all_tools)} total tools")
def test_45_ensembl_gene_lookup():
"""ToolUniverse: Ensembl gene lookup for TP53"""
from tooluniverse import ToolUniverse
tu = ToolUniverse()
tu.load_tools()
result = tu.tools.ensembl_lookup_gene(id='TP53', species='homo_sapiens')
assert result is not None, "No result from Ensembl"
if isinstance(result, dict):
data = result.get('data', result)
if isinstance(data, dict):
seq_region = data.get('seq_region_name', '')
print(f" TP53 chromosome: {seq_region}")
print(f" TP53 start: {data.get('start', 'N/A')}")
print(f" TP53 end: {data.get('end', 'N/A')}")
def test_46_screen_regulatory_elements():
"""ToolUniverse: SCREEN cis-regulatory elements for TP53"""
from tooluniverse import ToolUniverse
tu = ToolUniverse()
tu.load_tools()
result = tu.tools.SCREEN_get_regulatory_elements(
gene_name="TP53", element_type="enhancer", limit=5
)
assert result is not None, "No SCREEN result"
print(f" SCREEN result type: {type(result).__name__}")
if isinstance(result, dict):
keys = list(result.keys())[:5]
print(f" Keys: {keys}")
def test_47_encode_experiment_search():
"""ToolUniverse: ENCODE experiment search for H3K27ac"""
from tooluniverse import ToolUniverse
tu = ToolUniverse()
tu.load_tools()
result = tu.tools.ENCODE_search_experiments(
assay_title="ChIP-seq",
target="H3K27ac",
organism="Homo sapiens",
limit=3
)
assert result is not None, "No ENCODE result"
print(f" ENCODE result type: {type(result).__name__}")
def test_48_chipatlas_experiments():
"""ToolUniverse: ChIPAtlas experiment search"""
from tooluniverse import ToolUniverse
tu = ToolUniverse()
tu.load_tools()
result = tu.tools.ChIPAtlas_get_experiments(
operation="get_experiment_list",
genome="hg38",
antigen="CTCF",
limit=5
)
assert result is not None, "No ChIPAtlas result"
print(f" ChIPAtlas result type: {type(result).__name__}")
if isinstance(result, dict):
keys = list(result.keys())[:5]
print(f" Keys: {keys}")
def test_49_ensembl_regulatory_features():
"""ToolUniverse: Ensembl regulatory features for TP53 region"""
from tooluniverse import ToolUniverse
tu = ToolUniverse()
tu.load_tools()
result = tu.tools.ensembl_get_regulatory_features(
region="17:7661779-7687550",
feature="regulatory",
species="human"
)
assert result is not None, "No Ensembl regulatory result"
print(f" Ensembl regulatory result type: {type(result).__name__}")
if isinstance(result, dict):
data = result.get('data', result)
if isinstance(data, list):
print(f" Found {len(data)} regulatory features")
# ============================================================
# Phase 16: BixBench-Style Question Tests
# ============================================================
def test_50_bixbench_complete_cases():
"""BixBench: How many patients have no missing data for all modalities?"""
np.random.seed(42)
# Setup data similar to BixBench scenario
patients = [f'P{i:03d}' for i in range(100)]
clinical = pd.DataFrame({
'vital_status': np.random.choice(['Alive', 'Dead', None], 100, p=[0.45, 0.45, 0.1]),
'age': np.random.uniform(30, 80, 100),
}, index=patients)
expr_patients = patients[:85] # 85 have expression data
meth_patients = patients[10:95] # 85 have methylation data
# Complete cases
has_vital = set(clinical[clinical['vital_status'].notna()].index)
complete = has_vital & set(expr_patients) & set(meth_patients)
print(f" Question: Patients with complete data?")
print(f" Answer: {len(complete)}")
assert len(complete) > 0
def test_51_bixbench_cpg_density_ratio():
"""BixBench: Chromosome density ratio of age-related CpGs"""
# Simulated age-related CpGs mapped to chromosomes
np.random.seed(42)
n_cpgs = 1000
probs = [0.08, 0.07, 0.06, 0.06, 0.06, 0.05, 0.05, 0.04, 0.04, 0.04,
0.04, 0.04, 0.03, 0.03, 0.03, 0.03, 0.03, 0.03, 0.06, 0.03,
0.02, 0.02]
probs = [p / sum(probs) for p in probs] # Normalize to sum to 1.0
chr_assignments = np.random.choice(
[f'chr{i}' for i in range(1, 23)],
size=n_cpgs,
p=probs
)
chr_lengths = {
'chr1': 248956422, 'chr2': 242193529, 'chr3': 198295559,
'chr4': 190214555, 'chr5': 181538259, 'chr6': 170805979,
'chr7': 159345973, 'chr8': 145138636, 'chr9': 138394717,
'chr10': 133797422, 'chr11': 135086622, 'chr12': 133275309,
'chr13': 114364328, 'chr14': 107043718, 'chr15': 101991189,
'chr16': 90338345, 'chr17': 83257441, 'chr18': 80373285,
'chr19': 58617616, 'chr20': 64444167, 'chr21': 46709983,
'chr22': 50818468,
}
chr_counts = pd.Series(chr_assignments).value_counts()
densities = {}
for chrom, count in chr_counts.items():
if chrom in chr_lengths:
densities[chrom] = count / chr_lengths[chrom]
# Ratio of chr19/chr1
if 'chr19' in densities and 'chr1' in densities:
ratio = densities['chr19'] / densities['chr1']
print(f" Question: chr19/chr1 density ratio?")
print(f" chr19 density: {densities['chr19']:.2e}")
print(f" chr1 density: {densities['chr1']:.2e}")
print(f" Answer: {ratio:.2f}")
else:
print(f" Densities: {densities}")
def test_52_bixbench_genome_wide_density():
"""BixBench: Genome-wide average CpG density"""
np.random.seed(42)
chr_lengths = {
'chr1': 248956422, 'chr2': 242193529, 'chr3': 198295559,
'chr19': 58617616, 'chr22': 50818468,
}
chr_cpg_counts = {'chr1': 500, 'chr2': 400, 'chr3': 350, 'chr19': 200, 'chr22': 100}
total_cpgs = sum(chr_cpg_counts.values())
total_length = sum(chr_lengths.values())
density = total_cpgs / total_length
print(f" Question: Genome-wide average density?")
print(f" Total CpGs: {total_cpgs}")
print(f" Total genome: {total_length:,} bp")
print(f" Answer: {density:.2e} CpGs/bp")
assert density > 0
def test_53_bixbench_sig_dmp_count():
"""BixBench: How many CpG sites show significant differential methylation?"""
np.random.seed(42)
n_probes = 500
n_per_group = 10
g1 = np.random.beta(2, 5, size=(n_probes, n_per_group))
g2 = np.random.beta(2, 5, size=(n_probes, n_per_group))
g2[:50, :] += 0.25
g2 = np.clip(g2, 0, 1)
pvals = []
for i in range(n_probes):
_, pval = stats.ttest_ind(g1[i], g2[i], equal_var=False)
pvals.append(pval)
reject, padj, _, _ = mt.multipletests(pvals, method='fdr_bh')
n_sig = sum(reject)
print(f" Question: How many significant DMPs (padj < 0.05)?")
print(f" Answer: {n_sig} / {n_probes}")
assert n_sig > 10, "Should detect significant DMPs"
def test_54_bixbench_chr_specific_delta():
"""BixBench: Average beta difference on chromosome 17"""
np.random.seed(42)
# Simulated data: 100 probes on chr17
probes_chr17 = 100
g1 = np.random.beta(2, 5, size=(probes_chr17, 10))
g2 = np.random.beta(3, 4, size=(probes_chr17, 10))
delta_betas = g2.mean(axis=1) - g1.mean(axis=1)
avg_delta = delta_betas.mean()
print(f" Question: Average beta difference on chr17?")
print(f" Answer: {avg_delta:.4f}")
assert isinstance(avg_delta, float)
# ============================================================
# Phase 17: Edge Case Tests
# ============================================================
def test_55_empty_dataframe():
"""Edge Case: Handle empty DataFrame"""
empty_df = pd.DataFrame()
assert len(empty_df) == 0
assert empty_df.shape == (0, 0)
print(f" Empty DataFrame handled correctly")
def test_56_single_sample():
"""Edge Case: Handle single-sample methylation data"""
np.random.seed(42)
beta = pd.DataFrame(
np.random.beta(2, 5, size=(100, 1)),
index=[f'cg{i:08d}' for i in range(100)],
columns=['SAMPLE_001']
)
global_mean = beta.mean().mean()
# With ddof=1 (pandas default), single sample variance is NaN (division by N-1=0)
# With ddof=0, single sample variance is 0.0
probe_var_ddof0 = beta.var(axis=1, ddof=0, skipna=True)
probe_var_ddof1 = beta.var(axis=1, ddof=1, skipna=True)
assert 0 < global_mean < 1
assert all(probe_var_ddof0 == 0), "Single sample should have zero variance (ddof=0)"
assert all(pd.isna(probe_var_ddof1)), "Single sample variance with ddof=1 should be NaN"
print(f" Single sample mean: {global_mean:.3f}, variance (ddof=0): all zero, variance (ddof=1): all NaN")
def test_57_all_nan_probe():
"""Edge Case: Handle probe with all NaN values"""
np.random.seed(42)
beta = generate_methylation_data(n_probes=10, n_samples=5)
beta.iloc[0, :] = np.nan # First probe all NaN
probe_mean = beta.mean(axis=1)
assert pd.isna(probe_mean.iloc[0]), "All-NaN probe should have NaN mean"
# Variance should also be NaN
probe_var = beta.var(axis=1, skipna=True)
assert pd.isna(probe_var.iloc[0]) or probe_var.iloc[0] == 0
print(f" All-NaN probe handled: mean={probe_mean.iloc[0]}, var={probe_var.iloc[0]}")
def test_58_large_chromosome_sort():
"""Edge Case: Sort chromosomes correctly (chr1, chr2, ..., chr10, chr22, chrX)"""
chroms = ['chr2', 'chr10', 'chr1', 'chr22', 'chrX', 'chr19']
def chr_sort_key(x):
c = x.replace('chr', '')
if c == 'X':
return 23
elif c == 'Y':
return 24
else:
return int(c)
sorted_chroms = sorted(chroms, key=chr_sort_key)
expected = ['chr1', 'chr2', 'chr10', 'chr19', 'chr22', 'chrX']
assert sorted_chroms == expected, f"Got {sorted_chroms}"
print(f" Chromosome sorting correct: {sorted_chroms}")
# ============================================================
# Main Runner
# ============================================================
def main():
print("=" * 70)
print("tooluniverse-epigenomics: Comprehensive Test Suite")
print("=" * 70)
print()
all_tests = [
# Phase 1: Methylation Data Loading
test_01_generate_and_load_beta_matrix,
test_02_detect_methylation_type,
test_03_beta_mvalue_conversion,
test_04_csv_methylation_io,
test_05_tsv_methylation_io,
# Phase 2: CpG Filtering
test_06_filter_by_probe_type,
test_07_filter_by_variance,
test_08_filter_by_missing_data,
test_09_filter_by_mean_beta_range,
test_10_filter_top_n_variable,
test_11_filter_by_chromosome,
# Phase 3: Differential Methylation
test_12_differential_methylation_ttest,
test_13_differential_methylation_wilcoxon,
test_14_identify_dmps_with_threshold,
test_15_hyper_hypo_classification,
# Phase 4: Age-Related CpGs
test_16_age_correlation,
test_17_spearman_correlation,
# Phase 5: Chromosome Statistics
test_18_chromosome_normalization,
test_19_chromosome_lengths,
test_20_cpg_density_per_chromosome,
test_21_genome_wide_average_density,
test_22_chromosome_density_ratio,
# Phase 6: BED/Peak Files
test_23_load_bed_dataframe,
test_24_peak_statistics,
test_25_peaks_per_chromosome,
test_26_write_and_read_bed,
# Phase 7: Peak Annotation
test_27_peak_to_gene_annotation,
test_28_classify_peak_regions,
# Phase 8: Peak Overlap
test_29_simple_overlap,
test_30_no_overlap,
# Phase 9: ATAC-seq
test_31_atac_peak_stats,
# Phase 10: Multi-Omics Integration
test_32_methylation_expression_correlation,
test_33_multi_probe_gene_correlation,
# Phase 11: Clinical Integration
test_34_missing_data_analysis,
test_35_sample_id_matching,
# Phase 12: Genome-Wide Statistics
test_36_global_methylation_stats,
test_37_probe_variance_distribution,
test_38_per_sample_statistics,
# Phase 13: Multiple Testing
test_39_bh_correction,
test_40_bonferroni_correction,
# Phase 14: Manifest Processing
test_41_manifest_generation,
test_42_manifest_chromosome_mapping,
test_43_cpg_island_relation,
# Phase 15: ToolUniverse Integration
test_44_tooluniverse_loading,
test_45_ensembl_gene_lookup,
test_46_screen_regulatory_elements,
test_47_encode_experiment_search,
test_48_chipatlas_experiments,
test_49_ensembl_regulatory_features,
# Phase 16: BixBench-Style Questions
test_50_bixbench_complete_cases,
test_51_bixbench_cpg_density_ratio,
test_52_bixbench_genome_wide_density,
test_53_bixbench_sig_dmp_count,
test_54_bixbench_chr_specific_delta,
# Phase 17: Edge Cases
test_55_empty_dataframe,
test_56_single_sample,
test_57_all_nan_probe,
test_58_large_chromosome_sort,
]
for test_func in all_tests:
run_test(test_func)
print()
# Summary
print("=" * 70)
print("TEST SUMMARY")
print("=" * 70)
passed = sum(1 for r in RESULTS if r['status'] == 'PASS')
failed = sum(1 for r in RESULTS if r['status'] == 'FAIL')
total = len(RESULTS)
total_time = sum(r['time'] for r in RESULTS)
print(f"Total: {total} | Passed: {passed} | Failed: {failed} | Time: {total_time:.1f}s")
print(f"Pass Rate: {passed}/{total} ({100*passed/total:.1f}%)")
print()
if failed > 0:
print("FAILED TESTS:")
for r in RESULTS:
if r['status'] == 'FAIL':
print(f" {r['name']}: {r['error']}")
print()
return 0 if failed == 0 else 1
if __name__ == "__main__":
sys.exit(main())
ToolUniverse Tool Parameter Reference
Detailed parameter specifications and return schemas for all ToolUniverse tools used in the epigenomics skill.
---
Regulatory Annotation Tools
| Tool | Parameters | Returns |
|---|---|---|
ensembl_lookup_gene | id: str, species: str (REQUIRED) | {status, data: {id, display_name, seq_region_name, start, end, strand, biotype}, url} |
ensembl_get_regulatory_features | region: str (no "chr"), feature: str, species: str | {status, data: [...features...]} |
ensembl_get_overlap_features | region: str, feature: str, species: str | Gene/transcript overlap data |
SCREEN_get_regulatory_elements | gene_name: str, element_type: str, limit: int | cCREs (enhancers, promoters, insulators) |
ReMap_get_transcription_factor_binding | gene_name: str, cell_type: str, limit: int | TF binding sites |
RegulomeDB_query_variant | rsid: str | {status, data, url} regulatory score |
jaspar_search_matrices | search: str, collection: str, species: str | {count, results: [...matrices...]} |
ENCODE_search_experiments | assay_title: str, target: str, organism: str, limit: int | Experiment metadata |
ChIPAtlas_get_experiments | operation: str (REQUIRED: "get_experiment_list"), genome: str, antigen: str, cell_type: str, limit: int | Experiment list |
ChIPAtlas_search_datasets | operation REQUIRED, antigenList/celltypeList | Dataset search results |
ChIPAtlas_enrichment_analysis | Various input types (BED regions, motifs, genes) | Enrichment results |
ChIPAtlas_get_peak_data | operation REQUIRED | Peak data download URLs |
FourDN_search_data | operation: str (REQUIRED: "search_data"), assay_title: str, limit: int | Chromatin conformation data |
Gene Annotation Tools
| Tool | Parameters | Returns |
|---|---|---|
MyGene_query_genes | query: str | {hits: [{_id, symbol, ensembl, ...}]} |
MyGene_batch_query | gene_ids: list[str], fields: str | {results: [{query, symbol, ...}]} |
HGNC_fetch_gene_by_symbol | symbol: str | Gene symbol, aliases, IDs |
GO_get_annotations_for_gene | gene_id: str | GO annotations |
Sequencing Data Retrieval Tools (SRA)
| Tool | Parameters | Returns |
|---|---|---|
SRA_search_experiments | query: str, organism: str (e.g. "Homo sapiens"), library_strategy: str ("ChIP-Seq", "Bisulfite-Seq", "ATAC-seq", "RNA-Seq"), platform: str ("ILLUMINA"), limit: int (default 10) | {data: {total, returned, query_used, experiments: [{uid, title, organism, platform, library_strategy, experiment_accession, study_accession, bioproject, runs}]}} |
SRA_get_experiment | accession: str (SRX/ERX/DRX/SRP/ERP/DRP/SRS/ERS/DRS) | Full experiment metadata with title, organism, platform, library info, runs |
Use cases: Finding raw ChIP-seq/Bisulfite-Seq/ATAC-seq experiments for cross-study comparison, identifying available datasets for a tissue/condition, retrieving SRA accessions for data download.
---
Critical Tool Notes
- ensembl_lookup_gene: REQUIRES
species='homo_sapiens'parameter -- will fail without it - ensembl_get_regulatory_features: Region format is
"17:start-end"(NO "chr" prefix) - ChIPAtlas tools: ALL require
operationparameter (SOAP-style API) - FourDN tools: ALL require
operationparameter (SOAP-style API) - SCREEN: Returns JSON-LD format with
@context, @graphkeys - ENCODE_search_experiments:
assay_titlemust be"TF ChIP-seq"not"ChIP-seq"(see Feature-73B) - RegulomeDB_query_variant: Use
genome=GRCh38notassembly=hg19; test with real rsIDs like rs4994
Related skills
How it compares
Pick tooluniverse-epigenomics for methylation and chromatin analysis rather than tooluniverse-chemical-compound-retrieval for small-molecule lookup.
FAQ
What data types does tooluniverse-epigenomics handle?
tooluniverse-epigenomics handles DNA methylation beta matrices, long-format CpG CSVs, ChIP-seq BED and narrowPeak files, ATAC-seq peaks, and multi-omics missing-data integration. It uses pandas, scipy, and pysam plus ENCODE, GTEx, and GEO ToolUniverse annotation.
What is the rows versus sites rule?
tooluniverse-epigenomics distinguishes row counts from unique CpG positions in long-format methylation CSVs with one row per sample and site. Filtering questions about sites removed usually mean rows removed, which can differ by orders of magnitude from unique-position counts.