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 dfgff = 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
- Which genes overlap? (Distance < 0)
- Which genes are closest together? (shortest non-overlapping distance, then find its downstream neighbour)
- 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
- Create a copy of the original DataFrame.
- Identify every genomic feature that occurs after the insertion point on the selected chromosome. Where ‘Start’ > ‘position’
- Shift all downstream features by the length of the inserted sequence. Remember that both the Start and End columns represent genomic coordinates.
- Create a new row describing the inserted gene
- Add the new gene to the existing annotation
- Restore the genomic order - the new gene is automatically added to the end, it must be placed according to the genomic coordinates.
- 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)