pythonforbio.
[WASM idle]
Core Concepts02/06

Coordinate Systems

Starting sandbox…
Beginnerlesson

Coordinate Systems

In one line: genomics uses two incompatible position conventions, and mixing them is the most common off-by-one bug in the field.

The two systems

1-based, fully closed ("interbase off"): the first base is position 1, and an interval [start, end] includes both endpoints. A single base at position 5 is 5-5. Length is end - start + 1.

0-based, half-open: the first base is position 0, and an interval [start, end) includes the start but excludes the end. A single base at index 5 is [5, 6). Length is end - start.

sequence:    A   C   G   T
1-based:     1   2   3   4
0-based:   0   1   2   3   4     ← coordinates sit *between* bases

The 0-based system is sometimes called "interbase" because the numbers label the gaps between bases, not the bases themselves.

Which format uses which

Format / tool System
BED 0-based, half-open
BAM (binary fields) 0-based
GFF / GTF 1-based, closed
VCF 1-based, closed
SAM (text POS field) 1-based
HGVS 1-based, closed
Python slicing / NumPy 0-based, half-open
Ensembl API 1-based
UCSC browser display 1-based (but its BED files are 0-based)

That last row is the classic trap: UCSC shows you 1-based coordinates but exports 0-based BED.

Converting

# 1-based closed  →  0-based half-open  (i.e. to a Python slice)
start0, end0 = start1 - 1, end1

# 0-based half-open  →  1-based closed
start1, end1 = start0 + 1, end0

seq[start1 - 1 : end1]   # extracting a GFF/VCF feature from a Python string

Only the start shifts. The end is numerically identical. That asymmetry is why the bug is so easy to make and so easy to miss — half of your test cases will pass.

Biopython does this for you

SeqFeature.location always stores 0-based half-open internally, regardless of the source format. A GFF feature at 1-based 100-200 becomes [99:200] in Biopython. When you print a location you see [99:200], and feature.extract(record.seq) gives the right subsequence.

Do not add or subtract 1 yourself when working with a SeqFeature — Biopython already did. See SeqFeature and Location.

hgvs and the extra subtleties

HGVS is 1-based, but c. coordinates carry two extra complications:

  • There is no position 0. In c. numbering, position 1 is the A of the initiator ATG, and the base immediately before it is c.-1, not c.0. Likewise *1 is the first base after the stop codon.
  • Intronic offsets. c.87+3 means "3 bases into the intron after coding position 87"; c.88-3 means "3 bases before coding position 88". These are offset values on the position object, not part of the base number.

hgvs models this with BaseOffsetPosition (fields base, offset, datum). Never do naive arithmetic on .base. See SequenceVariant and hgvs Pitfalls.

Insertions are where everyone dies

An insertion has no width, so where does it go?

  • HGVS: c.100_101insA — names the two flanking bases. Unambiguous.
  • VCF: POS=100 REF=A ALT=AT — anchors on the preceding base and includes it in both alleles.
  • BED: start == end — a zero-width interval.

Converting between these is not a coordinate shift, it is a representation change. Use hgvs Normalizer and a real VCF library rather than string surgery.

Practical defences

  • Name your variables start0 / start1, never bare start.
  • Write one assertion per interval-producing function: assert end >= start for half-open, assert end >= start and length == end - start + 1 for closed.
  • Test with a single-base feature and a two-base feature. Off-by-ones survive tests on long intervals.
  • Test at a chromosome start (position 1 / index 0). Underflow shows up there.

See also

SeqFeature and Location · Applied - GFF and Genomic Intervals · Applied - Genomic File Formats · hgvs Pitfalls · HGVS Nomenclature · Indexing and Slicing

scratch

No output yet — run the code to populate this drawer.