# Bio Applied Promoter

> Detect TATA box/Inr/DPE promoter elements, call CpG islands (O/E ratio), and build/score PWMs for TFBS scanning. Use when finding a TATA box, calling CpG islands, or scanning a sequence for transcription factor binding sites.

- Skill: `pavel-kravchenko/bio-applied-promoter` (Agent Skill)
- Install (CLI): `npx skillmds@latest add pavel-kravchenko/bio-applied-promoter`
- Raw SKILL.md: https://api.skillmd.com/api/skills/pavel-kravchenko/bio-applied-promoter/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: pavel-kravchenko (https://skillmd.com/u/pavel-kravchenko)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/pavel-kravchenko/bio-applied-promoter

---


# Core Promoter Elements

## When to Use

- Annotating the core promoter architecture (TATA box, Inr, DPE) around a known or predicted TSS
- Classifying a gene's promoter as CpG-island-associated (housekeeping-like) vs CpG-poor (tissue-specific)
- Building a position weight matrix (PWM) from a set of aligned transcription factor binding sites
- Scanning a promoter or genomic region for candidate TFBS hits and picking a score threshold
- Fetching a known TF motif (e.g. CTCF, from JASPAR) instead of building one from scratch

For GRN-level context (linking TFBS hits to target genes, network construction), see `bio-applied-regulatory-analysis`.

## Version Compatibility

Pure Python/NumPy — no version-sensitive APIs. Written against Python ≥3.9, NumPy ≥1.24. Optional: `pyjaspar` ≥3.0 (JASPAR2024 release) for fetching curated motifs.

## Prerequisites

- `numpy`, `pandas` for numeric/PWM work
- `pyjaspar` if pulling motifs from JASPAR instead of building your own PPM/PWM
- Familiarity with PFM → PPM → PWM (log-odds) pipeline — see `bio-core-sequence-motifs` / `bio-core-motif-discovery` for the general motif-discovery background this specializes

## Promoter Architecture

| Element | Position | Notes |
|---------|----------|-------|
| TATA box | −25 to −35 | TATAAA consensus; ~10–20% of human genes |
| Initiator (Inr) | +1 | Present in TATA-less promoters |
| DPE | +28 to +32 | Downstream promoter element; compensates for absent TATA |
| CpG island | −200 to +100 | ~70% of human promoters; mark of constitutive expression |

## TATA Box Detection

**Goal:** flag candidate TATA boxes in a TSS-relative promoter sequence.
**Approach:** regex match on the consensus (strict) or the IUPAC-degenerate TATAWAW pattern (loose), then filter by expected position (−25 to −35 from TSS) to cut false positives.

```python
import re

def find_tata_boxes(sequence, strict=True):
    """Find TATA box matches in a sequence.

    strict=True:  exact TATAAA consensus.
    strict=False: TATAWAW (IUPAC W = A/T), catches degenerate boxes.
    Returns list of (0-based start, matched string).
    """
    pattern = 'TATAAA' if strict else r'TATA[AT]A[AT]'
    return [(m.start(), m.group())
            for m in re.finditer(pattern, sequence.upper())]

def filter_by_tss_position(hits, seq_len, tss_offset, window=(-35, -25)):
    """Keep only hits whose position (relative to TSS at tss_offset) falls in window."""
    lo, hi = window
    return [(pos, seq) for pos, seq in hits
            if lo <= (pos - tss_offset) <= hi]
```

## CpG Island Scanner

**Goal:** identify CpG islands using the classic Gardiner-Garden & Frommer (1987) criteria.
**Approach:** slide a window across the sequence, compute GC content and observed/expected CpG ratio, and flag windows passing both thresholds (length ≥200 bp, GC ≥50%, O/E ≥0.6).

```python
def cpg_island_scanner(sequence, window=200, step=50, gc_thresh=0.5, oe_thresh=0.6):
    """Scan for CpG islands.

    CpG O/E = (N_CpG * window_length) / (N_C * N_G) -- NOT (N_C+N_G)^2/4.
    Returns list of (start, end, gc_fraction, oe_ratio) for windows passing
    both the GC and O/E thresholds.
    """
    sequence = sequence.upper()
    islands = []
    for i in range(0, len(sequence) - window + 1, step):
        win = sequence[i:i + window]
        n_c, n_g, n_cpg = win.count('C'), win.count('G'), win.count('CG')
        gc = (n_c + n_g) / window
        oe = (n_cpg * window) / (n_c * n_g) if n_c > 0 and n_g > 0 else 0.0
        if gc >= gc_thresh and oe >= oe_thresh:
            islands.append((i, i + window, round(gc, 3), round(oe, 3)))
    return islands
```

## PWM Construction and Scanning

**Goal:** turn a set of aligned binding-site strings into a scoring matrix, then scan a sequence for high-scoring matches.
**Approach:** count bases per column (PFM) → add a pseudocount and normalize (PPM) → take log2-odds against a background model (PWM). Score every window of the target sequence by summing per-position log-odds.

```python
import numpy as np

def build_pwm(sites, pseudocount=0.5, bg=None):
    """Build a log2-odds PWM directly from aligned binding-site strings.

    sites: list of equal-length strings (e.g. known TF binding sites).
    bg: background base frequencies, default uniform (0.25 each).
    Returns dict {base: [log2-odds per position]}.
    """
    bg = bg or {b: 0.25 for b in 'ACGT'}
    n, L = len(sites), len(sites[0])
    counts = {b: [0] * L for b in 'ACGT'}
    for site in sites:
        for i, b in enumerate(site.upper()):
            counts[b][i] += 1
    pwm = {}
    for b in 'ACGT':
        pwm[b] = [np.log2((counts[b][i] + pseudocount) /
                           (n + 4 * pseudocount) / bg[b])
                  for i in range(L)]
    return pwm

def scan_pwm(sequence, pwm, threshold=0.0):
    """Slide the PWM across sequence, returning (pos, window, score) for hits >= threshold."""
    L = len(pwm['A'])
    sequence = sequence.upper()
    hits = []
    for i in range(len(sequence) - L + 1):
        window = sequence[i:i + L]
        score = sum(pwm.get(b, [0] * L)[j] for j, b in enumerate(window))
        if score >= threshold:
            hits.append((i, window, score))
    return hits
```

To use a curated motif instead of building one from scratch, fetch it from JASPAR:

```python
from pyjaspar import jaspardb  # pip install pyjaspar

jdb = jaspardb(release="JASPAR2024")
motif = jdb.fetch_motif_by_id("MA0139.1")  # CTCF
print(motif.name, motif.counts)  # motif.counts -> PFM you can feed into build_pwm-style logic
```

## Pitfalls

- **CpG O/E formula**: denominator is `N_C * N_G`, not `(N_C + N_G)^2 / 4` — use the Gardiner-Garden & Frommer (1987) formula shown above
- **Zero counts in PFM → −∞ PWM**: always use pseudocounts (0.5 is standard); this is the most common source of `-inf` scores
- **TATA box false positives**: random AT-rich sequences generate TATAAA by chance; always cross-check the −25 to −35 position window relative to the TSS
- **Coordinate systems**: BED = 0-based half-open; VCF/GFF = 1-based inclusive — mismatched conventions silently shift every position by one
- **Scanner step size**: `step=1` gives exact island boundaries; `step=50` can miss islands smaller than the step or blur their true edges

## See Also

- `bio-applied-regulatory-analysis` — GRN context, linking TFBS hits to target genes
- `bio-core-motif-discovery` — de novo motif discovery (MEME-style EM), significance thresholds
- `bio-core-sequence-motifs` — general PFM/PPM/PWM background and information content
- `jaspar-database` — querying JASPAR for curated TF motifs

