epigenomics
SkillDev toolsToolUniverse workflow — Epigenomics
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the epigenomics skill
What this skill tells your AI
The instructions your AI receives, as published by lamm-mit/scienceclaw in skills/epigenomics/SKILL.md and read by ahel’s review.
name: tooluniverse-epigenomics description: Production-ready genomics and epigenomics data processing for BixBench questions. Handles methylation array analysis (CpG filtering, differential methylation, age-related CpG detection, chromosome-level density), ChIP-seq peak analysis (peak calling, motif enrichment, coverage stats), ATAC-seq chromatin accessibility, multi-omics integration (expression + methylation correlation), and genome-wide statistics. Pure Python computation (pandas, scipy, numpy, pysam, statsmodels) plus ToolUniverse annotation tools (Ensembl, ENCODE, SCREEN, JASPAR, ReMap, RegulomeDB, ChIPAtlas). Supports BED, BigWig, methylation beta-value matrices, Illumina manifest files, and multi-sample clinical data. Use when processing methylation data, ChIP-seq peaks, ATAC-seq signals, or answering questions about CpG sites, differential methylation, chromatin accessibility, histone marks, or epigenomic statistics.
Genomics and Epigenomics Data Processing
Production-ready computational skill for processing and analyzing epigenomics data. Combines local Python computation (pandas, scipy, numpy, pysam, statsmodels) with ToolUniverse annotation tools for regulatory context. Designed to solve BixBench-style questions about methylation, ChIP-seq, ATAC-seq, and multi-omics integration.
When to Use This Skill
Triggers:
- User provides methylation data (beta-value matrices, Illumina arrays) and asks about CpG sites
- Questions about differential methylation analysis
- Age-related CpG detection or epigenetic clock questions
- Chromosome-level methylation density or statistics
- ChIP-seq peak files (BED format) with analysis questions
- ATAC-seq chromatin accessibility questions
- Multi-omics integration (expression + methylation, expression + ChIP-seq)
- Genome-wide epigenomic statistics
- Questions mentioning "methylation", "CpG", "ChIP-seq", "ATAC-seq", "histone", "chromatin", "epigenetic"
- Questions about missing data across clinical/genomic/epigenomic modalities
- Regulatory element annotation for processed epigenomic data
Example Questions This Skill Solves:
- "How many patients have no missing data for vital status, gene expression, and methylation data?"
- "What is the ratio of filtered age-related CpG density between chromosomes?"
- "What is the genome-wide average chromosomal density of unique age-related CpGs per base pair?"
- "How many CpG sites show significant differential methylation (padj < 0.05)?"
- "What is the Pearson correlation between methylation and expression for gene X?"
- "How many ChIP-seq peaks overlap with promoter regions?"
- "What fraction of ATAC-seq peaks are in enhancer regions?"
- "Which chromosome has the highest density of hypermethylated CpGs?"
- "Filter CpG sites by variance > threshold and map to nearest genes"
- "What is the average beta value difference between tumor and normal for chromosome 17?"
NOT for (use other skills instead):
- Gene regulation lookup without data files -> Use existing epigenomics annotation pattern
- RNA-seq differential expression -> Use
tooluniverse-rnaseq-deseq2 - Variant calling/annotation from VCF -> Use
tooluniverse-variant-analysis - Gene enrichment analysis -> Use
tooluniverse-gene-enrichment - Protein structure analysis -> Use
tooluniverse-protein-structure-retrieval
Required Python Packages
# Core (MUST be available)
import pandas as pd
import numpy as np
from scipy import stats
import statsmodels.stats.multitest as mt
# Optional but useful
import pysam # BAM/CRAM file access
import gseapy # Enrichment of genes from methylation analysis
# ToolUniverse (for annotation)
from tooluniverse import ToolUniverse
KEY PRINCIPLES
- Data-first approach - Load and inspect data files BEFORE any analysis
- Question-driven - Parse what the user is actually asking and extract the specific numeric answer
- File format detection - Automatically detect methylation arrays, BED files, BigWig, clinical data
- Coordinate system awareness - Track genome build (hg19, hg38, mm10), handle chr prefix differences
- Statistical rigor - Proper multiple testing correction, effect size filtering, sample size awareness
- Missing data handling - Explicitly report and handle NaN/missing values
- Chromosome normalization - Always normalize chromosome names (chr1 vs 1, chrX vs X)
- CpG site identification - Parse Illumina probe IDs (cg/ch probes), genomic coordinates
- Report-first - Create output file first, populate progressively
- English-first queries - Use English in all tool calls
Complete Workflow
Phase 0: Question Parsing and Data Discovery
CRITICAL FIRST STEP: Before writing ANY code, parse the question to identify what is being asked and what data files are available.
0.1 Discover Available Data Files
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'])]
# Print summary
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}")
0.2 Parse Question Parameters
Extract these from the question:
| 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" |
0.3 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
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 tab first, then comma
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 Methylation Manifest / Probe Annotation
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
"""
# Try reading manifest - may have header rows to skip
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
# Normalize column names
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 chromosome sizes (main chromosomes)
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), filter probes with extreme mean beta
- 'missing_threshold': float (0-1), max fraction of NaN allowed per probe
- 'chromosomes': list, keep only these chromosomes
- 'exclude_sex_chr': bool, remove chrX and chrY
- 'probe_type': 'cg' or 'ch', keep only one type
- '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)
# Probe type filter (cg vs ch)
if 'probe_type' in filters:
ptype = filters['probe_type']
probe_mask &= df.index.str.startswith(ptype)
# Missing data filter
if 'missing_threshold' in filters:
threshold = filters['missing_threshold']
missing_frac = df.isna().mean(axis=1)
probe_mask &= missing_frac <= threshold
# Variance filter
if 'variance_threshold' in filters:
var_threshold = filters['variance_threshold']
probe_var = df.var(axis=1, skipna=True)
probe_mask &= probe_var >= var_threshold
# Mean beta range filter
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)
# Top N most variable
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)
# Manifest-based filters
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
# Chromosome filter
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)
# Exclude sex chromosomes
if filters.get('exclude_sex_chr', False) and 'chr' in manifest_indexed.columns:
sex_chr = ['chrX', 'chrY', 'X', 'Y']
nonsex_probes = manifest_indexed[
~manifest_indexed['chr'].apply(normalize_chromosome).isin(['chrX', 'chrY'])
].index
probe_mask &= df.index.isin(nonsex_probes)
# CpG island relation filter
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)
# Gene group filter (TSS200, Body, etc.)
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_df
1.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.
Args:
beta_df: Beta-value matrix (probes x samples)
group1_samples: list of sample IDs for group 1
group2_samples: list of sample IDs for group 2
test: 'ttest', 'wilcoxon', or 'ks' (Kolmogorov-Smirnov)
correction: multiple testing correction method
alpha: significance threshold
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')
# Multiple testing correction
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).
Args:
dm_results: Output from differential_methylation()
alpha: adjusted p-value threshold
delta_beta_threshold: minimum absolute beta-value difference
Returns:
DataFrame of significant 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.
Args:
beta_df: Beta-value matrix (probes x samples)
ages: Series or array of ages corresponding to samples
method: 'correlation' (Pearson/Spearman) or 'regression'
correction: multiple testing method
alpha: significance threshold
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')
# Multiple testing correction
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
1.6 Chromosome-Level Methylation Statistics
def chromosome_cpg_density(cpg_probes, manifest, genome='hg38'):
"""Calculate CpG density per chromosome.
Args:
cpg_probes: list/Index of CpG probe IDs
manifest: probe annotation with chr and position columns
genome: genome build for chromosome lengths
Returns:
DataFrame with chr, n_cpgs, chr_length, density (CpGs per bp)
"""
chr_lengths = get_chromosome_lengths(genome)
# Map probes to chromosomes
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
# Get chromosome for each probe
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")
# Count probes per chromosome
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.
Args:
density_df: Output from chromosome_cpg_density()
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 242
- Forks
- 42
- Last commit
- Aug 2026
Advanced
- Catalog kind
- skill
- Gateway key
epigenomics- Source
- github.com/lamm-mit/scienceclaw