| name | bio-motif-search |
| description | Find sequence motifs, degenerate IUPAC patterns, and transcription-factor binding sites in DNA/RNA using Biopython and regex, including position weight matrix (PWM/PSSM) scoring. Use when locating regulatory elements, counting overlapping motif occurrences, scanning for binding-site matches above a significance threshold, or reading motif matrices from JASPAR/MEME/TRANSFAC files. For restriction enzyme sites, use restriction-analysis/restriction-sites. |
| tool_type | python |
| primary_tool | Bio.motifs |
Version Compatibility
Reference examples tested with: BioPython 1.83+
Before using code patterns, verify installed versions match. If versions differ:
- Python:
pip show <package> then help(module.function) to check signatures
If code throws ImportError, AttributeError, or TypeError, introspect the installed
package and adapt the example to match the actual API rather than retrying.
Motif Search
"Search for a sequence motif or binding-site pattern" -> Scan sequences for a fixed motif, a degenerate IUPAC consensus, or a probabilistic PWM, on one or both strands, and locate transcription-factor binding sites, regulatory elements, or custom patterns.
- Python:
Bio.SeqUtils.nt_search (IUPAC + overlaps), re (regex/lookahead), Bio.motifs (PWM/PSSM scoring + matrix file parsing)
The Governing Principle
Two silent failures dominate motif searching; both return a plausible-but-wrong answer with no error:
- Overlapping matches are dropped.
str.count, str.find, and re.findall consume the string left to right, so a motif that overlaps its own next occurrence is undercounted. Target AAGCGCGCGAA, motif GCGC: str.count returns 1, the true answer is 2 (starts at positions 2 and 4). Use a zero-width lookahead re.finditer(r'(?=(GCGC))', target) or Bio.SeqUtils.nt_search, both of which report overlaps.
- A PSSM score is a likelihood in bits, not a probability.
pssm.calculate returns log2-odds versus background. A "high-looking" threshold chosen by eye is arbitrary and non-reproducible; derive the threshold from the score distribution at a chosen false-positive rate. And a PSSM scans only the strand it is given, so scoring just the forward strand silently misses roughly half of real sites on double-stranded DNA.
Which Approach for Which Question
| Question | Tool |
|---|
| Position of first exact hit | str.find / Seq.find (returns -1 if absent) |
| All exact hits, possibly overlapping | re.finditer(r'(?=(motif))', seq) |
Degenerate IUPAC consensus (e.g. GATNNTC), with overlaps | Bio.SeqUtils.nt_search(seq, motif) |
| Flexible / repeat / variable-spacer pattern | re with explicit character classes and quantifiers |
| Graded match to many aligned sites (binding sites) | Bio.motifs PWM -> PSSM, score and threshold |
| Match significance / false-positive control | pssm.distribution(...).threshold_fpr(fpr) |
| Restriction enzyme recognition sites | restriction-analysis/restriction-sites |
IUPAC Degenerate Motifs
A degenerate motif expands each ambiguity code to a regex character class:
| Code | Class | Code | Class | Code | Class |
|---|
| N | [ACGT] | R | [AG] | Y | [CT] |
| W | [AT] | S | [GC] | K | [GT] |
| M | [AC] | B | [CGT] | D | [AGT] |
| H | [ACT] | V | [ACG] | | |
B, D, H, V each exclude A, C, G, T respectively (the code preceding the one they drop is a mnemonic).
IUPAC_DNA = {'N': '[ACGT]', 'R': '[AG]', 'Y': '[CT]', 'W': '[AT]', 'S': '[GC]',
'K': '[GT]', 'M': '[AC]', 'B': '[CGT]', 'D': '[AGT]', 'H': '[ACT]', 'V': '[ACG]'}
def iupac_to_regex(pattern):
return ''.join(IUPAC_DNA.get(base, base) for base in pattern)
Bio.SeqUtils.nt_search expands IUPAC ambiguity in the query motif automatically and reports overlapping hits, so it is the shortest correct path for a degenerate consensus.
Overlapping Matches (the count trap)
Goal: Report every start position of a motif, including self-overlapping occurrences.
Approach: Use a zero-width lookahead so the regex engine never consumes the matched text; recover the match string from the inner capture group. For IUPAC motifs, prefer nt_search, which both expands ambiguity and reports overlaps.
Reference (BioPython 1.83+):
import re
from Bio.SeqUtils import nt_search
target = 'AAGCGCGCGAA'
starts = [match.start(1) for match in re.finditer(r'(?=(GCGC))', target)]
hits = [(match.start(1), match.group(1)) for match in re.finditer(r'(?=([AG]CG[CT]))', target)]
result = nt_search(target, 'GCGC')
pattern, positions = result[0], result[1:]
nt_search returns a heterogeneous list: result[0] is the (expanded) pattern string and result[1:] are the 0-based start positions. When there are no hits it returns just [pattern] (length 1), so test len(result) > 1 before indexing rather than truthiness.
Bio.motifs PWM / PSSM Pipeline
Goal: Build a probabilistic model from a set of aligned binding sites and score a target sequence for graded matches.
Approach: Create a motif from instances or a matrix file, set pseudocounts and background, read the recomputed PSSM, then scan. The count matrix m.counts['A', 0] is indexed [base, position].
Reference (BioPython 1.83+):
from Bio import motifs
from Bio.Seq import Seq
m = motifs.create([Seq('TACAA'), Seq('TACGA'), Seq('TACTA'), Seq('TGCAA')])
m.pseudocounts = 0.5
m.background = None
pwm = m.pwm
pssm = m.pssm
m.consensus
m.degenerate_consensus
m.counts.normalize(pseudocounts=0.5) returns a position weight matrix and pwm.log_odds() returns a PSSM; these are equivalent to reading m.pwm / m.pssm after setting m.pseudocounts.
The Pseudocount / -inf Trap (silent)
A column where some base has count 0 gives that base frequency 0 and a log-odds of negative infinity; any target carrying that base at that position then scores -inf and is unmatchable. This is common with short motifs or few instances. Setting m.pseudocounts (a flat 0.5, or sqrt(N) with N the number of instances; scalar or per-base dict) makes every cell finite by shrinking toward background.
Critically, m.pssm is recomputed from m.pseudocounts and m.background on every access. Set both BEFORE reading m.pssm (or pwm.log_odds()), or the matrix is silently wrong.
Score, Threshold, and P-value
pssm.calculate(seq) returns the log2-odds score in bits for each window (a relative likelihood, not a probability). pssm.search(seq, threshold=...) yields (position, score) pairs at or above the threshold.
To convert a bit score into a false-positive rate, build the null distribution and ask it for a threshold:
dist = pssm.distribution(background=m.background, precision=10**4)
threshold = dist.threshold_fpr(0.01)
threshold = dist.threshold_fnr(0.1)
threshold = dist.threshold_balanced(1000)
Choosing a threshold "because it looks high" is the classic non-reproducible error. Higher precision gives finer threshold resolution at the cost of memory.
Both Strands
pssm.calculate scans only the strand it is handed. pssm.search defaults to both=True, scanning both strands in one call; with both=True a hit at negative position p lies on the reverse strand and its forward-coordinate start is len(seq) + p. To handle strands separately, set both=False and scan the reverse-complemented PSSM explicitly:
combined = list(pssm.search(seq, threshold=3.0))
rc_pssm = pssm.reverse_complement()
forward = list(pssm.search(seq, threshold=3.0, both=False))
reverse = list(rc_pssm.search(seq, threshold=3.0, both=False))
Reading Motif Matrix Files
motifs.read(handle, fmt) reads exactly one motif; motifs.parse(handle, fmt) returns an iterator over many. The format string must match the file layout exactly.
fmt string | File type / source |
|---|
jaspar | multi-motif JASPAR PFM collection (use parse) |
pfm | single JASPAR-style PFM (use read) |
pfm-four-columns | CIS-BP, HOMER, HOCOMOCO (A C G T as columns) |
pfm-four-rows | ScerTF, YeTFaSCo (A C G T as rows) |
sites | JASPAR sites file (use read) |
meme | MEME program output (use parse) |
minimal | MEME minimal text format |
transfac | TRANSFAC matrices |
mast, alignace, clusterbuster, xms | respective tool outputs |
'cisbp', 'homer', and 'hocomoco' are NOT valid strings; those databases use pfm-four-columns. The four-columns versus four-rows distinction is the most common mix-up: a 4-column matrix read as pfm-four-rows parses without error but produces a meaningless transposed motif.
from Bio import motifs
with open('collection.jaspar') as handle:
for m in motifs.parse(handle, 'jaspar'):
print(m.matrix_id, m.name, m.consensus)
m.format('jaspar')
m.format('transfac')
Common Motif Patterns
| Motif | Pattern | Description |
|---|
| Start codon | ATG | Translation initiation |
| Kozak | [AG]CCATGG | Eukaryotic translation initiation |
| TATA box | TATA[AT]A[AT] | Core promoter element |
| GC box (Sp1) | GGGCGG | Promoter element |
| CAAT box | CCAAT | Promoter element |
| Poly-A signal | AATAAA | mRNA polyadenylation |
| E-box (bHLH) | CA[ACGT]{2}TG | bHLH TF binding |
Common Errors
| Symptom | Cause | Fix |
|---|
| Count is too low | str.count/re.findall skip overlaps | re.finditer(r'(?=(motif))', seq) or nt_search |
IndexError on nt_search result | No hits returns [pattern] (length 1) | Test len(result) > 1 before reading result[1:] |
Every target scores -inf | Count-0 cell gives -inf log-odds | Set m.pseudocounts (0.5 or sqrt(N)) before reading m.pssm |
| PSSM scores look wrong | Pseudocounts/background set after reading m.pssm | Set them first; m.pssm is recomputed on each access |
| Roughly half of sites missed | Only forward strand scanned | Score pssm.reverse_complement() or pass both=True |
| Threshold not reproducible | Cutoff chosen by eye | pssm.distribution(...).threshold_fpr(fpr) |
ValueError parsing matrix | Wrong fmt (4-column vs 4-row, jaspar vs pfm) | Match fmt to the actual layout |
| No matches | Case or strand mismatch | .upper() both; check reverse complement |
Related Skills
- seq-objects - Create Seq objects for searching
- reverse-complement - Reverse-complement the target to search the opposite strand
- transcription-translation - ORF and codon-context motifs in coding sequences
- sequence-properties - GC content and per-sequence properties around hits
- restriction-analysis/restriction-sites - Restriction enzyme recognition sites
- chip-seq/motif-analysis - De novo motif discovery and enrichment in peak sets
- database-access/entrez-fetch - Download motif matrices from JASPAR/NCBI