CONTENTO
  • Lemon Lab
  1. Input Files
  2. Genome Annotations
  • Introduction
  • Input Files
    • SE File
    • Genome Annotations

On this page

  • Overview:
    • Annotation files for functional enrichment analysis
    • Additional Benefits of Annotation Files
    • Examples: Do I need an annotation file?
  • Step 1: Source Genome Annotations
    • 1.1 Ensembl-Based Organisms (Human, Mouse, other msigdbr Species)
    • 1.2 NCBI/RefSeq-Based Organisms (Bacteria and Other Non-Ensembl Genomes)
    • 1.3 Generate your own GFF3 file (Optional)
  • Step 2: Parse Source Annotations into a CONTENTO Annotation Data Frame
    • 2.1 From a GTF (Ensembl)
    • 2.2 From a GFF3 (NCBI/Prokka)
  • Step 3: Enhance Your Annotation File (Optional)
    • 3.1 Merge Custom Annotations
    • 3.2 Example: Merging RegPrecise Regulons
    • 3.3 Example: Enhance Annotations with Anvi’o
  • Step 4: Verify Your Annotation File

Create Genome Annotations for CONTENTO

Published

October 15, 2026

Overview:

Annotation files are optional in CONTENTO, but they enable many of the platform’s most useful features. Depending on your organism and the identifiers used in your SummarizedExperiment (SE) object, functionality may be limited without an annotation file. In particular, annotation files are used for:

  • Functional enrichment analysis (GSEA/GESECA)
  • Displaying informative gene labels and symbols
  • Genomic neighborhood analysis (bacterial genomes only)

Annotation files for functional enrichment analysis

CONTENTO can perform functional enrichment analysis using gene sets from two sources:

  1. Annotation-Based Gene Sets: Available for any organism through a user-provided annotation data frame (.rds format). Required elements:

    • A Geneid column matching the rownames() of the SE object, with exactly one row per Geneid. CONTENTO rejects annotation files with duplicated Geneid values.
    • One or more functional annotation columns containing gene set assignments (e.g. COG categories, KEGG pathways, GO terms, regulons, custom categories). Multiple values per gene must be collapsed into a single string separated by !!! (e.g., "GO:0006415!!!GO:0016149"). Do not use any other separator (,, ;, |, …): CONTENTO only splits on !!!, so "GO:0006415,GO:0016149" would be treated as one single category. CONTENTO counts the gene as belonging to each !!!-separated category for functional enrichment analysis. See Multiple annotations per gene for how to build these strings.
  2. MSigDB Gene Sets: Available only for human, mouse, and other species supported by msigdbr R package. It requires:

    • metadata(se)$organism must exactly match a valid msigdbr species name (e.g. Homo sapiens, Mus musculus, etc.).
    • Gene identifiers must be Ensembl gene IDs (ENSG..., ENSMUSG..., etc.), either: directly in rownames(se), or in a column of the annotation file containing Ensembl IDs. CONTENTO automatically detects Ensembl identifier columns when present.

Additional Benefits of Annotation Files

Annotation files also improve the general CONTENTO experience.

  • Gene symbols/labels can be displayed throughout tables and visualizations instead of raw gene identifiers. This is especially useful for bacteria, where Geneid is often a locus tag (e.g. SAUSA300_0001) and not an informative label like the associated gene name or product description.
  • Genomic neighborhood analysis, which additionally requires start, end, strand columns in the annotation file. This is a beta feature available only when metadata(se)$organism is exactly “Bacteria”

Examples: Do I need an annotation file?

SE organism SE rownames Desired Enrichment Annotation file needed?
Supported by msigdbr (e.g. Homo sapiens) Ensembl IDs MSigDB No (optional for gene labels)
Supported by msigdbr (e.g. Homo sapiens) Non-Ensembl IDs MSigDB Yes (must contain an Ensembl ID column)
Any organism Any identifiers Annotation-based enrichment Yes
Any organism Any identifiers None (Differential Expression only) No

Built-in annotations. CONTENTO ships ready-made annotation files (in inst/annotations, also included in the container) for:

Annotation Organism Genome
Homo_sapiens.GRCh38.113_annot Homo sapiens GRCh38, Ensembl release 113
Sau_KPL4403_annot Staphylococcus aureus LAC (USA300_FPR3757) GCF_000013465.1
Sau_KPL2115_annot Staphylococcus aureus JE2 GCF_002085525.1
Cac_KPL1818_annot Corynebacterium accolens KPL1818 GCF_000478135.1
Cps_KPL1989_annot Corynebacterium pseudodiphtheriticum KPL1989 GCF_000477955.1
Dpi_KPL3065_annot Dolosigranulum pigrum KPL3065 GCF_017655845.1

If your SE was quantified against one of these genomes, set metadata(se)$annotation to its name and CONTENTO loads it automatically; you don’t need to create your own. Use this tutorial for any other genome, or to add annotation columns of your own.


Step 1: Source Genome Annotations

The annotation file used by CONTENTO should be derived from the same genome annotation used during RNA-Seq alignment and quantification. Using the same annotation source ensures that:

  • Gene identifiers match the rownames() of your SE object.
  • Functional annotations can be merged without identifier conversion.
  • Gene coordinates used in the genomic neighborhood analyses are consistent with the reference genome used for alignment.

In practice, use the annotation files downloaded together with the genome used to build the alignment index. NCBI re-annotates RefSeq genomes periodically, so a new download of the same accession can have a slightly different set of genes than the one used for quantification.

The following sections describe how to obtain the source annotation files required to build a CONTENTO annotation data frame.

1.1 Ensembl-Based Organisms (Human, Mouse, other msigdbr Species)

For organisms distributed through Ensembl, download the same GTF and genome assembly used during RNA-seq analysis. The genome FASTA is not required to create a CONTENTO annotation file, but is often downloaded alongside the GTF because both are used during alignment, genome indexing, or other downstream analyses.

Example: Human GRCh38 (Ensembl Release 113)

FILES_DIR="inst/extdata/genomes" 
GENOME_NAME="Homo_sapiens.GRCh38" 
VERSION=113

# Gene annotation file used here and in the downstream analysis
wget -P "$FILES_DIR" \
  "https://ftp.ensembl.org/pub/release-${VERSION}/gtf/homo_sapiens/${GENOME_NAME}.${VERSION}.gtf.gz"

# Reference genome used in the downstream analysis (alignment/quantification)
wget -P "$FILES_DIR" \
  "https://ftp.ensembl.org/pub/release-${VERSION}/fasta/homo_sapiens/dna/${GENOME_NAME}.dna_sm.primary_assembly.fa.gz"

1.2 NCBI/RefSeq-Based Organisms (Bacteria and Other Non-Ensembl Genomes)

For organisms that are not maintained in Ensembl, the preferred annotation source is typically the NCBI RefSeq assembly. These annotations are distributed as GFF3 and GenBank (.gbff) files and contain gene coordinates, locus tags, product descriptions, and other metadata that can be incorporated into CONTENTO annotation files.

Option A: Download from the NCBI Assembly Website

  1. Go to NCBI Assembly
  2. Search for your genome accession (e.g., GCF_000013465.1 for Staphylococcus aureus subsp. aureus FPR3757)
  3. Download the assembly in GFF3 format
  4. Unzip and save in your working directory

Option B: Download Using the NCBI Datasets CLI

For reproducible workflows, the NCBI Datasets command-line tool is recommended.

# Install NCBI Datasets CLI if you haven't already. This should be run in your terminal.
conda activate ncbi_datasets 

# Update the genome accession and directory as needed
FILES_DIR="inst/extdata/genomes" 
GENOME_NAME="GCF_000013465.1" 

# Update to download the desired files (genome sequence, GenBank flat file, GFF3...)
datasets download genome accession "$GENOME_NAME" \
            --include genome,gbff,gff3,seq-report \
            --filename "$FILES_DIR/$GENOME_NAME.zip"
            
# Unzip
unzip "$FILES_DIR/$GENOME_NAME.zip" -d "$FILES_DIR"
# Individual files can be renamed to include the genome name for easier identification.

The accompanying GenBank file (.gbff) is optional for CONTENTO itself but is useful for downstream tools such as anvi’o, Prokka, or Bakta.

1.3 Generate your own GFF3 file (Optional)

For newly assembled genomes or organisms lacking public annotations, you may generate your own annotation files using tools such as Prokka or Bakta. These tools typically produce GFF3 files that can be processed using the same workflow described below. Regardless of the annotation source, the resulting GFF3 should contain stable gene identifiers (e.g. locus_tag) that correspond to the identifiers used in downstream RNA-Seq quantification.


Step 2: Parse Source Annotations into a CONTENTO Annotation Data Frame

IMPORTANT:

CONTENTO annotation files are standard R data frames saved as .rds files. The only universally required column is Geneid whose values must match the gene identifiers stored in rownames(se).

2.1 From a GTF (Ensembl)

We can import Geneid from the gene_id attribute in the GTF file using rtracklayer, along with other useful information such as gene name and biotype.

# Set up your R environment and read the GTF file. Adapt based on the GTF name and location
library(tidyverse)
library(here)
library(rtracklayer)

GENOMES_DIR <- here("../inst/extdata/genomes")
GENOME_NAME <- "Homo_sapiens.GRCh38.113"

# Import GTF file
gtf_data <- import(file.path(GENOMES_DIR, paste0(GENOME_NAME, ".gtf.gz")))

# Extract gene annotations. For available column names check colnames(mcols(gtf_data))
annotations <- data.frame(
  Geneid = mcols(gtf_data)$gene_id,
  gene = mcols(gtf_data)$gene_name,
  biotype = mcols(gtf_data)$gene_biotype
)
# Sanity check: Ensure there are no duplicated Gene IDs
annotations <- annotations[!duplicated(annotations$Geneid), ]

# Save the annotations to a CSV/RDS file
write_csv(annotations, file.path(GENOMES_DIR, paste0(GENOME_NAME, "_annot.csv")))
saveRDS(annotations, file.path(GENOMES_DIR, paste0(GENOME_NAME, "_annot.rds")))

2.2 From a GFF3 (NCBI/Prokka)

The GFF3 file contains the Geneid information as locus_tag, which we can parse along with other useful information from the attributes column.

# Set up your R environment and read the GFF3 file. Adapt based on the GFF3 name and location
library(tidyverse)
library(here)

GENOMES_DIR <- here("../inst/extdata/genomes")
GENOME_NAME <- "GCF_000013465.1"

gff_file <- file.path(GENOMES_DIR, paste0(GENOME_NAME, ".gff"))

# Read the GFF3 file into a data frame
gff_df <- read_delim(
  gff_file,
  delim = "\t",
  col_names = c("seqname", "source", "feature", "start", "end", "score", "strand", "frame", "attributes"),
  comment = "#",
  show_col_types = FALSE
)

# Keep the features with one row per gene: CDS (protein-coding genes and pseudogenes) and
# non-coding RNAs, so that every gene counted by featureCounts (-t gene) is annotated
gff_genes <- gff_df |>
  filter(feature %in% c("CDS", "tRNA", "rRNA", "tmRNA", "ncRNA", "SRP_RNA", "RNase_P_RNA"))

# Function to parse the attributes column of GFF3
parse_gff3_attributes <- function(attributes_string) {
  pairs <- str_split(attributes_string, ";")[[1]]
  
  attr_list <- list()
  
  for (pair in pairs) {
    if (pair == "") next
    
    parts <- str_split(pair, "=", n = 2)[[1]]
    
    if (length(parts) == 2) {
      key <- parts[1]
      value <- parts[2]
      attr_list[[key]] <- value
    }
  }
  
  return(attr_list)
}

# Apply the parsing function to the attributes column and create new columns for each key
annotations <- gff_genes |>
  rowwise() |>
  mutate(attr_list = list(parse_gff3_attributes(attributes)))  |>
  ungroup() |>
  unnest_wider(attr_list, names_repair = "universal") |> # Unnest all key=value pairs as columns
  select(-attributes) |>
  dplyr::rename(Geneid = locus_tag) # Required column for CONTENTO

# Replace commas in GO-related annotation fields with "!!!" to be used in multiple groupings in functional analysis
annotations <- annotations |>
  mutate(across(c(Ontology_term, go_function, go_process, go_component), ~ gsub(",", "!!!", .x)))

# Aggregate fragments into gene-level annotations (e.g. CDS with programmed frameshifts)
annotations <- annotations |>
  group_by(Geneid) |>
  dplyr::summarise(
    start  = min(start),
    end    = max(end),
    strand = unique(strand),   # should be invariant per gene
    across(
      -c(start, end, strand, frame),
      dplyr::first # explicit: rtracklayer (section 2.1) masks first()
    ),
    .groups = "drop"
  )

# Sanity check: Ensure there are no duplicated Gene IDs
stopifnot(!anyDuplicated(annotations$Geneid))

# Save the annotations to a CSV/RDS file
write_csv(annotations, file.path(GENOMES_DIR, paste0(GENOME_NAME, "_annot.csv")))
saveRDS(annotations, file.path(GENOMES_DIR, paste0(GENOME_NAME, "_annot.rds")))

Step 3: Enhance Your Annotation File (Optional)

The GTF/GFF3 derived file provides basic gene information, but you can significantly enhance your annotations by running additional bioinformatic tools.

These annotations are useful in CONTENTO for:

  • Functional enrichment analysis (GSEA) using for example COG and KEGG categories
  • Gene exploration - displaying comprehensive functional information for individual genes

3.1 Merge Custom Annotations

You can merge annotations from any source that contains gene identifiers (Gene IDs). These can be sourced from:

  • Public repositories
  • Your own experimental data (e.g., Tn-seq, RNA-seq, proteomics)
  • Manually curated annotations from literature searches
Found a Useful Annotation Source?

Open an issue on GitHub to share it with the community! Include the name, URL, and how you used it.

Requirements and Format

Key requirement: Your custom annotation file must have a column with gene identifiers that match the Geneid column in your GTF/GFF3 derived annotations. This column might have a different name in your custom file (e.g., “Geneid”, “Gene ID”, “Locus tag”, …), but it should contain the same values as Geneid.

Important notes:

  • Columns: Your custom annotation file can have as many additional columns as you want. All columns will be merged into the final dataframe, or you can select only a subset of columns before merging. In the merging step, you can add a prefix to custom annotation columns (e.g., RegPrecise_, reg_) to avoid name clashes and easily identify annotation sources.

  • Gene coverage: No requirement for the same number of genes as in the GTF/GFF3 derived annotations, as long as there is a common key (Geneid) for merging. Genes in GTF/GFF3 derived annotations without a corresponding custom annotation entry will be included in the final dataframe with NA values for the custom columns.

  • Multiple annotations per gene: See the dedicated section below. In short: one row per gene, multiple values joined with !!!.

Multiple annotations per gene

The final annotation file must have exactly one row per Geneid. If a Geneid appears in more than one row, CONTENTO refuses to load the file and shows an error listing some of the duplicated IDs (otherwise each duplicate would silently duplicate that gene’s differential expression results).

If a gene has several values in a column (several KEGG pathways, GO terms, COG categories, …), concatenate them into a single string separated by !!! (e.g., "Glycolysis / Gluconeogenesis!!!Carbon fixation"). !!! is the only separator CONTENTO recognises; values joined by commas, semicolons or pipes are treated as one single category. CONTENTO splits on !!! and counts the gene as belonging to each category for functional enrichment analysis.

Duplicates usually appear when a source file is in “long” format (one row per gene–annotation pair) or when a left_join() matches several rows per gene. Collapse such a table before merging it with your other annotations:

# Long format: one row per gene-pathway pair
#   locus_tag   KEGG_Pathway
#   gene_0001   Glycolysis / Gluconeogenesis
#   gene_0001   Carbon fixation
#   gene_0001   Carbon metabolism
#   gene_0002   Glycolysis / Gluconeogenesis

custom_annot <- custom_annot |>
  group_by(locus_tag) |>
  summarise(across(everything(),
                   ~ paste(unique(na.omit(.x)), collapse = "!!!")),
            .groups = "drop") |>
  mutate(across(everything(), ~ na_if(.x, "")))
#   locus_tag   KEGG_Pathway
#   gene_0001   Glycolysis / Gluconeogenesis!!!Carbon fixation!!!Carbon metabolism
#   gene_0002   Glycolysis / Gluconeogenesis

# If a column already uses another separator (e.g. commas), convert it to "!!!"
annotations <- annotations |>
  mutate(across(c(go_process, go_function), ~ gsub("\\s*,\\s*", "!!!", .x)))

Before saving the .rds, check that no Geneid is duplicated:

# Should return 0 rows. If not, collapse those genes as shown above.
annotations |> count(Geneid) |> filter(n > 1)

stopifnot(!anyDuplicated(annotations$Geneid))

3.2 Example: Merging RegPrecise Regulons

In this example, we demonstrate how to merge the RegPrecise regulon annotations with the GFF3-derived annotations. The RegPrecise file is tab-delimited and contains a locus_tag column that matches the Geneid column in the GFF3-derived annotations.

# Set up your R environment and read the RegPrecise file. Adapt based on the file name and location
library(tidyverse)
library(here)

GENOMES_DIR <- here("../inst/extdata/genomes")
GENOME_NAME <- "GCF_000013465.1"

# Load your GFF3-derived annotations
annotations <- read_csv(file.path(GENOMES_DIR, paste0(GENOME_NAME, "_annot.csv")))

# Load custom annotation file tab delimited
custom_annot <- read_delim(file.path(GENOMES_DIR, "USA300_FPR3757_RegPrecise.txt"))

# Collapse to one row per gene: genes controlled by several regulators become "Reg1!!!Reg2"
custom_annot <- custom_annot |>
  group_by(locus_tag) |>
  summarise(Regulator = paste(unique(na.omit(Regulator)), collapse = "!!!"), .groups = "drop")

# Merge the custom annotations with the GFF3-derived annotations using locus_tag as the key
annotations_merged <- annotations |>
  left_join(custom_annot, by = c("Geneid" = "locus_tag")) |> # Adjust to the "Geneid" column name in your custom annotation file
  rename_with(
    ~ paste0("RegPrecise_", .), # Prefix custom annotation columns to avoid name clashes with GFF3-derived annotations
    -all_of(colnames(annotations))
  )

# Sanity check: Ensure there are no duplicated Gene IDs (CONTENTO rejects files with duplicates)
stopifnot(!anyDuplicated(annotations_merged$Geneid))

# Save the annotations to a CSV/RDS file
output_file <- file.path(GENOMES_DIR, paste0(GENOME_NAME, "_annot_RegPrecise.csv"))
write_csv(annotations_merged, output_file)
saveRDS(annotations_merged, file.path(GENOMES_DIR, paste0(GENOME_NAME, "_annot_RegPrecise.rds"))) 

3.3 Example: Enhance Annotations with Anvi’o

Anvi’o is a comprehensive platform that integrates multiple annotation sources and can be used to generate a rich annotation file for bacterial genomes. It can be run on any genome in GenBank format and will produce a single annotation file with multiple functional annotations for each gene.

  • HMMs - Generic protein domain detection
  • COG24 - Clusters of Orthologous Groups (excellent for functional enrichment)
  • KEGG - Kyoto Encyclopedia of Genes and Genomes pathways (excellent for functional enrichment)
  • Pfams - Protein family annotations
  • CAZymes - Carbohydrate-active enzyme classification

Run Anvi’o Annotation Pipeline

For more details on how to run anvi’o see the Anvi’o documentation. If running on Windows, we recommend utilizing Ubuntu.

# Install Anvio if you haven't already. This should be run in your terminal.
conda activate anvio-9

# Update the genome accession and directory as needed
FILES_DIR="inst/extdata/genomes" 
GENOME_NAME="GCF_000013465.1" 

# Process the GenBank file to extract gene calls, functions, and fasta sequences 
anvi-script-process-genbank -i "$FILES_DIR/$GENOME_NAME.gbff" \
--output-gene-calls "$FILES_DIR/${GENOME_NAME}_calls.txt" \
--output-functions "$FILES_DIR/${GENOME_NAME}_prodigal.txt" \
--output-fasta "$FILES_DIR/${GENOME_NAME}.fa" \
--annotation-source prodigal \
--include-locus-tags-as-functions

# Create anvi'o contigs database from the fasta file and gene calls
anvi-gen-contigs-database -f "$FILES_DIR/${GENOME_NAME}.fa" \
-o "$FILES_DIR/${GENOME_NAME}_anvio.db" \
--external-gene-calls "$FILES_DIR/${GENOME_NAME}_calls.txt" \
-n "$GENOME_NAME"

# Import functions from prodigal output
anvi-import-functions -c "$FILES_DIR/${GENOME_NAME}_anvio.db" \
-i "$FILES_DIR/${GENOME_NAME}_prodigal.txt"

# Run HMM annotations 
anvi-run-hmms -T 8 -c "$FILES_DIR/${GENOME_NAME}_anvio.db"

# Run NCBI COG annotations with DIAMOND
anvi-run-ncbi-cogs -T 8 --search-with diamond -c "$FILES_DIR/${GENOME_NAME}_anvio.db"

# Run KEGG KOfam annotations
anvi-run-kegg-kofams -T 8 -E 1e-15 -c "$FILES_DIR/${GENOME_NAME}_anvio.db"

# Run Pfam domain annotations
anvi-run-pfams -T 8 -c "$FILES_DIR/${GENOME_NAME}_anvio.db"

# Run CAZyme annotations
anvi-run-cazymes -T 8 -c "$FILES_DIR/${GENOME_NAME}_anvio.db"

# Export all functions to text file
anvi-export-functions -c "$FILES_DIR/${GENOME_NAME}_anvio.db" \
-o "$FILES_DIR/${GENOME_NAME}_annot.txt"

# Export contigs stats and database info
anvi-display-contigs-stats "$FILES_DIR/${GENOME_NAME}_anvio.db" \
--report-as-text -o "$FILES_DIR/${GENOME_NAME}_stats.txt"

anvi-db-info "$FILES_DIR/${GENOME_NAME}_anvio.db" &>> "$FILES_DIR/${GENOME_NAME}_log.txt"

Parse Anvi’o Annotations into CSV

The resulting anvi’o annotation file contains comprehensive functional annotations for each gene, which can be parsed into a CSV format with the following R code.

# Set up your R environment and read the anvi'o annotation file. Adapt based on the file name and location
library(tidyverse)
library(here)

GENOMES_DIR <- here("../inst/extdata/genomes")
GENOME_NAME <- "GCF_000013465.1"

# Read the anvi'o annotation 
anvio_annot <- read_delim(file.path(GENOMES_DIR, paste0(GENOME_NAME, "_annot.txt")), delim = "\t") |>
  mutate(source = as.factor(source)) |>
  mutate(across(where(is.character), trimws)) |>
  select(-e_value) |> # Exclude e-values to keep dataframe simple
  dplyr::rename("acc" = "accession", "func" = "function") |> # Shorter names
  mutate(across(c(acc, func), trimws))

# Convert from long format (multiple rows per gene) to wide format (one row per gene)
anvio_annot <- anvio_annot |>
  pivot_wider(
    names_from = source,
    values_from = c(acc, func),
    values_fn = list(
      acc = ~ paste(unique(.), collapse = "!!!"), 
      func = ~ paste(unique(.), collapse = "!!!"))
  ) 

Parse KEGG Hierarchical Annotations

The KEGG annotations from anvi’o come as hierarchical levels separated by semicolons (e.g., “Metabolism; Carbohydrate metabolism; Glycolysis / Gluconeogenesis”). To facilitate functional enrichment analysis and gene exploration in CONTENTO, it’s useful to split these into separate columns for each level of the KEGG hierarchy (e.g., KEGG Group, KEGG Pathway). When a gene has several KEGG entries, anvi’o joins them with !!! (e.g., "Pathway modules; Amino acid metabolism; Lysine metabolism!!!Pathway modules; Amino acid metabolism; Arginine and proline metabolism"), so the entries are first split on !!! and only then on ;.

# Split multiple KEGG annotations into separate rows (one row per KEGG annotation) 
anvio_annot <- anvio_annot |>
  separate_rows(func_KEGG_Class, sep = "!!!")

# Split into hierarchy levels (separated by ";", dropping the space after it)
anvio_annot <- anvio_annot |>
  separate(func_KEGG_Class,
           into = c("func_KEGG_Class", "func_KEGG_Subclass", "func_KEGG_Pathway"),
           sep = ";\\s*",
           extra = "drop",
           fill = "right")

# Collapse back to one row per gene (the other columns, including func_KEGG_Module and func_KEGG_BRITE, are kept as they are)
anvio_annot <- anvio_annot |>
  group_by(across(-c(func_KEGG_Class, func_KEGG_Subclass, func_KEGG_Pathway))) |>
  summarise(
    func_KEGG_Class = paste(unique(na.omit(func_KEGG_Class)), collapse = "!!!"),
    func_KEGG_Subclass = paste(unique(na.omit(func_KEGG_Subclass)), collapse = "!!!"),
    func_KEGG_Pathway = paste(unique(na.omit(func_KEGG_Pathway)), collapse = "!!!"),
    .groups = "drop"
  ) |>
  mutate(across(starts_with("func_KEGG_"), ~ na_if(., "")))

Add AA Sequences to Annotations (Optional)

You can also merge the anvi’o annotations with the aa sequences from the calls file to have a comprehensive annotation dataframe with all the information for each gene. This can be useful for gene exploration in CONTENTO, where you can display the sequence along with the functional annotations for each gene.

# Read calls file to get aa sequences
anvio_calls <- read_delim(file.path(GENOMES_DIR, paste0(GENOME_NAME, "_calls.txt")), delim = "\t") |>
  select(gene_callers_id, aa_sequence)

# Merge with anvio_annot by gene_callers_id
anvio_annot <- anvio_annot |>
  left_join(anvio_calls, by = "gene_callers_id")

Merge Anvi’o Annotations

Finally, you should merge the anvi’o annotations with the GFF3-derived annotations and the custom annotations from RegPrecise to create a comprehensive annotation CSV file for CONTENTO.

# Load GFF3-derived annotations (with RegPrecise already merged, or use original if skipping section 3.2)
annotations <- read_csv(file.path(GENOMES_DIR, paste0(GENOME_NAME, "_annot_RegPrecise.csv")))

# Merge anvi'o annotations using GENBANK_LOCUS_TAG (the column added by anvi'o) as a key for merging, which should match the Geneid column in the GFF3-derived annotations. 
annotations_merged <- annotations |>
  left_join(anvio_annot, by = c("Geneid" = "func_GENBANK_LOCUS_TAG")) |> 
  rename_with(
    ~ paste0("anvio_", .), # Prefix anvio annotation columns 
    -all_of(colnames(annotations))  
  )

# Sanity check: Ensure there are no duplicated Gene IDs (CONTENTO rejects files with duplicates)
stopifnot(!anyDuplicated(annotations_merged$Geneid))

# Save the annotations to a CSV/RDS file
output_file <- file.path(GENOMES_DIR, paste0(GENOME_NAME, "_annot_RegPrecise_Anvio.csv"))
write_csv(annotations_merged, output_file)
saveRDS(annotations_merged, file.path(GENOMES_DIR, paste0(GENOME_NAME, "_annot_RegPrecise_Anvio.rds")))

Step 4: Verify Your Annotation File

Before uploading, confirm:

  • Geneid column exists, is character type, and matches rownames(SE) exactly (same values, same case).
  • Exactly one row per Geneid (!anyDuplicated(annot$Geneid) is TRUE). Files with duplicated Geneids are rejected by CONTENTO.
  • Functional columns (if any) use !!! — and only !!! — to separate multiple values per gene. Other separators (,, ;, |) are not split and the whole string becomes one category.
  • If you want MSigDB and your rownames aren’t Ensembl-style, at least one column contains Ensembl-style IDs (ENSG..., ENSMUSG..., etc.) — CONTENTO auto-detects this column.
  • For genomic neighborhood analysis (bacteria), start, end, and strand columns are present and numeric/character as appropriate.
  • File is saved as .rds containing a data frame or tibble (not a list).

If a feature is greyed out or missing in the app (e.g. MSigDB radio button absent, neighborhood tab missing), it’s almost always one of the above checks failing silently. Problems with the Geneid column (missing, not character, or duplicated) are the exception: CONTENTO shows an error and does not load the file.