Querying GFF

UBDS 2026: Basic Python

Day 4 : Using Pandas for Genome Annotation

One of the most common file formats used in bioinformatics is the General Feature Format (GFF) file. A GFF file describes the locations of genomic features such as genes, exons, coding sequences (CDS), regulatory regions and many other annotations.

Each row represents a genomic feature and contains information such as: | Column | Description | | ———- | ———————————————————— | | Chromosome | Which chromosome or contig the feature belongs to | | Source | The software or database that produced the annotation | | Feature | The type of feature (gene, exon, CDS, mRNA…) | | Start | Start coordinate | | End | End coordinate | | Score | Confidence score (often “.”) | | Strand | + or - strand | | Frame | Reading frame for CDS features | | Attributes | Additional information such as gene names and transcript IDs |

A simplified example looks like: |Chromosome |Source |Feature |Start |End |Score |Strand |Frame |Attributes | |———–|——-|——–|——|—-|——|——-|——|————| |chr1 |RefSeq |gene |1000 |2500 |. |+ |. |gene=BRCA1 | |chr1 |RefSeq |exon |1000 |1200 |. |+ |. |Parent=BRCA1| |chr1 |RefSeq |CDS |1100 |2400 |. |+ |0 |Parent=BRCA1|

In my own research, I use GFF files extensively when analysing genome annotations. For example, I use them to:

  • Use the coordinates to extract the sequence of a gene from a genome fasta file.
  • Calculate distances between neighbouring genes
  • Compare annotations produced by different software
  • Locate genes that overlap RNA-seq evidence

Because Pandas makes it easy to filter, sort and manipulate tables, it has become one of the most useful tools for working with genome annotations. It is the fatest way for me to access this information.

EXERCISE 1

Load a gff file to see the different features annotated in the genome of Encephalitozoon intestinalis

Unlike a CSV file, a GFF file: - is tab-separated - contains comment lines beginning with # - usually has no column headers

import pandas as pd

# Create a function for reading a gff file using pandas
def load_gff(filename):

    df = pd.read_csv(
        filename,
        sep="\t",
        comment="#",
        header=None
    )

    df.columns = [
        "Chromosome",
        "Source",
        "Feature",
        "Start",
        "End",
        "Score",
        "Strand",
        "Frame",
        "Attributes"
    ]

    return df
gff = load_gff("Encephalitozoon_intestinalis.gff")

gff.head()
gff.info()
gff["Feature"].value_counts()

EXERCISE 2

Find the distance between neighboring genes.

First keep only the gene features - create a copy of the dataframe which have “gene” in the Feature column

genes = gff[gff["Feature"] == "gene"].copy()
print(genes)

Next, sort by chromosome and genomic position.

The example gff appears to be sorted correctly - but it is good practice to ensure that the genes are in the correct order along the chromosome.

genes = genes.sort_values(["Chromosome", "Start"])
print(genes)

Now, calculate the distance to the next gene.

genes["Next_gene_start"] = genes.groupby("Chromosome")["Start"].shift(-1)

genes["Distance"] = (
    genes["Next_gene_start"] - genes["End"]
)

print(genes)

Answer the folowing questions

  1. Which genes overlap? (Distance < 0)
  2. Which genes are closest together? (shortest non-overlapping distance, then find its downstream neighbour)
  3. What is the average distance between genes?
#### Your Code Here ####

EXERCISE 3

Dictionaries and DataFrames

GFF files often store many pieces of information inside a single Attributes column. A very common bioinformatics task is to use regular expressions to extract the specific field you need (such as Name, ID, Parent, or gene) before performing analysis with Pandas.

Suppose we want a dictionary mapping each gene to its genomic start position. First we will extract out the gene name using a regex expression.

Regex Part Meaning Matches
Name= Find the literal text Name= Name=
(…) Create a capture group (this is what will be returned)
[^;]+ Match one or more characters that are not semicolons GPK93_01g00010
genes['Gene'] = genes['Attributes'].str.extract(r"Name=([^;]+)")[0]

gene_dict = dict(zip(
    genes["Gene"],
    genes["Start"]
))

Finding the start of a gene is now straightforward.

gene_dict['GPK93_01g00360']

We can easily convert the filtered information into a new dataframe

start_df = pd.DataFrame(
    gene_dict.items(),
    columns=["Gene", "Start"]
)
print(start_df)

Exercise 4 – Updating a GFF after inserting a new gene

Imagine we have genetically modified an organism by inserting a new gene into its genome.

In this exercise, we will write a function that updates the genomic coordinates of all the downstream genes.

When DNA is inserted into a chromosome, the chromosome becomes longer. As a result, every genomic feature located after the insertion site (genes, exons, CDS features, transcripts, regulatory regions, etc.) must have its genomic coordinates updated.

Your task is to write a function that performs this modification automatically. Create the following function:

insert_gene(gff, chromosome, position, length, gene_name)

Parameter Description
gff A DataFrame containing the genome annotation
chromosome The chromosome where the new gene will be inserted
position The genomic coordinate where the insertion begins
length The length of the inserted gene (in nucleotides)
gene_name The name of the new gene

Steps

  1. Create a copy of the original DataFrame.
  2. Identify every genomic feature that occurs after the insertion point on the selected chromosome. Where ‘Start’ > ‘position’
  3. Shift all downstream features by the length of the inserted sequence. Remember that both the Start and End columns represent genomic coordinates.
  4. Create a new row describing the inserted gene
  5. Add the new gene to the existing annotation
  6. Restore the genomic order - the new gene is automatically added to the end, it must be placed according to the genomic coordinates.
  7. Reset the dataframe index.
# Helper Code 
# Create a dictionary for your new gene
new_gene = {
    "Chromosome": "ExampleChromosome",
    "Source": "Course",
    "Feature": "gene",
    "Start": "Examplenew_start",
    "End": "Examplenew_end",
    "Score": ".",
    "Strand": "+",
    "Frame": ".",
    "Attributes": "Name=ExampleGene"
}
# Convert dictionary to a one-row dataframe
new_gene = pd.DataFrame([new_gene])

# Add this row to your existing dataframe
modified_gff = pd.concat(
        [gff, new_gene],
        ignore_index=True
    )

# Save the edited dataframe to a new gff file 
modified_gff.to_csv(
    "modified_genome.gff",
    sep="\t",
    header=False,
    index=False
)
#### Your Code Here ####

Once your function is complete, try the following example

modified_gff = insert_gene(
    gff,
    chromosome="CP075158.1",
    position=180000,
    length=2100,
    gene_name="ExampleGene"
)
modified_gff.tail(50)