Contents Menu Expand Light mode Dark mode Auto light/dark, in light mode Auto light/dark, in dark mode Skip to content
GFFBase
Logo

Getting started

  • Installation
  • Quickstart
  • Compatibility & strict modes

Using GFFBase

  • Usage Gallery — every public method, copy-pasteable
  • Analysis gallery
  • Command line
  • Connections & concurrency
  • Migrating from gffutils to gffbase
  • Cookbooks
    • GENCODE / Ensembl GTF Cookbook
    • NCBI RefSeq GFF3 Cookbook
    • MANE (Matched Annotation from NCBI and EBI) Cookbook
    • Machine Learning Workflows Cookbook

Background

  • Performance
  • Benchmark methodology
  • Datasets
  • Architecture
  • Schema v2, multipart features, and the compat/strict axis

Reference

  • API reference
  • Frequently asked questions
  • Troubleshooting
  • Citation
  • License
  • Contact

Project

  • Testing and release gates
  • Roadmap
  • Changelog
  • Contributing to GFFBase
  • Security Policy
  • Security advisory — SQL injection via order_by and set_pragmas
Back to top
View this page
Edit this page

Analysis gallery¶

Complete analyses, each one a real question with the code that answers it and the output it actually produces. Every Python block on this page runs in CI against the vendored tests/data/hierarchy.gff3 fixture, so the numbers below are what the code prints — not what it printed once.

For per-method snippets see the Usage gallery; for corpus-specific workflows see the Cookbooks.

The setup every example below assumes:

from gffbase import create_db

db = create_db(HIERARCHY_PATH, str(TMP / "gallery.duckdb"), force=True)

Extract splice sites¶

Every donor and acceptor in the annotation — the input a splice-site model like Splam or OpenSpliceAI trains on. An intron is the gap between consecutive exons of the same transcript, so this is one sort and one pairwise walk.

for tx in db.features_of_type("mRNA"):
    exons = sorted(db.children(tx.id, featuretype="exon"), key=lambda e: e.start)
    for left, right in zip(exons, exons[1:]):
        print(
            f"{tx.id}  donor {left.end + 1}  acceptor {right.start - 1}  "
            f"intron {right.start - left.end - 1} bp"
        )
t1  donor 201  acceptor 499  intron 299 bp

Tip

At whole-genome scale, batch it

On a real annotation, fetch every exon in one query instead of looping: db.children_batched(transcript_ids, featuretype="exon", format="arrow") returns an Arrow table with an anchor column naming each row’s transcript, so you can group without re-querying. See Machine learning workflows.

create_introns() gives you the intron features directly if you want them as Feature objects rather than coordinates.


Pick the longest isoform per gene¶

The standard reduction to one transcript per gene — by exonic length, not genomic span, which is what children_bp measures.

for gene in db.features_of_type("gene"):
    isoforms = list(db.children(gene.id, featuretype="mRNA"))
    longest = max(isoforms, key=lambda t: db.children_bp(t, child_featuretype="exon"))
    span = db.children_bp(longest, child_featuretype="exon")
    print(f"{gene.id}: {longest.id} ({span} exonic bp of {len(isoforms)} isoforms)")
g1: t1 (202 exonic bp of 2 isoforms)

For a curated answer rather than a computed one, MANE ships exactly one representative transcript per gene — see MANE.


Exon-count distribution¶

Single-exon genes behave differently from multi-exon ones in almost every analysis, so this histogram is worth looking at before trusting a result.

from collections import Counter

counts = Counter(
    len(list(db.children(t.id, featuretype="exon")))
    for t in db.features_of_type("mRNA")
)
for n_exons, n_tx in sorted(counts.items()):
    print(f"{n_exons} exon(s): {n_tx} transcript(s)")
1 exon(s): 1 transcript(s)
2 exon(s): 1 transcript(s)

Coding fraction per transcript¶

How much of each transcript is protein-coding. A transcript with no CDS is a non-coding isoform, which this makes visible rather than silently averaging away.

for tx in db.features_of_type("mRNA"):
    coding = db.children_bp(tx, child_featuretype="CDS")
    exonic = db.children_bp(tx, child_featuretype="exon")
    pct = 100 * coding / exonic if exonic else 0
    print(f"{tx.id}: {coding}/{exonic} bp coding ({pct:.0f}%)")
t1: 162/202 bp coding (80%)
t2: 0/201 bp coding (0%)

Hand the whole annotation to pandas¶

When the analysis is columnar, skip Feature objects entirely. to_table() takes the same filters as all_features() and returns Arrow, pandas or polars.

exons = db.to_table("exon", format="arrow")
print(exons.num_rows, "exons;", exons.schema.names[:6])
3 exons; ['id', 'seqid', 'source', 'featuretype', 'start', 'end']

Restrict to a locus with limit=, and get a dataframe instead:

region = db.to_table("exon", limit=("chr1", 100, 300), format="df")
print(len(region), "exons in chr1:100-300")

What is in an unfamiliar annotation?¶

Before any of the above, from the shell:

gffbase stats annotations.duckdb

Feature counts by type, sequence count and names, and how the database was built. See Command line.


Where to go next¶

  • Cookbooks — the same techniques against GENCODE, RefSeq and MANE, at whole-genome scale.

  • Machine learning workflows — bulk extraction into PyTorch and Hugging Face with no per-row Python.

  • Usage gallery — one snippet per public method.

Next
Command line
Previous
Usage Gallery — every public method, copy-pasteable
Copyright © 2026, Kuan-Hao Chao
Made with Sphinx and @pradyunsg's Furo
On this page
  • Analysis gallery
    • Extract splice sites
    • Pick the longest isoform per gene
    • Exon-count distribution
    • Coding fraction per transcript
    • Hand the whole annotation to pandas
    • What is in an unfamiliar annotation?
    • Where to go next